diff --git a/.gitignore b/.gitignore index 5636c11..0e3bc18 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,15 @@ dev.sh -.idea/* +.idea +pyweb-3.0.py +__pycache__ +test/__pycache__/*.pyc +tests/.svn/* +py_web_tool.egg-info/* +*.pyc +*.aux +*.out +*.toc +v2_test +.tox +pyweb-3.0.py diff --git a/.nojekyll b/.nojekyll index 8b13789..139597f 100644 --- a/.nojekyll +++ b/.nojekyll @@ -1 +1,2 @@ + diff --git a/MANIFEST.in b/MANIFEST.in index 6bef1c8..04dda64 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,4 @@ -include *.w *.css *.html *.conf *.rst -include test/*.w test/*.css test/*.html test/*.conf test/*.py +include src/*.w src/*.css src/*.html src/*.conf src/*.rst src/*.py +include bootstrap/*.py +include tests/*.w tests/*.css tests/*.html tests/*.conf tests/*.py include jedit/*.xml diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..f2ed99f --- /dev/null +++ b/Makefile @@ -0,0 +1,56 @@ +# Makefile for py-web-tool. +# Requires a pyweb-3.0.py (untouched) to bootstrap the current version. + +SOURCE_PYLPWEB = src/pyweb.w src/intro.w src/overview.w src/impl.w src/tests.w src/todo.w src/done.w src/language.w src/usage.w +TEST_PYLPWEB = tests/pyweb_test.w tests/intro.w tests/unit.w tests/func.w tests/scripts.w +EXAMPLES_PYLPWEB = examples/hello_world_latex.w examples/hello_world_rst.w ackermanns.w +DOCUTILS_PYLPWEB = docutils.conf pyweb.css page-layout.css + +.PHONY : test + +# Note the bootstrapping new version from version 3.0 as baseline. +# Handy to keep this *outside* the project's Git repository. +# Note that the bootstrap 3.0 version doesn't support the -o option. +PYLPWEB_BOOTSTRAP=${PWD}/bootstrap/pyweb.py + +test : $(SOURCE_PYLPWEB) $(TEST_PYLPWEB) + cd src && python3 $(PYLPWEB_BOOTSTRAP) -xw pyweb.w + python3 src/pyweb.py tests/pyweb_test.w -o tests + PYTHONPATH=${PWD}/src pytest + python3 src/pyweb.py tests/pyweb_test.w -xt -o tests + rst2html.py tests/pyweb_test.rst tests/pyweb_test.html + mypy --strict --show-error-codes src + +doc : src/pyweb.html + +build : src/pyweb.py src/tangle.py src/weave.py src/pyweb.html + +examples : examples/hello_world_latex.tex examples/hello_world_rst.html examples/ackermanns.html + +src/pyweb.py src/pyweb.rst : $(SOURCE_PYLPWEB) + cd src && python3 $(PYLPWEB_BOOTSTRAP) pyweb.w + +src/pyweb.html : src/pyweb.rst $(DOCUTILS_PYLPWEB) + rst2html.py $< $@ + +tests/pyweb_test.rst : src/pyweb.py $(TEST_PYLPWEB) + python3 src/pyweb.py tests/pyweb_test.w -o tests + +tests/pyweb_test.html : tests/pyweb_test.rst $(DOCUTILS_PYLPWEB) + rst2html.py $< $@ + +examples/hello_world_rst.rst : examples/hello_world_rst.w + python3 src/pyweb.py -w rst examples/hello_world_rst.w -o examples + +examples/hello_world_rst.html : examples/hello_world_rst.rst $(DOCUTILS_PYLPWEB) + rst2html.py $< $@ + +examples/hello_world_latex.tex : examples/hello_world_latex.w + python3 src/pyweb.py -w latex examples/hello_world_latex.w -o examples + +examples/ackermanns.rst : examples/ackermanns.w + python3 src/pyweb.py -w rst examples/ackermanns.w -o examples + python -m doctest examples/ackermanns.py + +examples/ackermanns.html : examples/ackermanns.rst $(DOCUTILS_PYLPWEB) + rst2html.py $< $@ diff --git a/README b/README index 119ca3a..66513f4 100644 --- a/README +++ b/README @@ -1,4 +1,4 @@ -pyWeb 3.0: In Python, Yet Another Literate Programming Tool +pyWeb 3.1: In Python, Yet Another Literate Programming Tool Literate programming is an attempt to reconcile the opposing needs of clear presentation to people with the technical issues of @@ -14,7 +14,7 @@ It is independent of any particular document markup or source language. Is uses a simple set of markup tags to define chunks of code and documentation. -The ``pyweb.w`` file is the source for the various pyweb module and script files. +The ``pyweb.w`` file is the source for the various ``pyweb`` module and script files. The various source code files are created by applying a tangle operation to the ``.w`` file. The final documentation is created by applying a weave operation to the ``.w`` file. @@ -22,64 +22,88 @@ applying a weave operation to the ``.w`` file. Installation ------------- +This requires Python 3.10. + +This is not (currently) hosted in PyPI. Instead of installing it with PIP, +clone the GitHub repository or download the distribution kit. + +Install pyweb "manually" using the provided ``setup.py``. + :: - python3 setup.py install + python setup.py install + +This will install the ``pyweb`` module. -This will install the pyweb module. +Produce Documentation +--------------------- -Document production --------------------- +The supplied documentation uses RST markup; it requires docutils. -The supplied documentation uses RST markup and requires docutils. +:: + + python3 -m pip install docutils :: - python3 -m pyweb pyweb.w - rst2html.py pyweb.rst pyweb.html + python3 -m pyweb src/pyweb.w -o src + rst2html.py src/pyweb.rst src/pyweb.html Authoring --------- -The pyweb document describes the simple markup used to define code chunks +The ``pyweb.html`` document describes the markup used to define code chunks and assemble those code chunks into a coherent document as well as working code. +You'll create a ``.w`` file with documentation and code. If you're a JEdit user, the ``jedit`` directory can be used -to configure syntax highlighting that includes PyWeb and RST. +to configure syntax highlighting that includes **py-web-tool** and RST. Operation --------- -You can then run pyweb with +After installation and authoring, you can then run **py-web-tool** with the following +command :: - python3 -m pyweb pyweb.w + python3 -m pyweb src/pyweb.w -o src + +This will create the various output files from the source ```.w`` file. -This will create the various output files from the source .w file. +- ``pyweb.rst`` is the final woven document. This can be run through docutils for publication. -- ``pyweb.html`` is the final woven document. +- ``pyweb.py``, ``tangle.py``, ``weave.py`` are the tangled code files. -- ``pyweb.py``, ``tangle.py``, ``weave.py``, ``README``, ``setup.py`` and ``MANIFEST.in`` - ``.nojekyll`` and ``index.html`` are tangled output files. +All of the files are produced from a single source. Testing ------- -The test directory includes ``pyweb_test.w``, which will create a +The ``tests`` directory includes ``pyweb_test.w``, which will create a complete test suite. +You can create this with the following command -This weaves a ``pyweb_test.html`` file. +:: + + python3 -m pyweb tests/pyweb_test.w -o tests + +This weaves a ``tests/pyweb_test.rst`` file. This can be run through docutils for publication. This tangles several test modules: ``test.py``, ``test_tangler.py``, ``test_weaver.py``, -``test_loader.py`` and ``test_unit.py``. Running the ``test.py`` module will include and -execute all tests. +``test_loader.py``, ``test_unit.py``, and ``test_scripts.py``. -:: +Use **pytest** to run all the tests. + +Here's a typical sequence, used during development: - cd test - python3 -m pyweb pyweb_test.w - PYTHONPATH=.. python3 test.py - rst2html.py pyweb_test.rst pyweb_test.html +:: + python3 bootstrap/pyweb.py -xw src/pyweb.w -o src + python3 src/pyweb.py tests/pyweb_test.w -o tests + PYTHONPATH=${PWD}/src pytest + rst2html.py tests/pyweb_test.rst tests/pyweb_test.html + mypy --strict src +Note that a previous release, untouched, is saved in the ``bootstrap`` directory. +This is **not** changed during development, since **py-web-tool** is written with **py-web-tool**. diff --git a/README.rst b/README.rst deleted file mode 100644 index 6ba1550..0000000 --- a/README.rst +++ /dev/null @@ -1,86 +0,0 @@ -pyWeb 3.0: In Python, Yet Another Literate Programming Tool - -Literate programming is an attempt to reconcile the opposing needs -of clear presentation to people with the technical issues of -creating code that will work with our current set of tools. - -Presentation to people requires extensive and sophisticated typesetting -techniques. Further, the "narrative arc" of a presentation may not -follow the source code as layed out for the compiler. - -pyWeb is a literate programming tool based on Knuth's Web_ to combine the actions -of weaving a document with tangling source files. -It is independent of any particular document markup or source language. -Is uses a simple set of markup tags to define chunks of code and -documentation. - -The ``pyweb.w`` file is the source for the various pyweb module and script files. -The various source code files are created by applying a -tangle operation to the ``.w`` file. The final documentation is created by -applying a weave operation to the ``.w`` file. - -Installation -------------- - -:: - - python3 setup.py install - -This will install the pyweb module. - -Document production --------------------- - -The supplied documentation uses RST markup and requires docutils. - -:: - - python3 -m pyweb pyweb.w - rst2html.py pyweb.rst pyweb.html - -Authoring ---------- - -The pyweb document describes the simple markup used to define code chunks -and assemble those code chunks into a coherent document as well as working code. - -If you're a JEdit user, the ``jedit`` directory can be used -to configure syntax highlighting that includes PyWeb and RST. - -Operation ---------- - -You can then run pyweb with - -:: - - python3 -m pyweb pyweb.w - -This will create the various output files from the source .w file. - -- ``pyweb.html`` is the final woven document. - -- ``pyweb.py``, ``tangle.py``, ``weave.py``, ``README``, ``setup.py`` and ``MANIFEST.in`` - ``.nojekyll`` and ``index.html`` are tangled output files. - -Testing -------- - -The test directory includes ``pyweb_test.w``, which will create a -complete test suite. - -This weaves a ``pyweb_test.html`` file. - -This tangles several test modules: ``test.py``, ``test_tangler.py``, ``test_weaver.py``, -``test_loader.py`` and ``test_unit.py``. Running the ``test.py`` module will include and -execute all tests. - -:: - - cd test - python3 -m pyweb pyweb_test.w - PYTHONPATH=.. python3 test.py - rst2html.py pyweb_test.rst pyweb_test.html - - -.. _Web: https://doi.org/10.1093/comjnl/27.2.97 diff --git a/additional.w b/additional.w deleted file mode 100644 index 57fa01d..0000000 --- a/additional.w +++ /dev/null @@ -1,352 +0,0 @@ -.. pyweb/additional.w - -Additional Files -================ - -Two aditional scripts, ``tangle.py`` and ``weave.py``, are provided as examples -which an be customized. - -The ``README`` and ``setup.py`` files are also an important part of the -distribution as are a ``.nojekyll`` and ``index.html`` that are part of -publishing from GitHub. - -The ``.CSS`` file and ``.conf`` file for RST production are also provided here. - -``tangle.py`` Script ---------------------- - -This script shows a simple version of Tangling. This has a permitted -error for '@@i' commands to allow an include file (for example test results) -to be omitted from the tangle operation. - -Note the general flow of this top-level script. - -1. Create the logging context. - -2. Create the options. This hard-coded object is a stand-in for - parsing command-line options. - -3. Create the web object. - -4. For each action (``LoadAction`` and ``TangleAction`` in this example) - Set the web, set the options, execute the callable action, and write - a summary. - -@o tangle.py -@{#!/usr/bin/env python3 -"""Sample tangle.py script.""" -import pyweb -import logging -import argparse - -with pyweb.Logger( pyweb.log_config ): - logger= logging.getLogger(__file__) - - options = argparse.Namespace( - webFileName= "pyweb.w", - verbosity= logging.INFO, - command= '@@', - permitList= ['@@i'], - tangler_line_numbers= False, - reference_style = pyweb.SimpleReference(), - theTangler= pyweb.TanglerMake(), - webReader= pyweb.WebReader(), - ) - - w= pyweb.Web() - - for action in LoadAction(), TangleAction(): - action.web= w - action.options= options - action() - logger.info( action.summary() ) - -@} - -``weave.py`` Script ---------------------- - -This script shows a simple version of Weaving. This shows how -to define a customized set of templates for a different markup language. - - -A customized weaver generally has three parts. - -@o weave.py -@{@ -@ -@ -@} - -@d weave.py overheads... -@{#!/usr/bin/env python3 -"""Sample weave.py script.""" -import pyweb -import logging -import argparse -import string -@} - -@d weave.py custom weaver definition... -@{ -class MyHTML( pyweb.HTML ): - """HTML formatting templates.""" - extension= ".html" - - cb_template= string.Template(""" - -

${fullName} (${seq}) ${concat}

-
\n""")
-
-    ce_template= string.Template("""
-    
-

${fullName} (${seq}). - ${references} -

\n""") - - fb_template= string.Template(""" - -

``${fullName}`` (${seq}) ${concat}

-
\n""") # Prevent indent
-        
-    fe_template= string.Template( """
-

◊ ``${fullName}`` (${seq}). - ${references} -

\n""") - - ref_item_template = string.Template( - '${fullName} (${seq})' - ) - - ref_template = string.Template( ' Used by ${refList}.' ) - - refto_name_template = string.Template( - '${fullName} (${seq})' - ) - refto_seq_template = string.Template( '(${seq})' ) - - xref_head_template = string.Template( "
\n" ) - xref_foot_template = string.Template( "
\n" ) - xref_item_template = string.Template( "
${fullName}
${refList}
\n" ) - - name_def_template = string.Template( '•${seq}' ) - name_ref_template = string.Template( '${seq}' ) -@} - -@d weaver.py processing... -@{ -with pyweb.Logger( pyweb.log_config ): - logger= logging.getLogger(__file__) - - options = argparse.Namespace( - webFileName= "pyweb.w", - verbosity= logging.INFO, - command= '@@', - theWeaver= MyHTML(), - permitList= [], - tangler_line_numbers= False, - reference_style = pyweb.SimpleReference(), - theTangler= pyweb.TanglerMake(), - webReader= pyweb.WebReader(), - ) - - w= pyweb.Web() - - for action in LoadAction(), WeaveAction(): - action.web= w - action.options= options - action() - logger.info( action.summary() ) -@} - -The ``setup.py`` and ``MANIFEST.in`` files --------------------------------------------- - -In order to support a pleasant installation, the ``setup.py`` file is helpful. - -@o setup.py -@{#!/usr/bin/env python3 -"""Setup for pyWeb.""" - -from distutils.core import setup - -setup(name='pyweb', - version='3.0', - description='pyWeb 3.0: Yet Another Literate Programming Tool', - author='S. Lott', - author_email='s_lott@@yahoo.com', - url='http://slott-softwarearchitect.blogspot.com/', - py_modules=['pyweb'], - classifiers=[ - 'Intended Audience :: Developers', - 'Topic :: Documentation', - 'Topic :: Software Development :: Documentation', - 'Topic :: Text Processing :: Markup', - ] - ) -@} - -In order build a source distribution kit the ``python3 setup.py sdist`` requires a -``MANIFEST``. We can either list all files or provide a ``MANIFEST.in`` -that specifies additional rules. -We use a simple inclusion to augment the default manifest rules. - -@o MANIFEST.in -@{include *.w *.css *.html *.conf *.rst -include test/*.w test/*.css test/*.html test/*.conf test/*.py -include jedit/*.xml -@} - -The ``README`` file ---------------------- - -Here's the README file. - -@o README -@{pyWeb 3.0: In Python, Yet Another Literate Programming Tool - -Literate programming is an attempt to reconcile the opposing needs -of clear presentation to people with the technical issues of -creating code that will work with our current set of tools. - -Presentation to people requires extensive and sophisticated typesetting -techniques. Further, the "narrative arc" of a presentation may not -follow the source code as layed out for the compiler. - -pyWeb is a literate programming tool based on Knuth's Web to combine the actions -of weaving a document with tangling source files. -It is independent of any particular document markup or source language. -Is uses a simple set of markup tags to define chunks of code and -documentation. - -The ``pyweb.w`` file is the source for the various pyweb module and script files. -The various source code files are created by applying a -tangle operation to the ``.w`` file. The final documentation is created by -applying a weave operation to the ``.w`` file. - -Installation -------------- - -:: - - python3 setup.py install - -This will install the pyweb module. - -Document production --------------------- - -The supplied documentation uses RST markup and requires docutils. - -:: - - python3 -m pyweb pyweb.w - rst2html.py pyweb.rst pyweb.html - -Authoring ---------- - -The pyweb document describes the simple markup used to define code chunks -and assemble those code chunks into a coherent document as well as working code. - -If you're a JEdit user, the ``jedit`` directory can be used -to configure syntax highlighting that includes PyWeb and RST. - -Operation ---------- - -You can then run pyweb with - -:: - - python3 -m pyweb pyweb.w - -This will create the various output files from the source .w file. - -- ``pyweb.html`` is the final woven document. - -- ``pyweb.py``, ``tangle.py``, ``weave.py``, ``README``, ``setup.py`` and ``MANIFEST.in`` - ``.nojekyll`` and ``index.html`` are tangled output files. - -Testing -------- - -The test directory includes ``pyweb_test.w``, which will create a -complete test suite. - -This weaves a ``pyweb_test.html`` file. - -This tangles several test modules: ``test.py``, ``test_tangler.py``, ``test_weaver.py``, -``test_loader.py`` and ``test_unit.py``. Running the ``test.py`` module will include and -execute all tests. - -:: - - cd test - python3 -m pyweb pyweb_test.w - PYTHONPATH=.. python3 test.py - rst2html.py pyweb_test.rst pyweb_test.html - - -@} - -The HTML Support Files ----------------------- - -To get the RST to look good, there are some additional files. - -``docutils.conf`` defines the CSS files to use. -The default CSS file (stylesheet-path) may need to be customized for your -installation of docutils. - -@o docutils.conf -@{# docutils.conf - -[html4css1 writer] -stylesheet-path: /Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages/docutils/writers/html4css1/html4css1.css, - page-layout.css -syntax-highlight: long -@} - -``page-layout.css`` This tweaks one CSS to be sure that -the resulting HTML pages are easier to read. - -@o page-layout.css -@{/* Page layout tweaks */ -div.document { width: 7in; } -.small { font-size: smaller; } -.code -{ - color: #101080; - display: block; - border-color: black; - border-width: thin; - border-style: solid; - background-color: #E0FFFF; - /*#99FFFF*/ - padding: 0 0 0 1%; - margin: 0 6% 0 6%; - text-align: left; - font-size: smaller; -} -@} - -Yes, this creates a (nearly) empty file for use by GitHub. There's a small -bug in ``NamedChunk.tangle()`` that prevents handling zero-length text. - -@o .nojekyll -@{ -@} - -Finally, an ``index.html`` to redirect GitHub to the ``pyweb.html`` file. - -@o index.html -@{ - - -Redirect - - -Sorry, you should have been redirected pyweb.html. - -@} \ No newline at end of file diff --git a/pyweb.py b/bootstrap/pyweb.py similarity index 99% rename from pyweb.py rename to bootstrap/pyweb.py index 4167527..58a7fe8 100644 --- a/pyweb.py +++ b/bootstrap/pyweb.py @@ -1388,12 +1388,12 @@ class HTML( Weaver ):

${fullName} (${seq}) ${concat}

-
\n""")
+    
\n""")
     
 
         
     ce_template= string.Template("""
-    
+

${fullName} (${seq}). ${references}

\n""") @@ -1403,11 +1403,11 @@ class HTML( Weaver ): fb_template= string.Template("""

``${fullName}`` (${seq}) ${concat}

-
\n""") # Prevent indent
+    
\n""") # Prevent indent
     
 
         
-    fe_template= string.Template( """
+ fe_template= string.Template( """

◊ ``${fullName}`` (${seq}). ${references}

\n""") diff --git a/docutils.conf b/docutils.conf index ed89747..522baed 100644 --- a/docutils.conf +++ b/docutils.conf @@ -1,6 +1,6 @@ # docutils.conf [html4css1 writer] -stylesheet-path: /Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages/docutils/writers/html4css1/html4css1.css, +stylesheet-path: /Users/slott/miniconda3/envs/pywebtool/lib/python3.10/site-packages/docutils/writers/html4css1/html4css1.css, page-layout.css syntax-highlight: long diff --git a/examples/ackermanns.html b/examples/ackermanns.html new file mode 100644 index 0000000..71c5c47 --- /dev/null +++ b/examples/ackermanns.html @@ -0,0 +1,1039 @@ + + + + + + +Ackermann’s Function + + + + + +
+

Ackermann’s Function

+

Steven F. Lott

+ + + + + + +
+

Definitions

+

Here are the definitions for Ackermann's full φ(m, n, p) function:

+
+φ(m, n, p) =  + + +φ(m, n, 0)  + + + = m + n  + + + + + +   + + +   + + + + + +φ(m, 0, 1)  + + + = 0  + + + + + +   + + +   + + + + + +φ(m, 0, 2)  + + + = 1  + + + + + +   + + +   + + + + + +φ(m, 0, p)  + + + = m for p > 2  + + + + + +   + + +   + + + + + +φ(m, n, p)  + + + = φ(m, φ(m, n − 1, p), p − 1)  for n, p > 0  + + + + + + +
+

These definitions have the following consequences:

+
+ + +φ(m, n, 0) + + + = m + n + + + + + +  + + +  + + + + + +φ(m, n, 1) + + + = m×n + + + + + +  + + +  + + + + + +φ(m, n, 2) + + + = mn + + + + +
+
+
+

Implementations

+

We'll look at a number of ways +to implement this.

+

ackermanns.py (1) =

+
+→set reduction (2)
+REPL_set_reduction = """
+→test set reduction (3)
+"""
+
+→dictionary reduction (4)
+REPL_dict_reduction = """
+→test dict reduction (5)
+"""
+
+→match statement (6)
+REPL_match_statement = """
+→test match statemnent (7)
+"""
+
+__test__ = {n: v for n, v in globals().items() if n.startswith('REPL')}
+
+ +
+

ackermanns.py (1).

+
+
+

Set Reduction

+

First, let's consider evaluating the conditionals into "value or None" conditions. +We can then create a set and keep the non-None items.

+

The set reduction involves computing a non-None result for the case that is true +and None result for all remaining cases. The resulting set of values will be reduced to +two items. Subtracting the None item from the set leaves the result value.

+

set reduction (2) =

+
+phi_1 = (
+    lambda m, n, p: (
+        {
+            m+n if p == 0 else None,
+            0 if n == 0 and p == 1 else None,
+            1 if n == 0 and p == 2 else None,
+            m if n == 0 and p > 2 else None,
+            phi_1(m, phi_1(m, n-1, p), p-1) if n > 0 and p > 0 else None,
+        } - {None}
+    ).pop()
+)
+
+ +
+

set reduction (2). Used by: ackermanns.py (1)

+
+

test set reduction (3) =

+
+>>> phi_1(3, 5, 0)
+8
+>>> phi_1(3, 5, 1)
+15
+>>> phi_1(3, 5, 2)
+243
+>>> 3**5
+243
+
+ +
+

test set reduction (3). Used by: ackermanns.py (1)

+
+
+
+

Dictionary Reduction

+

We can use the condition value (True or False) as a dictionary +key. The value for each key is the lambda to evaluate when the key is True. +Picking the True key from the dictionary maps to the applicable lambda. +The other lambda, mapped to False can be ignored.

+

dictionary reduction (4) =

+
+phi = (
+    lambda m, n, p: (
+        {
+            p == 0: lambda m, n, p: m+n,
+            n == 0 and p == 1: lambda m, n, p: 0,
+            n == 0 and p == 2: lambda m, n, p: 1,
+            n == 0 and p > 2: lambda m, n, p: m,
+            n > 0 and p > 0: lambda m, n, p: phi(m, phi(m, n-1, p), p-1)
+        }[True](m, n, p)
+    )
+)
+
+ +
+

dictionary reduction (4). Used by: ackermanns.py (1)

+
+

test dict reduction (5) =

+
+>>> phi(3, 5, 0) == 3+5
+True
+>>> phi(3, 5, 1) == 3*5
+True
+>>> phi(3, 5, 2) == 3**5
+True
+
+ +
+

test dict reduction (5). Used by: ackermanns.py (1)

+
+
+
+

Match/Case

+

We can use Python 3.10's match statement, also. +This is generally what folks expect to see.

+

match statement (6) =

+
+def phi_m(m, n, p):
+    match (m, n, p):
+        case (_, _, 0):
+            return m + n
+        case (_, 0, 1):
+            return 0
+        case (_, 0, 2):
+            return 1
+        case (_, 0, _) if p > 2:
+            return m
+        case (_, _, _) if n > 0 and p > 0:
+            return phi_m(m, phi_m(m, n-1, p), p-1)
+
+ +
+

match statement (6). Used by: ackermanns.py (1)

+
+

test match statemnent (7) =

+
+>>> phi_m(3, 5, 0) == 3+5
+True
+>>> phi_m(3, 5, 1) == 3*5
+True
+>>> phi_m(3, 5, 2) == 3**5
+True
+
+ +
+

test match statemnent (7). Used by: ackermanns.py (1)

+
+
+
+
+

Conclusion

+

We've looked at three ways to define a fairly complex function with a lot of complex-looking +special cases.

+

The match statement seems to fit most people's expectations of the complex-looking math.

+
+
+ + diff --git a/examples/ackermanns.py b/examples/ackermanns.py new file mode 100644 index 0000000..c2d29ea --- /dev/null +++ b/examples/ackermanns.py @@ -0,0 +1,77 @@ + + +phi_1 = ( + lambda m, n, p: ( + { + m+n if p == 0 else None, + 0 if n == 0 and p == 1 else None, + 1 if n == 0 and p == 2 else None, + m if n == 0 and p > 2 else None, + phi_1(m, phi_1(m, n-1, p), p-1) if n > 0 and p > 0 else None, + } - {None} + ).pop() +) + +REPL_set_reduction = """ + +>>> phi_1(3, 5, 0) +8 +>>> phi_1(3, 5, 1) +15 +>>> phi_1(3, 5, 2) +243 +>>> 3**5 +243 + +""" + + +phi = ( + lambda m, n, p: ( + { + p == 0: lambda m, n, p: m+n, + n == 0 and p == 1: lambda m, n, p: 0, + n == 0 and p == 2: lambda m, n, p: 1, + n == 0 and p > 2: lambda m, n, p: m, + n > 0 and p > 0: lambda m, n, p: phi(m, phi(m, n-1, p), p-1) + }[True](m, n, p) + ) +) + +REPL_dict_reduction = """ + +>>> phi(3, 5, 0) == 3+5 +True +>>> phi(3, 5, 1) == 3*5 +True +>>> phi(3, 5, 2) == 3**5 +True + +""" + + +def phi_m(m, n, p): + match (m, n, p): + case (_, _, 0): + return m + n + case (_, 0, 1): + return 0 + case (_, 0, 2): + return 1 + case (_, 0, _) if p > 2: + return m + case (_, _, _) if n > 0 and p > 0: + return phi_m(m, phi_m(m, n-1, p), p-1) + +REPL_match_statement = """ + +>>> phi_m(3, 5, 0) == 3+5 +True +>>> phi_m(3, 5, 1) == 3*5 +True +>>> phi_m(3, 5, 2) == 3**5 +True + +""" + +__test__ = {n: v for n, v in globals().items() if n.startswith('REPL')} diff --git a/examples/ackermanns.rst b/examples/ackermanns.rst new file mode 100644 index 0000000..8df7afa --- /dev/null +++ b/examples/ackermanns.rst @@ -0,0 +1,254 @@ +#################### +Ackermann’s Function +#################### + +============== +Steven F. Lott +============== + +.. include:: +.. include:: + +.. contents:: + +Definitions +=========== + +Here are the definitions for Ackermann's full :math:`\varphi (m,n,p)` function: + +.. math:: + + + \varphi (m,n,p) = \begin{cases} + \varphi (m,n,0)&=m+n\\ + \varphi (m,0,1)&=0\\ + \varphi (m,0,2)&=1\\ + \varphi (m,0,p)&=m {\textbf{ for }}p>2\\ + \varphi (m,n,p)&=\varphi (m,\varphi (m,n-1,p),p-1) {\textbf{ for }}n,p>0 + \end{cases} + +These definitions have the following consequences: + +.. math:: + + + \begin{align} + \varphi (m,n,0)&=m+n\\ + \varphi (m,n,1)&=m\times n\\ + \varphi (m,n,2)&=m^{n} + \end{align} + +Implementations +=============== + +We'll look at a number of ways +to implement this. + + +.. _`1`: +.. rubric:: ackermanns.py (1) = +.. parsed-literal:: + :class: code + + + |srarr|\ set reduction (`2`_) + REPL\_set\_reduction = """ + |srarr|\ test set reduction (`3`_) + """ + + |srarr|\ dictionary reduction (`4`_) + REPL\_dict\_reduction = """ + |srarr|\ test dict reduction (`5`_) + """ + + |srarr|\ match statement (`6`_) + REPL\_match\_statement = """ + |srarr|\ test match statemnent (`7`_) + """ + + \_\_test\_\_ = {n: v for n, v in globals().items() if n.startswith('REPL')} + +.. + + .. class:: small + + |loz| *ackermanns.py (1)*. + + +Set Reduction +------------- + +First, let's consider evaluating the conditionals into "value or None" conditions. +We can then create a set and keep the non-None items. + +The set reduction involves computing a non-\ ``None`` result for the case that is true +and ``None`` result for all remaining cases. The resulting set of values will be reduced to +two items. Subtracting the ``None`` item from the set leaves the result value. + + +.. _`2`: +.. rubric:: set reduction (2) = +.. parsed-literal:: + :class: code + + + phi\_1 = ( + lambda m, n, p: ( + { + m+n if p == 0 else None, + 0 if n == 0 and p == 1 else None, + 1 if n == 0 and p == 2 else None, + m if n == 0 and p > 2 else None, + phi\_1(m, phi\_1(m, n-1, p), p-1) if n > 0 and p > 0 else None, + } - {None} + ).pop() + ) + +.. + + .. class:: small + + |loz| *set reduction (2)*. Used by: ackermanns.py (`1`_) + + + +.. _`3`: +.. rubric:: test set reduction (3) = +.. parsed-literal:: + :class: code + + + >>> phi\_1(3, 5, 0) + 8 + >>> phi\_1(3, 5, 1) + 15 + >>> phi\_1(3, 5, 2) + 243 + >>> 3\*\*5 + 243 + +.. + + .. class:: small + + |loz| *test set reduction (3)*. Used by: ackermanns.py (`1`_) + + + +Dictionary Reduction +-------------------- + +We can use the condition value (``True`` or ``False``) as a dictionary +key. The value for each key is the lambda to evaluate when the key is ``True``. +Picking the ``True`` key from the dictionary maps to the applicable lambda. +The other lambda, mapped to ``False`` can be ignored. + + +.. _`4`: +.. rubric:: dictionary reduction (4) = +.. parsed-literal:: + :class: code + + + phi = ( + lambda m, n, p: ( + { + p == 0: lambda m, n, p: m+n, + n == 0 and p == 1: lambda m, n, p: 0, + n == 0 and p == 2: lambda m, n, p: 1, + n == 0 and p > 2: lambda m, n, p: m, + n > 0 and p > 0: lambda m, n, p: phi(m, phi(m, n-1, p), p-1) + }[True](m, n, p) + ) + ) + +.. + + .. class:: small + + |loz| *dictionary reduction (4)*. Used by: ackermanns.py (`1`_) + + + +.. _`5`: +.. rubric:: test dict reduction (5) = +.. parsed-literal:: + :class: code + + + >>> phi(3, 5, 0) == 3+5 + True + >>> phi(3, 5, 1) == 3\*5 + True + >>> phi(3, 5, 2) == 3\*\*5 + True + +.. + + .. class:: small + + |loz| *test dict reduction (5)*. Used by: ackermanns.py (`1`_) + + + +Match/Case +---------- + +We can use Python 3.10's ``match`` statement, also. +This is generally what folks expect to see. + + +.. _`6`: +.. rubric:: match statement (6) = +.. parsed-literal:: + :class: code + + + def phi\_m(m, n, p): + match (m, n, p): + case (\_, \_, 0): + return m + n + case (\_, 0, 1): + return 0 + case (\_, 0, 2): + return 1 + case (\_, 0, \_) if p > 2: + return m + case (\_, \_, \_) if n > 0 and p > 0: + return phi\_m(m, phi\_m(m, n-1, p), p-1) + +.. + + .. class:: small + + |loz| *match statement (6)*. Used by: ackermanns.py (`1`_) + + + +.. _`7`: +.. rubric:: test match statemnent (7) = +.. parsed-literal:: + :class: code + + + >>> phi\_m(3, 5, 0) == 3+5 + True + >>> phi\_m(3, 5, 1) == 3\*5 + True + >>> phi\_m(3, 5, 2) == 3\*\*5 + True + +.. + + .. class:: small + + |loz| *test match statemnent (7)*. Used by: ackermanns.py (`1`_) + + +Conclusion +========== + +We've looked at three ways to define a fairly complex function with a lot of complex-looking +special cases. + +The ``match`` statement seems to fit most people's expectations of the complex-looking math. diff --git a/examples/ackermanns.w b/examples/ackermanns.w new file mode 100644 index 0000000..0db9d9a --- /dev/null +++ b/examples/ackermanns.w @@ -0,0 +1,177 @@ +#################### +Ackermann’s Function +#################### + +============== +Steven F. Lott +============== + +.. include:: +.. include:: + +.. contents:: + +Definitions +=========== + +Here are the definitions for Ackermann's full :math:`\varphi (m,n,p)` function: + +.. math:: + + + \varphi (m,n,p) = \begin{cases} + \varphi (m,n,0)&=m+n\\ + \varphi (m,0,1)&=0\\ + \varphi (m,0,2)&=1\\ + \varphi (m,0,p)&=m {\textbf{ for }}p>2\\ + \varphi (m,n,p)&=\varphi (m,\varphi (m,n-1,p),p-1) {\textbf{ for }}n,p>0 + \end{cases} + +These definitions have the following consequences: + +.. math:: + + + \begin{align} + \varphi (m,n,0)&=m+n\\ + \varphi (m,n,1)&=m\times n\\ + \varphi (m,n,2)&=m^{n} + \end{align} + +Implementations +=============== + +We'll look at a number of ways +to implement this. + +@o ackermanns.py +@{ +@ +REPL_set_reduction = """ +@ +""" + +@ +REPL_dict_reduction = """ +@ +""" + +@ +REPL_match_statement = """ +@ +""" + +__test__ = {n: v for n, v in globals().items() if n.startswith('REPL')} +@} + +Set Reduction +------------- + +First, let's consider evaluating the conditionals into "value or None" conditions. +We can then create a set and keep the non-None items. + +The set reduction involves computing a non-\ ``None`` result for the case that is true +and ``None`` result for all remaining cases. The resulting set of values will be reduced to +two items. Subtracting the ``None`` item from the set leaves the result value. + +@d set reduction +@{ +phi_1 = ( + lambda m, n, p: ( + { + m+n if p == 0 else None, + 0 if n == 0 and p == 1 else None, + 1 if n == 0 and p == 2 else None, + m if n == 0 and p > 2 else None, + phi_1(m, phi_1(m, n-1, p), p-1) if n > 0 and p > 0 else None, + } - {None} + ).pop() +) +@} + +@d test set reduction +@{ +>>> phi_1(3, 5, 0) +8 +>>> phi_1(3, 5, 1) +15 +>>> phi_1(3, 5, 2) +243 +>>> 3**5 +243 +@} + + +Dictionary Reduction +-------------------- + +We can use the condition value (``True`` or ``False``) as a dictionary +key. The value for each key is the lambda to evaluate when the key is ``True``. +Picking the ``True`` key from the dictionary maps to the applicable lambda. +The other lambda, mapped to ``False`` can be ignored. + +@d dictionary reduction +@{ +phi = ( + lambda m, n, p: ( + { + p == 0: lambda m, n, p: m+n, + n == 0 and p == 1: lambda m, n, p: 0, + n == 0 and p == 2: lambda m, n, p: 1, + n == 0 and p > 2: lambda m, n, p: m, + n > 0 and p > 0: lambda m, n, p: phi(m, phi(m, n-1, p), p-1) + }[True](m, n, p) + ) +) +@} + +@d test dict reduction +@{ +>>> phi(3, 5, 0) == 3+5 +True +>>> phi(3, 5, 1) == 3*5 +True +>>> phi(3, 5, 2) == 3**5 +True +@} + + +Match/Case +---------- + +We can use Python 3.10's ``match`` statement, also. +This is generally what folks expect to see. + +@d match statement +@{ +def phi_m(m, n, p): + match (m, n, p): + case (_, _, 0): + return m + n + case (_, 0, 1): + return 0 + case (_, 0, 2): + return 1 + case (_, 0, _) if p > 2: + return m + case (_, _, _) if n > 0 and p > 0: + return phi_m(m, phi_m(m, n-1, p), p-1) +@} + +@d test match statemnent +@{ +>>> phi_m(3, 5, 0) == 3+5 +True +>>> phi_m(3, 5, 1) == 3*5 +True +>>> phi_m(3, 5, 2) == 3**5 +True +@} + +Conclusion +========== + +We've looked at three ways to define a fairly complex function with a lot of complex-looking +special cases. + +The ``match`` statement seems to fit most people's expectations of the complex-looking math. diff --git a/test/test_latex.tex b/examples/hello_world_latex.tex similarity index 74% rename from test/test_latex.tex rename to examples/hello_world_latex.tex index 47e795d..da5eb1e 100644 --- a/test/test_latex.tex +++ b/examples/hello_world_latex.tex @@ -26,7 +26,7 @@ \subsection{Output files} \label{pyweb1} \begin{flushleft} - \textit{Code example test.py (1)} + \textit{Code example hw_latex_1.py (1)} \begin{Verbatim}[commandchars=\\\{\},codes={\catcode`$=3\catcode`^=7},frame=single] $\triangleright$ Code Example Import the os module (3) @@ -34,16 +34,16 @@ \subsection{Output files} $\triangleright$ Code Example Construct the message with Concatenation (5) $\triangleright$ Code Example Print the message (7) - \end{Verbatim} - - \end{flushleft} +\end{Verbatim} +[] +\end{flushleft} The second uses string substitution: \label{pyweb2} \begin{flushleft} - \textit{Code example test2.py (2)} + \textit{Code example hw_latex_2.py (2)} \begin{Verbatim}[commandchars=\\\{\},codes={\catcode`$=3\catcode`^=7},frame=single] $\triangleright$ Code Example Import the os module (3) @@ -51,9 +51,9 @@ \subsection{Output files} $\triangleright$ Code Example Construct the message with Substitution (6) $\triangleright$ Code Example Print the message (7) - \end{Verbatim} - - \end{flushleft} +\end{Verbatim} +[] +\end{flushleft} \subsection{Retrieving the OS description} @@ -67,20 +67,19 @@ \subsection{Retrieving the OS description} import os - \end{Verbatim} - +\end{Verbatim} + \footnotesize Used by: \begin{list}{}{} - \item Code example test.py (1) (Sect. \ref{pyweb1}, p. \pageref{pyweb1}) + \item Code example hw_latex_1.py (1) (Sect. \ref{pyweb1}, p. \pageref{pyweb1}) ; - \item Code example test2.py (2) (Sect. \ref{pyweb2}, p. \pageref{pyweb2}) + \item Code example hw_latex_2.py (2) (Sect. \ref{pyweb2}, p. \pageref{pyweb2}) \end{list} \normalsize - - \end{flushleft} +\end{flushleft} That having been done, we can retrieve Python's name for the OS type: @@ -92,20 +91,19 @@ \subsection{Retrieving the OS description} os_name = os.name - \end{Verbatim} - +\end{Verbatim} + \footnotesize Used by: \begin{list}{}{} - \item Code example test.py (1) (Sect. \ref{pyweb1}, p. \pageref{pyweb1}) + \item Code example hw_latex_1.py (1) (Sect. \ref{pyweb1}, p. \pageref{pyweb1}) ; - \item Code example test2.py (2) (Sect. \ref{pyweb2}, p. \pageref{pyweb2}) + \item Code example hw_latex_2.py (2) (Sect. \ref{pyweb2}, p. \pageref{pyweb2}) \end{list} \normalsize - - \end{flushleft} +\end{flushleft} \subsection{Building the message} @@ -119,18 +117,17 @@ \subsection{Building the message} msg = "Hello, " + os_name + "!" - \end{Verbatim} - +\end{Verbatim} + \footnotesize Used by: \begin{list}{}{} - \item Code example test.py (1) (Sect. \ref{pyweb1}, p. \pageref{pyweb1}) + \item Code example hw_latex_1.py (1) (Sect. \ref{pyweb1}, p. \pageref{pyweb1}) \end{list} \normalsize - - \end{flushleft} +\end{flushleft} But wait! Is there a better way? Using string substitution might be @@ -141,24 +138,23 @@ \subsection{Building the message} \textit{Code example Construct the message with Substitution (6)} \begin{Verbatim}[commandchars=\\\{\},codes={\catcode`$=3\catcode`^=7},frame=single] -msg = "Hello, %s!" % os_name +msg = f"Hello, {os_name}!" + +\end{Verbatim} - \end{Verbatim} - \footnotesize Used by: \begin{list}{}{} - \item Code example test2.py (2) (Sect. \ref{pyweb2}, p. \pageref{pyweb2}) + \item Code example hw_latex_2.py (2) (Sect. \ref{pyweb2}, p. \pageref{pyweb2}) \end{list} \normalsize - - \end{flushleft} +\end{flushleft} -We'll use the first of these methods in \texttt{test.py}, and the -other in \texttt{test2.py}. +We'll use the first of these methods in \texttt{latex_test.py}, and the +other in \texttt{latex_test_2.py}. \subsection{Printing the message} @@ -171,22 +167,21 @@ \subsection{Printing the message} \textit{Code example Print the message (7)} \begin{Verbatim}[commandchars=\\\{\},codes={\catcode`$=3\catcode`^=7},frame=single] -print msg +print(msg) + +\end{Verbatim} - \end{Verbatim} - \footnotesize Used by: \begin{list}{}{} - \item Code example test.py (1) (Sect. \ref{pyweb1}, p. \pageref{pyweb1}) + \item Code example hw_latex_1.py (1) (Sect. \ref{pyweb1}, p. \pageref{pyweb1}) ; - \item Code example test2.py (2) (Sect. \ref{pyweb2}, p. \pageref{pyweb2}) + \item Code example hw_latex_2.py (2) (Sect. \ref{pyweb2}, p. \pageref{pyweb2}) \end{list} \normalsize - - \end{flushleft} +\end{flushleft} \end{document} diff --git a/test/test_latex.w b/examples/hello_world_latex.w similarity index 90% rename from test/test_latex.w rename to examples/hello_world_latex.w index f0811fc..b44d510 100644 --- a/test/test_latex.w +++ b/examples/hello_world_latex.w @@ -24,7 +24,7 @@ This document contains the makings of two files; the first, \texttt{test.py}, uses simple string concatenation to build its output message: -@o test.py +@o hw_latex_1.py @{ @< Import the os module @> @< Get the OS description @> @@ -34,7 +34,7 @@ message: The second uses string substitution: -@o test2.py +@o hw_latex_2.py @{ @< Import the os module @> @< Get the OS description @> @@ -72,11 +72,11 @@ better: @d Construct the message with Substitution @{ -msg = "Hello, %s!" % os_name +msg = f"Hello, {os_name}!" @} -We'll use the first of these methods in \texttt{test.py}, and the -other in \texttt{test2.py}. +We'll use the first of these methods in \texttt{latex_test.py}, and the +other in \texttt{latex_test_2.py}. \subsection{Printing the message} @@ -86,7 +86,7 @@ they have: @d Print the message @{ -print msg +print(msg) @} \end{document} diff --git a/test/test_rst.html b/examples/hello_world_rst.html similarity index 57% rename from test/test_rst.html rename to examples/hello_world_rst.html index d38b314..e74386e 100644 --- a/test/test_rst.html +++ b/examples/hello_world_rst.html @@ -3,13 +3,13 @@ - + Test Program + @@ -308,26 +385,31 @@

Test Program

Jason R. Fruit

+ +
-

Introduction

+

Introduction

This test program prints the word "hello", followed by the name of the operating system as understood by Python. It is implemented in Python and uses the os module. It builds the message string @@ -335,71 +417,94 @@

Introduction

two different files.

-

Implementation

+

Implementation

-

Output files

+

Output files

This document contains the makings of two files; the first, test.py, uses simple string concatenation to build its output message:

-

test.py (1)

-
-→ Import the os module (3)
-→ Get the OS description (4)
-→ Construct the message with Concatenation (5)
-→ Print the message (7)
+

hw_rst_1.py (1) =

+
+→Import the os module (3)
+→Get the OS description (4)
+→Construct the message with Concatenation (5)
+→Print the message (7)
 
+ +
+

hw_rst_1.py (1).

+

The second uses string substitution:

-

test2.py (2)

-
-→ Import the os module (3)
-→ Get the OS description (4)
-→ Construct the message with Substitution (6)
-→ Print the message (7)
+

hw_rst_2.py (2) =

+
+→Import the os module (3)
+→Get the OS description (4)
+→Construct the message with Substitution (6)
+→Print the message (7)
 
+ +
+

hw_rst_2.py (2).

+
-

Retrieving the OS description

+

Retrieving the OS description

First we must import the os module so we can learn about the OS:

-

Import the os module (3)

-
+

Import the os module (3) =

+
 import os
 
-

Used by: test.py (1); test2.py (2)

+ +
+

Import the os module (3). Used by: hw_rst_1.py (1); hw_rst_2.py (2)

+

That having been done, we can retrieve Python's name for the OS type:

-

Get the OS description (4)

-
+

Get the OS description (4) =

+
 os_name = os.name
 
-

Used by: test.py (1); test2.py (2)

+ +
+

Get the OS description (4). Used by: hw_rst_1.py (1); hw_rst_2.py (2)

+
-

Building the message

+

Building the message

Now, we're ready for the meat of the application: concatenating two strings:

-

Construct the message with Concatenation (5)

-
+

Construct the message with Concatenation (5) =

+
 msg = "Hello, " + os_name + "!"
 
-

Used by: test.py (1)

+ +
+

Construct the message with Concatenation (5). Used by: hw_rst_1.py (1)

+

But wait! Is there a better way? Using string substitution might be better:

-

Construct the message with Substitution (6)

-
-msg = "Hello, %s!" % os_name
+

Construct the message with Substitution (6) =

+
+msg = f"Hello, {os_name}!"
 
-

Used by: test2.py (2)

-

We'll use the first of these methods in test.py, and the -other in test2.py.

+ +
+

Construct the message with Substitution (6). Used by: hw_rst_2.py (2)

+
+

We'll use the first of these methods in rst_test_1.py, and the +other in rst_test_2.py.

-

Printing the message

+

Printing the message

Finally, we print the message out for the user to see. Hopefully, a cheery greeting will make them happy to know what operating system they have:

-

Print the message (7)

-
-print msg
+

Print the message (7) =

+
+print(msg)
 
-

Used by: test.py (1); test2.py (2)

+ +
+

Print the message (7). Used by: hw_rst_1.py (1); hw_rst_2.py (2)

+
diff --git a/examples/hello_world_rst.rst b/examples/hello_world_rst.rst new file mode 100644 index 0000000..325c89e --- /dev/null +++ b/examples/hello_world_rst.rst @@ -0,0 +1,177 @@ +#################### +Test Program +#################### + +=============== +Jason R. Fruit +=============== + +.. include:: +.. include:: + +.. contents:: + + +Introduction +============ + +This test program prints the word "hello", followed by the name of +the operating system as understood by Python. It is implemented in +Python and uses the ``os`` module. It builds the message string +in two different ways, and writes separate versions of the program to +two different files. + +Implementation +============== + +Output files +------------ + +This document contains the makings of two files; the first, +``test.py``, uses simple string concatenation to build its output +message: + + +.. _`1`: +.. rubric:: hw_rst_1.py (1) = +.. parsed-literal:: + :class: code + + + |srarr|\ Import the os module (`3`_) + |srarr|\ Get the OS description (`4`_) + |srarr|\ Construct the message with Concatenation (`5`_) + |srarr|\ Print the message (`7`_) + +.. + + .. class:: small + + |loz| *hw_rst_1.py (1)*. + + +The second uses string substitution: + + +.. _`2`: +.. rubric:: hw_rst_2.py (2) = +.. parsed-literal:: + :class: code + + + |srarr|\ Import the os module (`3`_) + |srarr|\ Get the OS description (`4`_) + |srarr|\ Construct the message with Substitution (`6`_) + |srarr|\ Print the message (`7`_) + +.. + + .. class:: small + + |loz| *hw_rst_2.py (2)*. + + +Retrieving the OS description +------------------------------- + +First we must import the os module so we can learn about the OS: + + +.. _`3`: +.. rubric:: Import the os module (3) = +.. parsed-literal:: + :class: code + + + import os + +.. + + .. class:: small + + |loz| *Import the os module (3)*. Used by: hw_rst_1.py (`1`_); hw_rst_2.py (`2`_) + + +That having been done, we can retrieve Python's name for the OS type: + + +.. _`4`: +.. rubric:: Get the OS description (4) = +.. parsed-literal:: + :class: code + + + os\_name = os.name + +.. + + .. class:: small + + |loz| *Get the OS description (4)*. Used by: hw_rst_1.py (`1`_); hw_rst_2.py (`2`_) + + +Building the message +--------------------- + +Now, we're ready for the meat of the application: concatenating two strings: + + +.. _`5`: +.. rubric:: Construct the message with Concatenation (5) = +.. parsed-literal:: + :class: code + + + msg = "Hello, " + os\_name + "!" + +.. + + .. class:: small + + |loz| *Construct the message with Concatenation (5)*. Used by: hw_rst_1.py (`1`_) + + +But wait! Is there a better way? Using string substitution might be +better: + + +.. _`6`: +.. rubric:: Construct the message with Substitution (6) = +.. parsed-literal:: + :class: code + + + msg = f"Hello, {os\_name}!" + +.. + + .. class:: small + + |loz| *Construct the message with Substitution (6)*. Used by: hw_rst_2.py (`2`_) + + +We'll use the first of these methods in ``rst_test_1.py``, and the +other in ``rst_test_2.py``. + +Printing the message +---------------------- + +Finally, we print the message out for the user to see. Hopefully, a +cheery greeting will make them happy to know what operating system +they have: + + +.. _`7`: +.. rubric:: Print the message (7) = +.. parsed-literal:: + :class: code + + + print(msg) + +.. + + .. class:: small + + |loz| *Print the message (7)*. Used by: hw_rst_1.py (`1`_); hw_rst_2.py (`2`_) + diff --git a/test/test_rst.w b/examples/hello_world_rst.w similarity index 90% rename from test/test_rst.w rename to examples/hello_world_rst.w index 0811081..a021705 100644 --- a/test/test_rst.w +++ b/examples/hello_world_rst.w @@ -7,6 +7,7 @@ Jason R. Fruit =============== .. include:: +.. include:: .. contents:: @@ -30,7 +31,7 @@ This document contains the makings of two files; the first, ``test.py``, uses simple string concatenation to build its output message: -@o test.py +@o hw_rst_1.py @{ @< Import the os module @> @< Get the OS description @> @@ -40,7 +41,7 @@ message: The second uses string substitution: -@o test2.py +@o hw_rst_2.py @{ @< Import the os module @> @< Get the OS description @> @@ -80,11 +81,11 @@ better: @d Construct the message with Substitution @{ -msg = "Hello, %s!" % os_name +msg = f"Hello, {os_name}!" @} -We'll use the first of these methods in ``test.py``, and the -other in ``test2.py``. +We'll use the first of these methods in ``rst_test_1.py``, and the +other in ``rst_test_2.py``. Printing the message ---------------------- @@ -95,6 +96,5 @@ they have: @d Print the message @{ -print msg +print(msg) @} - diff --git a/examples/hw_latex_1.py b/examples/hw_latex_1.py new file mode 100644 index 0000000..0ce46a8 --- /dev/null +++ b/examples/hw_latex_1.py @@ -0,0 +1,13 @@ + + +import os + + +os_name = os.name + + +msg = "Hello, " + os_name + "!" + + +print(msg) + diff --git a/examples/hw_latex_2.py b/examples/hw_latex_2.py new file mode 100644 index 0000000..75bd573 --- /dev/null +++ b/examples/hw_latex_2.py @@ -0,0 +1,13 @@ + + +import os + + +os_name = os.name + + +msg = f"Hello, {os_name}!" + + +print(msg) + diff --git a/examples/hw_rst_1.py b/examples/hw_rst_1.py new file mode 100644 index 0000000..0ce46a8 --- /dev/null +++ b/examples/hw_rst_1.py @@ -0,0 +1,13 @@ + + +import os + + +os_name = os.name + + +msg = "Hello, " + os_name + "!" + + +print(msg) + diff --git a/examples/hw_rst_2.py b/examples/hw_rst_2.py new file mode 100644 index 0000000..75bd573 --- /dev/null +++ b/examples/hw_rst_2.py @@ -0,0 +1,13 @@ + + +import os + + +os_name = os.name + + +msg = f"Hello, {os_name}!" + + +print(msg) + diff --git a/index.html b/index.html index bf878c6..6029ba8 100644 --- a/index.html +++ b/index.html @@ -1,8 +1,12 @@ - - - -Redirect - - -Sorry, you should have been redirected pyweb.html. + + + + + + + py-web-tool + + +

Sorry, you should have been redirected src/pyweb.html.

+ diff --git a/intro.w b/intro.w deleted file mode 100644 index 5fde886..0000000 --- a/intro.w +++ /dev/null @@ -1,865 +0,0 @@ -.. pyweb/intro.w - -Introduction -============ - -Literate programming was pioneered by Knuth as a method for -developing readable, understandable presentations of programs. -These would present a program in a literate fashion for people -to read and understand; this would be in parallel with presentation as source text -for a compiler to process and both would be generated from a common source file. - -One intent is to synchronize the program source with the -documentation about that source. If the program and the documentation -have a common origin, then the traditional gaps between intent -(expressed in the documentation) and action (expressed in the -working program) are significantly reduced. - -**pyWeb** is a literate programming tool that combines the actions -of *weaving* a document with *tangling* source files. -It is independent of any source language. -It is designed to work with RST document markup. -Is uses a simple set of markup tags to define chunks of code and -documentation. - -Background ------------ - -The following is an almost verbatim quote from Briggs' *nuweb* documentation, -and provides an apt summary of Literate Programming. - - In 1984, Knuth introduced the idea of *literate programming* and - described a pair of tools to support the practise (Donald E. Knuth, - "Literate Programming", *The Computer Journal* 27 (1984), no. 2, 97-111.) - His approach was to combine Pascal code with T\ :sub:`e`\ X documentation to - produce a new language, ``WEB``, that offered programmers a superior - approach to programming. He wrote several programs in ``WEB``, - including ``weave`` and ``tangle``, the programs used to support - literate programming. - The idea was that a programmer wrote one document, the web file, that - combined documentation written in T\ :sub:`e`\ X (Donald E. Knuth, - T\ :sub:`e`\ X book, Computers and Typesetting, 1986) with code (written in Pascal). - - Running ``tangle`` on the web file would produce a complete - Pascal program, ready for compilation by an ordinary Pascal compiler. - The primary function of ``tangle`` is to allow the programmer to - present elements of the program in any desired order, regardless of - the restrictions imposed by the programming language. Thus, the - programmer is free to present his program in a top-down fashion, - bottom-up fashion, or whatever seems best in terms of promoting - understanding and maintenance. - - Running ``weave`` on the web file would produce a T\ :sub:`e`\ X file, ready - to be processed by T\ :sub:`e`\ X. The resulting document included a variety of - automatically generated indices and cross-references that made it much - easier to navigate the code. Additionally, all of the code sections - were automatically prettyprinted, resulting in a quite impressive - document. - - Knuth also wrote the programs for T\ :sub:`e`\ X and ``METAFONT`` - entirely in ``WEB``, eventually publishing them in book - form. These are probably the - largest programs ever published in a readable form. - - -Other Tools ------------- - -Numerous tools have been developed based on Knuth's initial -work. A relatively complete survey is available at sites -like `Literate Programming `_, -and the OASIS -`XML Cover Pages: Literate Programming with SGML and XML `_. - -The immediate predecessors to this **pyWeb** tool are -`FunnelWeb `_, -`noweb `_ and -`nuweb `_. The ideas lifted from these other -tools created the foundation for **pyWeb**. - -There are several Python-oriented literate programming tools. -These include -`LEO `_, -`interscript `_, -`lpy `_, -`py2html `_, -`PyLit `_. - -The *FunnelWeb* tool is independent of any programming language -and only mildly dependent on T\ :sub:`e`\ X. -It has 19 commands, many of which duplicate features of HTML or -L\ :sub:`a`\ T\ :sub:`e`\ X. - -The *noweb* tool was written by Norman Ramsey. -This tool uses a sophisticated multi-processing framework, via Unix -pipes, to permit flexible manipulation of the source file to tangle -and weave the programming language and documentation markup files. - -The *nuweb* Simple Literate Programming Tool was developed by -Preston Briggs (preston@@tera.com). His work was supported by ARPA, -through ONR grant N00014-91-J-1989. It is written -in C, and very focused on producing L\ :sub:`a`\ T\ :sub:`e`\ X documents. It can -produce HTML, but this is clearly added after the fact. It cannot be -easily extended, and is not object-oriented. - -The *LEO* tool is a structured GUI editor for creating -source. It uses XML and *noweb*\ -style chunk management. It is more -than a simple weave and tangle tool. - -The *interscript* tool is very large and sophisticated, but doesn't gracefully -tolerate HTML markup in the document. It can create a variety of -markup languages from the interscript source, making it suitable for -creating HTML as well as L\ :sub:`a`\ T\ :sub:`e`\ X. - -The *lpy* tool can produce very complex HTML representations of -a Python program. It works by locating documentation markup embedded -in Python comments and docstrings. This is called "inverted literate -programming". - -The *py2html* tool does very sophisticated syntax coloring. - -The *PyLit* tool is perhaps the very best approach to simple Literate -programming, since it leverages an existing lightweight markup language -and it's output formatting. However, it's limited in the presentation order, -making it difficult to present a complex Python module out of the proper -Python required presentation. - -**pyWeb** ---------- - -**pyWeb** works with any -programming language. It can work with any markup language, but is currently -configured to work with RST only. This philosophy -comes from *FunnelWeb* -*noweb*, *nuweb* and *interscript*. The primary differences -between **pyWeb** and other tools are the following. - -- **pyWeb** is object-oriented, permitting easy extension. - *noweb* extensions - are separate processes that communicate through a sophisticated protocol. - *nuweb* is not easily extended without rewriting and recompiling - the C programs. - -- **pyWeb** is built in the very portable Python programming - language. This allows it to run anywhere that Python 3.3 runs, with - only the addition of docutils. This makes it a useful - tool for programmers in any language. - -- **pyWeb** is much simpler than *FunnelWeb*, *LEO* or *Interscript*. It has - a very limited selection of commands, but can still produce - complex programs and HTML documents. - -- **pyWeb** does not invent a complex markup language like *Interscript*. - Because *Iterscript* has its own markup, it can generate L\ :sub:`a`\ T\ :sub:`e`\ X or HTML or other - output formats from a unique input format. While powerful, it seems simpler to - avoid inventing yet another sophisticated markup language. The language **pyWeb** - uses is very simple, and the author's use their preferred markup language almost - exclusively. - -- **pyWeb** supports the forward literate programming philosophy, - where a source document creates programming language and markup language. - The alternative, deriving the document from markup embedded in - program comments ("inverted literate programming"), seems less appealing. - The disadvantage of inverted literate programming is that the final document - can't reflect the original author's preferred order of exposition, - since that informtion generally isn't part of the source code. - -- **pyWeb** also specifically rejects some features of *nuweb* - and *FunnelWeb*. These include the macro capability with parameter - substitution, and multiple references to a chunk. These two capabilities - can be used to grow object-like applications from non-object programming - languages (*e.g.* C or Pascal). Since most modern languages (Python, - Java, C++) are object-oriented, this macro capability is more of a problem - than a help. - -- Since **pyWeb** is built in the Python interpreter, a source document - can include Python expressions that are evaluated during weave operation to - produce time stamps, source file descriptions or other information in the woven - or tangled output. - - -**pyWeb** works with any programming language; it can work with any markup language. -The initial release supports RST via simple templates. - -The following is extensively quoted from Briggs' *nuweb* documentation, -and provides an excellent background in the advantages of the very -simple approach started by *nuweb* and adopted by **pyWeb**. - - The need to support arbitrary - programming languages has many consequences: - - :No prettyprinting: - Both ``WEB`` and ``CWEB`` are able to - prettyprint the code sections of their documents because they - understand the language well enough to parse it. Since we want to use - *any* language, we've got to abandon this feature. - However, we do allow particular individual formulas or fragments - of L\ :sub:`a`\ T\ :sub:`e`\ X - or HTML code to be formatted and still be part of the output files. - - :Limited index of identifiers: - Because ``WEB`` knows about Pascal, - it is able to construct an index of all the identifiers occurring in - the code sections (filtering out keywords and the standard type - identifiers). Unfortunately, this isn't as easy in our case. We don't - know what an identifier looks like in each language and we certainly - don't know all the keywords. We provide a mechanism to mark - identifiers, and we use a pretty standard pattern for recognizing - identifiers almost most programming languages. - - - Of course, we've got to have some compensation for our losses or the - whole idea would be a waste. Here are the advantages I [Briggs] can see: - - :Simplicity: - The majority of the commands in ``WEB`` are concerned with control of the - automatic prettyprinting. Since we don't prettyprint, many commands are - eliminated. A further set of commands is subsumed by L\ :sub:`a`\ T\ :sub:`e`\ X - and may also be eliminated. As a result, our set of commands is reduced to - only about seven members (explained in the next section). - This simplicity is also reflected in the size of this tool, - which is quite a bit smaller than the tools used with other approaches. - - :No prettyprinting: - Everyone disagrees about how their code should look, so automatic - formatting annoys many people. One approach is to provide ways to - control the formatting. Our approach is simpler -- we perform no - automatic formatting and therefore allow the programmer complete - control of code layout. - - :Control: - We also offer the programmer reasonably complete control of the - layout of his output files (the files generated during tangling). - Of course, this is essential for languages that are sensitive to layout; - but it is also important in many practical situations, *e.g.*, debugging. - - :Speed: - Since [**pyWeb**] doesn't do too much, it runs very quickly. - It combines the functions of ``tangle`` and ``weave`` into a single - program that performs both functions at once. - - :Chunk numbers: - Inspired by the example of **noweb**, [**pyWeb**] refers to all program code - chunks by a simple, ascending sequence number through the file. - This becomes the HTML anchor name, also. - - :Multiple file output: - The programmer may specify more than one output file in a single [**pyWeb**] - source file. This is required when constructing programs in a combination of - languages (say, Fortran and C). It's also an advantage when constructing - very large programs. - -Use Cases ------------ - -**pyWeb** supports two use cases, `Tangle Source Files`_ and `Weave Documentation`_. -These are often combined into a single request of the application that will both -weave and tangle. - -Tangle Source Files -~~~~~~~~~~~~~~~~~~~ - -A user initiates this process when they have a complete ``.w`` file that contains -a description of source files. These source files are described with ``@@o`` commands -in the ``.w`` file. - -The use case is successful when the source files are produced. - -Outside this use case, the user will debug those source files, possibly updating the -``.w`` file. This will lead to a need to restart this use case. - -The use case is a failure when the source files cannot be produced, due to -errors in the ``.w`` file. These must be corrected based on information in log messages. - -The sequence is simply ``./pyweb.py *theFile*.w``. - -Weave Documentation -~~~~~~~~~~~~~~~~~~~~ - -A user initiates this process when they have a ``.w`` file that contains -a description of a document to produce. The document is described by the entire -``.w`` file. - -The use case is successful when the documentation file is produced. - -Outside this use case, the user will edit the documentation file, possibly updating the -``.w`` file. This will lead to a need to restart this use case. - -The use case is a failure when the documentation file cannot be produced, due to -errors in the ``.w`` file. These must be corrected based on information in log messages. - -The sequence is simply ``./pyweb.py *theFile*.w``. - -Tangle, Regression Test and Weave -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -A user initiates this process when they have a ``.w`` file that contains -a description of a document to produce. The document is described by the entire -``.w`` file. Further, their final document should include regression test output -from the source files created by the tangle operation. - -The use case is successful when the documentation file is produced, including -current regression test output. - -Outside this use case, the user will edit the documentation file, possibly updating the -``.w`` file. This will lead to a need to restart this use case. - -The use case is a failure when the documentation file cannot be produced, due to -errors in the ``.w`` file. These must be corrected based on information in log messages. - -The use case is a failure when the documentation file does not include current -regression test output. - -The sequence is as follows: - -.. parsed-literal:: - - ./pyweb.py -xw -pi *theFile*\ .w - python *theTest* >\ *aLog* - ./pyweb.py -xt *theFile*\ .w - - -The first step excludes weaving and permits errors on the ``@@i`` command. The ``-pi`` option -is necessary in the event that the log file does not yet exist. The second step -runs the regression test, creating a log file. The third step weaves the final document, -including the regression test output. - -Writing **pyWeb** ``.w`` Files -------------------------------- - -The essence of literate programming is a markup language that distinguishes code -from documentation. For tangling, the code is relevant. For weaving, both code -and documentation are relevant. - -The **pyWeb** markup defines a sequence of *Chunks*. -Each Chunk is either program source code to -be *tangled* or it is documentation to be *woven*. The bulk of -the file is typically documentation chunks that describe the program in -some human-oriented markup language like RST, HTML, or LaTeX. - - -The **pyWeb** tool parses the input, and performs the -tangle and weave operations. It *tangles* each individual output file -from the program source chunks. It *weaves* a final documentation file -file from the entire sequence of chunks provided, mixing the author's -original documentation with some markup around the embedded program source. - -**pyWeb** markup surrounds the code with tags. Everything else is documentation. -When tangling, the tagged code is assembled into the final file. -When weaving, the tags are replaced with output markup. This means that **pyWeb** -is not **totally** independent of the output markup. - -The code chunks will have their indentation adjusted to match the context in which -they were originally defined. This assures that Python (which relies on indentation) -parses correctly. For other languages, proper indentation is expected but not required. - -The non-code chunks are not transformed up in any way. Everything that's not -explicitly a code chunk is simply output without modification. - -All of the **pyWeb** tags begin with ``@@``. This can be changed. - -The *Structural* tags (historically called "major commands") partition the input and define the -various chunks. The *Inline* tags are (called "minor commands") are used to control the -woven and tangled output from those chunks. There are *Content* tags which generate -summary cross-reference content in woven files. - - -Structural Tags -~~~~~~~~~~~~~~~ - -There are two definitional tags; these define the various chunks -in an input file. - -``@@o`` *file* ``@@{`` *text* ``@@}`` - - The ``@@o`` (output) command defines a named output file chunk. - The text is tangled to the named - file with no alteration. It is woven into the document - in an appropriate fixed-width font. - - There are options available to specify comment conventions - for the tangled output; this allows inclusion of source - line numbers. - -``@@d`` *name* ``@@{`` *text* ``@@}`` - - The ``@@d`` (define) command defines a named chunk of program source. - This text is tangled - or woven when it is referenced by the *reference* inline tag. - - There are options available to specify the indentation for this - particular chunk. In rare cases, it can be helpful to override - the indentation context. - -Each ``@@o`` and ``@@d`` tag is followed by a chunk which is -delimited by ``@@{`` and ``@@}`` tags. -At the end of that chunk, there is an optional "major" tag. - -``@@|`` - - A chunk may define user identifiers. The list of defined identifiers is placed - in the chunk, separated by the ``@@|`` separator. - - -Additionally, these tags provide for the inclusion of additional input files. -This is necessary for decomposing a long document into easy-to-edit sections. - -``@@i`` *file* - - The ``@@i`` (include) command includes another file. The previous chunk - is ended. The file is processed completely, then a new chunk - is started for the text after the ``@@i`` command. - -All material that is not explicitly in a ``@@o`` or ``@@d`` named chunk is -implicitly collected into a sequence of anonymous document source chunks. -These anonymous chunks form the backbone of the document that is woven. -The anonymous chunks are never tangled into output program source files. -They are woven into the document without any alteration. - -Note that white space (line breaks (``'\n'``), tabs and spaces) have no effect on the input parsing. -They are completely preserved on output. - -The following example has three chunks: - -.. parsed-literal:: - - Some RST-format documentation that describes the following piece of the - program. - - @@o myFile.py - @@{ - import math - print( math.pi ) - @@| math math.pi - @@} - - Some more RST documentation. - -This starts with an anonymous chunk of -documentation. It includes a named output chunk which will write to ``myFile.py``. -It ends with an anonymous chunk of documentation. - -Inline Tags -~~~~~~~~~~~~ - -There are several tags that are replaced by content in the woven output. - -``@@@@`` - - The ``@@@@`` command creates a single ``@@`` in the output file. - This is replaced in tangled as well as woven output. - -``@@<``\ *name*\ ``@@>`` - - The *name* references a named chunk. - When tangling, the referenced chunk replaces the reference command. - When weaving, a reference marker is used. For example, in RST, this can be - replaced with RST ```reference`_`` markup. - Note that the indentation prior to the ``@@<`` tag is preserved - for the tangled chunk that replaces the tag. - - -``@@(``\ *Python expression*\ ``@@)`` - - The *Python expression* is evaluated and the result is tangled or - woven in place. A few global variables and modules are available. - These are described in `Expression Context`_. - -Content Tags -~~~~~~~~~~~~~ - -There are three index creation tags that are replaced by content in the woven output. - - -``@@f`` - - The ``@@f`` command inserts a file cross reference. This - lists the name of each file created by an ``@@o`` command, and all of the various - chunks that are concatenated to create this file. - -``@@m`` - - The ``@@m`` command inserts a named chunk ("macro") cross reference. This - lists the name of each chunk created by a ``@@d`` command, and all of the various - chunks that are concatenated to create the complete chunk. - -``@@u`` - - The ``@@u`` command inserts a user identifier cross reference. - This index lists the name of each chunk created by an ``@@d`` command or ``@@|``, - and all of the various chunks that are concatenated to create the complete chunk. - - -Additional Features -~~~~~~~~~~~~~~~~~~~ - -**Sequence Numbers**. The named chunks (from both ``@@o`` and ``@@d`` commands) are assigned -unique sequence numbers to simplify cross references. - -**Case Sensitive**. Chunk names and file names are case sensitive. - -**Abbreviations**. Chunk names can be abbreviated. A partial name can have a trailing ellipsis (...), -this will be resolved to the full name. The most typical use for this -is shown in the following example: - -.. parsed-literal:: - - Some RST-format documentation. - - @@o myFile.py - @@{ - @@ - print( math.pi,time.time() ) - @@} - - Some notes on the packages used. - - @@d imports... - @@{ - import math,time - @@| math time - @@} - - Some more RST-format documentation. - -This example shows five chunks. - -1. An anonymous chunk of documentation. - -2. A named chunk that tangles the ``myFile.py`` output. It has - a reference to the ``imports of the various packages used`` chunk. - Note that the full name of the chunk is essentially a line of - documentation, traditionally done as a comment line in a non-literate - programming environment. - -3. An anonymous chunk of documentation. - -4. A named chunk with an abbreviated name. The ``imports...`` - matches the name ``imports of the various packages used``. - Set off after the ``@@|`` separator is - the list of user-specified identifiers defined in this chunk. - -5. An anonymous chunk of documentation. - -Note that the first time a name appears (in a reference or definition), -it **must** be the full name. All subsequent uses can be elisions. -Also not that ambiguous elision is an annoying problem when you -first start creating a document. - -**Concatenation**. Named chunks are concatenated from their various pieces. -This allows a named chunk to be broken into several pieces, simplifying -the description. This is most often used when producing -fairly complex output files. - -.. parsed-literal:: - - An anonymous chunk with some RST documentation. - - @@o myFile.py - @@{ - import math,time - @@} - - Some notes on the packages used. - - @@o myFile.py - @@{ - print math.pi,time.time() - @@} - - Some more HTML documentation. - -This example shows five chunks. - -1. An anonymous chunk of documentation. - -2. A named chunk that tangles the ``myFile.py`` output. It has - the first part of the file. In the woven document - this is marked with ``"="``. - -3. An anonymous chunk of documentation. - -4. A named chunk that also tangles the ``myFile.py`` output. This - chunk's content is appended to the first chunk. In the woven document - this is marked with ``"+="``. - -5. An anonymous chunk of documentation. - -**Newline Preservation**. Newline characters are preserved on input. -Because of this the output may appear to have excessive newlines. -In all of the above examples, each -named chunk was defined with the following. - -.. parsed-literal:: - - @@{ - import math,time - @@} - -This puts a newline character before and after the import line. - -Controlling Indentation -~~~~~~~~~~~~~~~~~~~~~~~ - -We have two choices in indentation: - -- Context-Sensitive. - -- Consistent. - -If we have context-sensitive indentation, then the indentation of a chunk reference -is applied to the entire chunk when expanded in place of the reference. This makes it -simpler to prepare source for languages (like Python) where indentation -is important. - -There are cases, however, when this is not desirable. There are some places in Python -where we want to create long, triple-quoted strings with indentation that does -not follow the prevailing indentations of the surrounding code. - -Here's how the context-sensitive indentation works. - -.. parsed-literal:: - - @@o myFile.py - @@{ - def aFunction( a, b ): - @@ - @@| aFunction @@} - - @@d body... - @@{ - """doc string""" - return a + b - @@} - -The tangled output from this will look like the following. -All of the newline characters are preserved, and the reference to -*body of the aFunction* is indented to match the prevailing -indent where it was referenced. In the following example, -explicit line markers of ``~`` are provided to make the blank lines -more obvious. - -.. parsed-literal:: - - ~ - ~def aFunction( a, b ): - ~ - ~ """doc string""" - ~ return a + b - ~ - -[The ``@@|`` command shows that this chunk defines the identifier ``aFunction``.] - -This leads to a difficult design choice. - -- Do we use context-sensitive indentation without any exceptions? - This is the current implementation. - -- Do we use consistent indentation and require the author to get it right? - This seems to make Python awkward, since we might indent our outdent a - ``@@<`` *name* ``@@>`` command, expecting the chunk to indent properly. - -- Do we use context-sensitive indentation with an exception indicator? - This seems to go against the utter simplicity we're cribbing from **noweb**. - However, it makes a great deal of sense to add an option for ``@@d`` chunks to - supersede context-sensitive indentation. The author must then get it right. - - The syntax to define a section looks like this: - -.. parsed-literal:: - - @@d -noindent some chunk name - @@{*First partial line* - *More that uses """* - @@} - -We might reference such a section like this. - -.. parsed-literal:: - - @@d some bigger chunk... - @@{*code* - @@ - @@} - -This will include the ``-noindent`` section by resetting the contextual indentation -to zero. The *First partial line* line will be output after the four spaces -provided by the ``some bigger chunk`` context. - -After the first newline (*More that uses """*) will be at the left margin. - -Tracking Source Line Numbers -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Since the tangled output files are -- well -- tangled, it can be difficult to -trace back from a Python error stack to the original line in the ``.w`` file that -needs to be fixed. - -To facilitate this, there is a two-step operation to get more detailed information -on how tangling worked. - -1. Use the -n command-line option to get line numbers. - -2. Include comment indicators on the ``@@o`` commands that define output files. - -The expanded syntax for ``@@o`` looks like this. - -.. parsed-literal:: - - @@o -start /* -end */ page-layout.css - @@{ - *Some CSS code* - @@} - -We've added two options: ``-start /*`` and ``-end */`` which define comment -start and end syntax. This will lead to comments embedded in the tangled output -which contain source line numbers for every (every!) chunk. - -Expression Context -~~~~~~~~~~~~~~~~~~~~ - -There are two possible implementations for evaluation of a Python -expression in the input. - -1. Create an ``ExpressionCommand``, and append this to the current ``Chunk``. - This will allow evaluation during weave processing and during tangle processing. This - makes the entire weave (or tangle) context available to the expression, including - completed cross reference information. - -2. Evaluate the expression during input parsing, and append the resulting text - as a ``TextCommand`` to the current ``Chunk``. This provides a common result - available to both weave and parse, but the only context available is the ``WebReader`` and - the incomplete ``Web``, built up to that point. - - -In this implementation, we adopt the latter approach, and evaluate expressions immediately. -A simple global context is created with the following variables defined. - -:os.path: - This is the standard ``os.path`` module. The complete ``os`` module is not - available. Just this one item. - -:datetime: - This is the standard ``datetime`` module. - -:platform: - This is the standard ``platform`` module. - -:__builtins__: - Most of the built-ins are available, too. Not all. - ``exec()``, ``eval()``, ``open()`` and ``__import__()`` aren't available. - -:theLocation: - A tuple with the file name, first line number and last line number - for the original expression's location. - -:theWebReader: - The ``WebReader`` instance doing the parsing. - -:theFile: - The ``.w`` file being processed. - -:thisApplication: - The name of the running **pyWeb** application. It may not be pyweb.py, - if some other script is being used. - -:__version__: - The version string in the **pyWeb** application. - - -Running **pyWeb** to Tangle and Weave --------------------------------------- - -Assuming that you have marked ``pyweb.py`` as executable, -you do the following. - -.. parsed-literal:: - - ./pyweb.py *file*... - -This will tangle the ``@@o`` commands in each *file*. -It will also weave the output, and create *file*.txt. - -Command Line Options -~~~~~~~~~~~~~~~~~~~~~ - -Currently, the following command line options are accepted. - - -:-v: - Verbose logging. - -:-s: - Silent operation. - -:-c\ *x*: - Change the command character from ``@@`` to ``*x*``. - -:-w\ *weaver*: - Choose a particular documentation weaver template. Currently the choices - are RST and HTML. - -:-xw: - Exclude weaving. This does tangling of source program files only. - -:-xt: - Exclude tangling. This does weaving of the document file only. - -:-p\ *command*: - Permit errors in the given list of commands. The most common - version is ``-pi`` to permit errors in locating an include file. - This is done in the following scenario: pass 1 uses ``-xw -pi`` to exclude - weaving and permit include-file errors; - the tangled program is run to create test results; pass 2 uses - ``-xt`` to exclude tangling and include the test results. - -Bootstrapping --------------- - -**pyWeb** is written using **pyWeb**. The distribution includes the original ``.w`` -files as well as a ``.py`` module. - -The bootstrap procedure is this. - -.. parsed-literal:: - - python pyweb.py pyweb.w - rst2html.py pyweb.rst pyweb.html - -The resulting ``pyweb.html`` file is the final documentation. - -Similarly, the tests are bootstrapped from ``.w`` files. - -.. parsed-literal:: - - cd test - python ../pyweb.py pyweb_test.w - PYTHONPATH=.. python test.py - rst2html.py pyweb_test.rst pyweb_test.html - -Dependencies -------------- - -**pyWeb** requires Python 3.3 or newer. - -If you create RST output, you'll want to use docutils to translate -the RST to HTML or LaTeX or any of the other formats supported by docutils. - -Acknowledgements ----------------- - -This application is very directly based on (derived from?) work that - preceded this, particularly the following: - -- Ross N. Williams' *FunnelWeb* http://www.ross.net/funnelweb/ - -- Norman Ramsey's *noweb* http://www.eecs.harvard.edu/~nr/noweb/ - -- Preston Briggs' *nuweb* http://sourceforge.net/projects/nuweb/ - - Currently supported by Charles Martin and Marc W. Mengel - -Also, after using John Skaller's *interscript* http://interscript.sourceforge.net/ -for two large development efforts, I finally understood the feature set I really needed. - -Jason Fruit contributed to the previous version. diff --git a/jedit.rst b/jedit.rst deleted file mode 100644 index a901fab..0000000 --- a/jedit.rst +++ /dev/null @@ -1,8832 +0,0 @@ -############################## -pyWeb Literate Programming 2.3 -############################## - -================================================= -Yet Another Literate Programming Tool -================================================= - -.. include:: -.. include:: - -.. contents:: - - -.. pyweb/intro.w - -Introduction -============ - -Literate programming was pioneered by Knuth as a method for -developing readable, understandable presentations of programs. -These would present a program in a literate fashion for people -to read and understand; this would be in parallel with presentation as source text -for a compiler to process and both would be generated from a common source file. - -One intent is to synchronize the program source with the -documentation about that source. If the program and the documentation -have a common origin, then the traditional gaps between intent -(expressed in the documentation) and action (expressed in the -working program) are significantly reduced. - -**pyWeb** is a literate programming tool that combines the actions -of *weaving* a document with *tangling* source files. -It is independent of any source language. -It is designed to work with RST document markup. -Is uses a simple set of markup tags to define chunks of code and -documentation. - -Background ------------ - -The following is an almost verbatim quote from Briggs' *nuweb* documentation, -and provides an apt summary of Literate Programming. - - In 1984, Knuth introduced the idea of *literate programming* and - described a pair of tools to support the practise (Donald E. Knuth, - "Literate Programming", *The Computer Journal* 27 (1984), no. 2, 97-111.) - His approach was to combine Pascal code with T\ :sub:`e`\ X documentation to - produce a new language, ``WEB``, that offered programmers a superior - approach to programming. He wrote several programs in ``WEB``, - including ``weave`` and ``tangle``, the programs used to support - literate programming. - The idea was that a programmer wrote one document, the web file, that - combined documentation written in T\ :sub:`e`\ X (Donald E. Knuth, - T\ :sub:`e`\ X book, Computers and Typesetting, 1986) with code (written in Pascal). - - Running ``tangle`` on the web file would produce a complete - Pascal program, ready for compilation by an ordinary Pascal compiler. - The primary function of ``tangle`` is to allow the programmer to - present elements of the program in any desired order, regardless of - the restrictions imposed by the programming language. Thus, the - programmer is free to present his program in a top-down fashion, - bottom-up fashion, or whatever seems best in terms of promoting - understanding and maintenance. - - Running ``weave`` on the web file would produce a T\ :sub:`e`\ X file, ready - to be processed by T\ :sub:`e`\ X. The resulting document included a variety of - automatically generated indices and cross-references that made it much - easier to navigate the code. Additionally, all of the code sections - were automatically prettyprinted, resulting in a quite impressive - document. - - Knuth also wrote the programs for T\ :sub:`e`\ X and ``METAFONT`` - entirely in ``WEB``, eventually publishing them in book - form. These are probably the - largest programs ever published in a readable form. - - -Other Tools ------------- - -Numerous tools have been developed based on Knuth's initial -work. A relatively complete survey is available at sites -like `Literate Programming `_, -and the OASIS -`XML Cover Pages: Literate Programming with SGML and XML `_. - -The immediate predecessors to this **pyWeb** tool are -`FunnelWeb `_, -`noweb `_ and -`nuweb `_. The ideas lifted from these other -tools created the foundation for **pyWeb**. - -There are several Python-oriented literate programming tools. -These include -`LEO `_, -`interscript `_, -`lpy `_, -`py2html `_, -`PyLit `_. - -The *FunnelWeb* tool is independent of any programming language -and only mildly dependent on T\ :sub:`e`\ X. -It has 19 commands, many of which duplicate features of HTML or -L\ :sub:`a`\ T\ :sub:`e`\ X. - -The *noweb* tool was written by Norman Ramsey. -This tool uses a sophisticated multi-processing framework, via Unix -pipes, to permit flexible manipulation of the source file to tangle -and weave the programming language and documentation markup files. - -The *nuweb* Simple Literate Programming Tool was developed by -Preston Briggs (preston@tera.com). His work was supported by ARPA, -through ONR grant N00014-91-J-1989. It is written -in C, and very focused on producing L\ :sub:`a`\ T\ :sub:`e`\ X documents. It can -produce HTML, but this is clearly added after the fact. It cannot be -easily extended, and is not object-oriented. - -The *LEO* tool is a structured GUI editor for creating -source. It uses XML and *noweb*\ -style chunk management. It is more -than a simple weave and tangle tool. - -The *interscript* tool is very large and sophisticated, but doesn't gracefully -tolerate HTML markup in the document. It can create a variety of -markup languages from the interscript source, making it suitable for -creating HTML as well as L\ :sub:`a`\ T\ :sub:`e`\ X. - -The *lpy* tool can produce very complex HTML representations of -a Python program. It works by locating documentation markup embedded -in Python comments and docstrings. This is called "inverted literate -programming". - -The *py2html* tool does very sophisticated syntax coloring. - -The *PyLit* tool is perhaps the very best approach to simple Literate -programming, since it leverages an existing lightweight markup language -and it's output formatting. However, it's limited in the presentation order, -making it difficult to present a complex Python module out of the proper -Python required presentation. - -pyWeb ------ - -**pyWeb** works with any -programming language. It can work with any markup language, but is currently -configured to work with RST only. This philosophy -comes from *FunnelWeb* -*noweb*, *nuweb* and *interscript*. The primary differences -between **pyWeb** and other tools are the following. - -- **pyWeb** is object-oriented, permitting easy extension. - *noweb* extensions - are separate processes that communicate through a sophisticated protocol. - *nuweb* is not easily extended without rewriting and recompiling - the C programs. - -- **pyWeb** is built in the very portable Python programming - language. This allows it to run anywhere that Python 3.3 runs, with - only the addition of docutils. This makes it a useful - tool for programmers in any language. - -- **pyWeb** is much simpler than *FunnelWeb*, *LEO* or *Interscript*. It has - a very limited selection of commands, but can still produce - complex programs and HTML documents. - -- **pyWeb** does not invent a complex markup language like *Interscript*. - Because *Iterscript* has its own markup, it can generate L\ :sub:`a`\ T\ :sub:`e`\ X or HTML or other - output formats from a unique input format. While powerful, it seems simpler to - avoid inventing yet another sophisticated markup language. The language **pyWeb** - uses is very simple, and the author's use their preferred markup language almost - exclusively. - -- **pyWeb** supports the forward literate programming philosophy, - where a source document creates programming language and markup language. - The alternative, deriving the document from markup embedded in - program comments ("inverted literate programming"), seems less appealing. - The disadvantage of inverted literate programming is that the final document - can't reflect the original author's preferred order of exposition, - since that informtion generally isn't part of the source code. - -- **pyWeb** also specifically rejects some features of *nuweb* - and *FunnelWeb*. These include the macro capability with parameter - substitution, and multiple references to a chunk. These two capabilities - can be used to grow object-like applications from non-object programming - languages (*e.g.* C or Pascal). Since most modern languages (Python, - Java, C++) are object-oriented, this macro capability is more of a problem - than a help. - -- Since **pyWeb** is built in the Python interpreter, a source document - can include Python expressions that are evaluated during weave operation to - produce time stamps, source file descriptions or other information in the woven - or tangled output. - - -**pyWeb** works with any programming language; it can work with any markup language. -The initial release supports RST via simple templates. - -The following is extensively quoted from Briggs' *nuweb* documentation, -and provides an excellent background in the advantages of the very -simple approach started by *nuweb* and adopted by **pyWeb**. - - The need to support arbitrary - programming languages has many consequences: - - :No prettyprinting: - Both ``WEB`` and ``CWEB`` are able to - prettyprint the code sections of their documents because they - understand the language well enough to parse it. Since we want to use - *any* language, we've got to abandon this feature. - However, we do allow particular individual formulas or fragments - of L\ :sub:`a`\ T\ :sub:`e`\ X - or HTML code to be formatted and still be part of the output files. - - :Limited index of identifiers: - Because ``WEB`` knows about Pascal, - it is able to construct an index of all the identifiers occurring in - the code sections (filtering out keywords and the standard type - identifiers). Unfortunately, this isn't as easy in our case. We don't - know what an identifier looks like in each language and we certainly - don't know all the keywords. We provide a mechanism to mark - identifiers, and we use a pretty standard pattern for recognizing - identifiers almost most programming languages. - - - Of course, we've got to have some compensation for our losses or the - whole idea would be a waste. Here are the advantages I [Briggs] can see: - - :Simplicity: - The majority of the commands in ``WEB`` are concerned with control of the - automatic prettyprinting. Since we don't prettyprint, many commands are - eliminated. A further set of commands is subsumed by L\ :sub:`a`\ T\ :sub:`e`\ X - and may also be eliminated. As a result, our set of commands is reduced to - only about seven members (explained in the next section). - This simplicity is also reflected in the size of this tool, - which is quite a bit smaller than the tools used with other approaches. - - :No prettyprinting: - Everyone disagrees about how their code should look, so automatic - formatting annoys many people. One approach is to provide ways to - control the formatting. Our approach is simpler -- we perform no - automatic formatting and therefore allow the programmer complete - control of code layout. - - :Control: - We also offer the programmer reasonably complete control of the - layout of his output files (the files generated during tangling). - Of course, this is essential for languages that are sensitive to layout; - but it is also important in many practical situations, *e.g.*, debugging. - - :Speed: - Since [**pyWeb**] doesn't do too much, it runs very quickly. - It combines the functions of ``tangle`` and ``weave`` into a single - program that performs both functions at once. - - :Chunk numbers: - Inspired by the example of *nowe*, [**pyWeb**] refers to all program code - chunks by a simple, ascending sequence number through the file. - This becomes the HTML anchor name, also. - - :Multiple file output: - The programmer may specify more than one output file in a single [**pyWeb**] - source file. This is required when constructing programs in a combination of - languages (say, Fortran and C). It's also an advantage when constructing - very large programs. - -Use Cases ------------ - -**pyWeb** supports two use cases, `Tangle Source Files`_ and `Weave Documentation`_. -These are often combined into a single request of the application that will both -weave and tangle. - -Tangle Source Files -~~~~~~~~~~~~~~~~~~~ - -A user initiates this process when they have a complete ``.w`` file that contains -a description of source files. These source files are described with ``@o`` commands -in the ``.w`` file. - -The use case is successful when the source files are produced. - -Outside this use case, the user will debug those source files, possibly updating the -``.w`` file. This will lead to a need to restart this use case. - -The use case is a failure when the source files cannot be produced, due to -errors in the ``.w`` file. These must be corrected based on information in log messages. - -The sequence is simply ``./pyweb.py *theFile*.w``. - -Weave Documentation -~~~~~~~~~~~~~~~~~~~~ - -A user initiates this process when they have a ``.w`` file that contains -a description of a document to produce. The document is described by the entire -``.w`` file. - -The use case is successful when the documentation file is produced. - -Outside this use case, the user will edit the documentation file, possibly updating the -``.w`` file. This will lead to a need to restart this use case. - -The use case is a failure when the documentation file cannot be produced, due to -errors in the ``.w`` file. These must be corrected based on information in log messages. - -The sequence is simply ``./pyweb.py *theFile*.w``. - -Tangle, Regression Test and Weave -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -A user initiates this process when they have a ``.w`` file that contains -a description of a document to produce. The document is described by the entire -``.w`` file. Further, their final document should include regression test output -from the source files created by the tangle operation. - -The use case is successful when the documentation file is produced, including -current regression test output. - -Outside this use case, the user will edit the documentation file, possibly updating the -``.w`` file. This will lead to a need to restart this use case. - -The use case is a failure when the documentation file cannot be produced, due to -errors in the ``.w`` file. These must be corrected based on information in log messages. - -The use case is a failure when the documentation file does not include current -regression test output. - -The sequence is as follows: - -.. parsed-literal:: - - ./pyweb.py -xw -pi *theFile*\ .w - python *theTest* >\ *aLog* - ./pyweb.py -xt *theFile*\ .w - - -The first step excludes weaving and permits errors on the ``@i`` command. The ``-pi`` option -is necessary in the event that the log file does not yet exist. The second step -runs the regression test, creating a log file. The third step weaves the final document, -including the regression test output. - -Writing **pyWeb** ``.w`` Files -------------------------------- - -The essence of literate programming is a markup language that distinguishes code -from documentation. For tangling, the code is relevant. For weaving, both code -and documentation are relevant. - -The **pyWeb** markup defines a sequence of *Chunks*. -Each Chunk is either program source code to -be *tangled* or it is documentation to be *woven*. The bulk of -the file is typically documentation chunks that describe the program in -some human-oriented markup language like RST, HTML, or LaTeX. - - -The **pyWeb** tool parses the input, and performs the -tangle and weave operations. It *tangles* each individual output file -from the program source chunks. It *weaves* a final documentation file -file from the entire sequence of chunks provided, mixing the author's -original documentation with some markup around the embedded program source. - -**pyWeb** markup surrounds the code with tags. Everything else is documentation. -When tangling, the tagged code is assembled into the final file. -When weaving, the tags are replaced with output markup. This means that **pyWeb** -is not **totally** independent of the output markup. - -The code chunks will have their indentation adjusted to match the context in which -they were originally defined. This assures that Python (which relies on indentation) -parses correctly. For other languages, proper indentation is expected but not required. - -The non-code chunks are not transformed up in any way. Everything that's not -explicitly a code chunk is simply output without modification. - -All of the **pyWeb** tags begin with ``@``. This can be changed. - -The *Structural* tags (historically called "major commands") partition the input and define the -various chunks. The *Inline* tags are (called "minor commands") are used to control the -woven and tangled output from those chunks. - - -Structural Tags -~~~~~~~~~~~~~~~ - -There are two definitional tags; these define the various chunks -in an input file. - -``@o *file* @{ *text* @}`` - - The ``@o`` (output) command defines a named output file chunk. - The text is tangled to the named - file with no alteration. It is woven into the document - in an appropriate fixed-width font. - -``@d *name* @{ *text* @}`` - - The ``@d`` (define) command defines a named chunk of program source. - This text is tangled - or woven when it is referenced by the *reference* inline tag. - - -Each ``@o`` and ``@d`` tag is followed by a chunk which is -delimited by ``@{`` and ``@}`` tags. -At the end of that chunk, there is an optional "major" tag. - -``@|`` - - A chunk may define user identifiers. The list of defined identifiers is placed - in the chunk, separated by the ``@|`` separator. - - -Additionally, these tags provide for the inclusion of additional input files. -This is necessary for decomposing a long document into easy-to-edit sections. - -``@i *file*`` - - The ``@i`` (include) command includes another file. The previous chunk - is ended. The file is processed completely, then a new chunk - is started for the text after the ``@i`` command. - -All material that is not explicitly in a ``@o`` or ``@d`` named chunk is -implicitly collected into a sequence of anonymous document source chunks. -These anonymous chunks form the backbone of the document that is woven. -The anonymous chunks are never tangled into output program source files. -They are woven into the document without any alteration. - -Note that white space (line breaks (``'\n'``), tabs and spaces) have no effect on the input parsing. -They are completely preserved on output. - -The following example has three chunks: - -.. parsed-literal:: - - Some RST-format documentation that describes the following piece of the - program. - - @o myFile.py - @{ - import math - print( math.pi ) - @| math math.pi - @} - - Some more RST documentation. - -This starts with an anonymous chunk of -documentation. It includes a named output chunk which will write to ``myFile.py``. -It ends with an anonymous chunk of documentation. - -Inline Tags -~~~~~~~~~~~~ - -There are several tags that are replaced by content in the woven output. - -``@@`` - - The ``@@`` command creates a single ``@`` in the output file. - This is replaced in tangled as well as woven output. - -``@<``\ *name*\ ``@>`` - - The *name* references a named chunk. - When tangling, the referenced chunk replaces the reference command. - When weaving, a reference marker is used. For example, in RST, this can be - replaced with ``\`reference\`\_`` markup. - Note that the indentation prior to the ``@<`` tag is preserved - for the tangled chunk that replaces the tag. - - -``@(``\ *Python expression*\ ``@)`` - - The *Python expression* is evaluated and the result is tangled or - woven in place. A few global variables and modules are available. - These are described in `Expression Context`_. - - -There are three index creation tags that are replaced by content in the woven output. - - -``@f`` - - The ``@f`` command inserts a file cross reference. This - lists the name of each file created by an ``@o`` command, and all of the various - chunks that are concatenated to create this file. - -``@m`` - - The ``@m`` command inserts a named chunk ("macro") cross reference. This - lists the name of each chunk created by a ``@d`` command, and all of the various - chunks that are concatenated to create the complete chunk. - -``@u`` - - The ``@u`` command inserts a user identifier cross reference. - This index lists the name of each chunk created by an ``@d`` command or ``@|``, - and all of the various chunks that are concatenated to create the complete chunk. - - -Additional Features -~~~~~~~~~~~~~~~~~~~ - -**Sequence Numbers**. The named chunks (from both ``@o`` and ``@d`` commands) are assigned -unique sequence numbers to simplify cross references. - -**Case Sensitive**. Chunk names and file names are case sensitive. - -**Abbreviations**. Chunk names can be abbreviated. A partial name can have a trailing ellipsis (...), -this will be resolved to the full name. The most typical use for this -is shown in the following example: - -.. parsed-literal:: - - Some RST-format documentation. - - @o myFile.py - @{ - @ - print( math.pi,time.time() ) - @} - - Some notes on the packages used. - - @d imports... - @{ - import math,time - @| math time - @} - - Some more RST-format documentation. - -This example shows five chunks. - -1. An anonymous chunk of documentation. - -2. A named chunk that tangles the ``myFile.py`` output. It has - a reference to the ``imports of the various packages used`` chunk. - Note that the full name of the chunk is essentially a line of - documentation, traditionally done as a comment line in a non-literate - programming environment. - -3. An anonymous chunk of documentation. - -4. A named chunk with an abbreviated name. The ``imports...`` - matches the name ``imports of the various packages used``. - Set off after the ``@|`` separator is - the list of user-specified identifiers defined in this chunk. - -5. An anonymous chunk of documentation. - -Note that the first time a name appears (in a reference or definition), -it **must** be the full name. All subsequent uses can be elisions. -Also not that ambiguous elision is an annoying problem when you -first start creating a document. - -**Concatenation**. Named chunks are concatenated from their various pieces. -This allows a named chunk to be broken into several pieces, simplifying -the description. This is most often used when producing -fairly complex output files. - -.. parsed-literal:: - - An anonymous chunk with some RST documentation. - - @o myFile.py - @{ - import math,time - @} - - Some notes on the packages used. - - @o myFile.py - @{ - print math.pi,time.time() - @} - - Some more HTML documentation. - -This example shows five chunks. - -1. An anonymous chunk of documentation. - -2. A named chunk that tangles the ``myFile.py`` output. It has - the first part of the file. In the woven document - this is marked with ``"="``. - -3. An anonymous chunk of documentation. - -4. A named chunk that also tangles the ``myFile.py`` output. This - chunk's content is appended to the first chunk. In the woven document - this is marked with ``"+="``. - -5. An anonymous chunk of documentation. - -**Newline Preservation**. Newline characters are preserved on input. -Because of this the output may appear to have excessive newlines. -In all of the above examples, each -named chunk was defined with the following. - -.. parsed-literal:: - - @{ - import math,time - @} - -This puts a newline character before and after the import line. - -**Indentation Preservation**. One transformation is performed when tangling output. -The indentation of a chunk reference is applied to the entire chunk. This makes it -simpler to prepare source for languages (like Python) where indentation -is important. It also gives the author control over how the final -tangled output looks. - -Also, note that the ``myFile.py`` uses the ``@|`` command -to show that this chunk defines the identifier ``aFunction``. - -.. parsed-literal:: - - An anonymous chunk with some RST documentation. - - @o myFile.py - @{ - def aFunction( a, b ): - @ - @| aFunction @} - - Some notes on the algorithm used. - - @d body... - @{ - """doc string""" - return a + b - @} - - Some more RST documentation. - -The tangled output from this will look like the following. -All of the newline characters are preserved, and the reference to -*body of the aFunction* is indented to match the prevailing -indent where it was referenced. In the following example, -explicit line markers of ``~`` are provided to make the blank lines -more obvious. - -.. parsed-literal:: - - ~ - ~def aFunction( a, b ): - ~ - ~ """doc string""" - ~ return a + b - ~ - -Expression Context -~~~~~~~~~~~~~~~~~~~~ - -There are two possible implementations for evaluation of a Python -expression in the input. - -1. Create an ``ExpressionCommand``, and append this to the current ``Chunk``. - This will allow evaluation during weave processing and during tangle processing. This - makes the entire weave (or tangle) context available to the expression, including - completed cross reference information. - -2. Evaluate the expression during input parsing, and append the resulting text - as a ``TextCommand`` to the current ``Chunk``. This provides a common result - available to both weave and parse, but the only context available is the ``WebReader`` and - the incomplete ``Web``, built up to that point. - - -In this implementation, we adopt the latter approach, and evaluate expressions immediately. -A simple global context is created with the following variables defined. - -:os.path: - This is the standard ``os.path`` module. The complete ``os`` module is not - available. Just this one item. - -:datetime: - This is the standard ``datetime`` module. - -:platform: - This is the standard ``platform`` module. - -:__builtins__: - Most of the built-ins are available, too. Not all. - ``exec()``, ``eval()``, ``open()`` and ``__import__()`` aren't available. - -:theLocation: - A tuple with the file name, first line number and last line number - for the original expression's location. - -:theWebReader: - The ``WebReader`` instance doing the parsing. - -:theFile: - The ``.w`` file being processed. - -:thisApplication: - The name of the running **pyWeb** application. It may not be pyweb.py, - if some other script is being used. - -:__version__: - The version string in the **pyWeb** application. - - -Running **pyWeb** to Tangle and Weave --------------------------------------- - -Assuming that you have marked ``pyweb.py`` as executable, -you do the following. - -.. parsed-literal:: - - ./pyweb.py *file*... - -This will tangle the ``@o`` commands in each *file*. -It will also weave the output, and create *file*.txt. - -Command Line Options -~~~~~~~~~~~~~~~~~~~~~ - -Currently, the following command line options are accepted. - - -:-v: - Verbose logging. - -:-s: - Silent operation. - -:-c\ *x*: - Change the command character from ``@`` to ``*x*``. - -:-w\ *weaver*: - Choose a particular documentation weaver template. Currently the choices - are RST and HTML. - -:-xw: - Exclude weaving. This does tangling of source program files only. - -:-xt: - Exclude tangling. This does weaving of the document file only. - -:-p\ *command*: - Permit errors in the given list of commands. The most common - version is ``-pi`` to permit errors in locating an include file. - This is done in the following scenario: pass 1 uses ``-xw -pi`` to exclude - weaving and permit include-file errors; - the tangled program is run to create test results; pass 2 uses - ``-xt`` to exclude tangling and include the test results. - -Bootstrapping --------------- - -**pyWeb** is written using **pyWeb**. The distribution includes the original ``.w`` -files as well as a ``.py`` module. - -The bootstrap procedure is this. - -.. parsed-literal:: - - python pyweb.py pyweb.w - rst2html.py pyweb.rst pyweb.html - -The resulting ``pyweb.html`` file is the final documentation. - -Similarly, the tests are bootstrapped from ``.w`` files. - -.. parsed-literal:: - - cd test - python ../pyweb.py pyweb_test.w - PYTHONPATH=.. python test.py - rst2html.py pyweb_test.rst pyweb_test.html - -Dependencies -------------- - -**pyWeb** requires Python 3.3 or newer. - -Currently, input is not detabbed; Python users generally are discouraged -from using tab characters in their files. - -Note that we have two *possible* dependencies: - -- Jinja2 - -- pyYAML - -There are advantages and disadvantages to depending on other projects. -The disadvantage is a (very low, but still present) barrier to adoption. -The advantage is a big simplification. - - -Acknowledgements ----------------- - -This application is very directly based on (derived from?) work that - preceded this, particularly the following: - -- Ross N. Williams' *FunnelWeb* http://www.ross.net/funnelweb/ - -- Norman Ramsey's *noweb* http://www.eecs.harvard.edu/~nr/noweb/ - -- Preston Briggs' *nuweb* http://sourceforge.net/projects/nuweb/ - - Currently supported by Charles Martin and Marc W. Mengel - -Also, after using John Skaller's *interscript* http://interscript.sourceforge.net/ -for two large development efforts, I finally understood the feature set I really needed. - -Jason Fruit contributed to the previous version. - -.. pyweb/todo.w - -To Do -======= - -Big Deals ----------- - -Remove the filename as part of Web construction. A basename comes from the -initial ``.w`` file loaded by the ``WebReader``. The splitext used by weavers -and tanglers should be refactored into the WebReader. - -Fix the Action class hierarchy so that composite actions are simpler. - -- We shouldn't need to configure each action in a composite. - We should configure the composite, and - the configuration should be pushed down. A ``types.SimpleNamespace`` will do. - Or an ``argparse.Namespace``. - -- Rethink the ActionSequence. Is this really necessary? Wouldn't the Application - be simpler without it? - -Add ``@h`` "header goes here" command to allow outputting the **pyWeb** addons to -a LaTeX header, HTML header or RST header when weaving the documentation. -These are extra ``.. include::``, ``\\usepackage{fancyvrb}`` or maybe an HTML CSS reference -that come from **pyWeb** and need to be folded into otherwise boilerplate documents. - -Consider adding a configuration file to configure templates and comment conventions. -A slightly more flexible option is a separate JSON configuration file. - -- See the ``weave.py`` example. - This removes any weaver command-line option; its defined within the source. - Also, setting the command character can be done in this header. - -- Consider getting markup templates from a "header" section in the ``.w`` file. - Or a separate configuration file. - - To support reuse over multiple projects, a header could be included with ``@i``. - The downside is that we have a lot of variable = value syntax that makes it - more like a properties file than a ``.w`` syntax file. It seems needless to invent - a lot of new syntax just for configuration. - - -Smaller Deals --------------- - -1. Fix OutputChunk to also include the comment convention for the file - being tangled. This allows us to include source line number via the specified comment convention. - - We could include it in the input: ``@o`` *name* *comment*. - We could include it in a configuration file. - - We'll map the file extension to a pattern and use the location information from the - chunk. - - .. parsed-literal:: - - {'.py': "# {file}:{line} \n", - '.java': "// {file}:{line} \n", - '.cpp': "// {file}:{line} \n", - '.css': "/\* {file}:{line} \*/\n", - } - -#. Offer a basic XHTML template that uses CDATA sections instead of quoting. - Does require the standard quoting for the CDATA end tag. - -#. The ``createUsedBy()`` method can be done incrementally by - accumulating a list of forward references to chunks; as each - new chunk is added, any references to the chunk are removed from - the forward references list, and a call is made to the Web's - setUsage method. References backward to already existing chunks - are easily resolved with a simple lookup. - -#. Use a **Builder** pattern to plug an explicit ``WebBuilder`` instance - into the ``WebReader`` class to build the parse tree rather than the - complex-looking handler. This can be overridden to, - for example, do incremental building in one pass. - -#. Note that the overall ``Web`` is a lot like a ``NamedChunk``; this similarity - could be factored out. This will create a more proper **Composition** pattern implementation. - -#. We might want to decompose the ``impl.w`` file: it's huge. - -#. JSON-based logging configuration would be helpful. - - -.. pyweb/done.w - -Change Log -=========== - -Changes since 2.2 - -- Changed to Python 3.3 -- Fixed ``except``, ``raise`` and ``%``. - -- Removed ``doWrite()`` and simplified ``doOpen()`` and ``doClose()``. - -- Cleaned up RST output to be much nicer. - -- Change the baseline documents to be RST instead of HTML. - -- Removed the open ``eval()`` function. Provided a very slim set of globals. - ``os`` is really just ``os.path``. - Any ``os.getcwd()`` can be changed to ``os.path.realpath('.')``. - ``time`` was removed and replaced with ``datetime``. - Any ``time.asctime()`` must be ``datetime.datetime.now().ctime()``. - -- Resolved a small dispute between ``weaveReferenceTo()`` (wrong) and ``tangle()`` (right). - for NamedChunks. The issue was one of failure to understand the differences - between weaving -- where indentation is localized -- and tangling -- where indentation - must be tracked globally. Root cause was a huge problem in ``codeBlock()`` which didn't - really weave properly at all. - -- Fix the tokenizer and parsing. Stop using a complex tokenizer and use a simpler - iterator over the tokens with ``StopIteration`` exception handling. - -- Replace ``optparse`` with ``argparse``. - -- Get rid of the global ``logger`` variable. - -Changes since version 1.4 - -- Removed home-brewed logger. - -- Replaced ``getopt`` with ``optparse``. - -- Replaced LaTeX markup. - -- Corrected significant problems in cross-reference resolution. - -- Replaced all HTML and LaTeX-specific features with a much simpler template - engine which applies a template to a Chunk. The Templates are separate - configuration items. The big issue with templates are conditional processing - and the use of loops to handle multiple references in a transitive closure. - While it's nice to depend on Jinja2, it's also nice to be totally stand-alone. - Sigh. Choices include the no-logic ``string.Template`` in the standard library - or the ``Templite+`` Recipe 576663. - -- Looked at SCons API. Renamed "Operation" to "Action"; renamed "perform" to "__call__". - Consider having "__call__" which does logging, then call "execute". Weaver fits with SCons - Builder since we can see ``Weave( "someFile.w" )`` as sensible. Tangling is tougher - because the ``@o`` commands define the dependencies there. - -- Eliminated the EmitterFactory; replace this with simple injection of - the proper template configuration. - -- Removed the ``@O`` command; it was essentially a variant template for LaTeX. - -- Disentangled indentation and quoting in the codeBlock. - Indentation rules vary between Tangling and Weaving. - Quoting is unique to a woven codeBlock. Fix ``referenceTo()`` to write - indented without code quoting. - -- Offer a basic RST template. - Note that colorizing may be easier to handle with an RST template. - The weaving markup template degenerates - to ``.. parsed-literal::`` and indent. By doing this, - the RST output from *pyWeb* can be run through DocUtils ``rst2html.py`` - or perhaps *Sphix* to create final HTML. The hard part is the indent. - -- Tweaked (but didn't fix) ReferenceCommand tangle and all setIndent/clrIndent operations. - Only a ReferenceCommand actually cares about indentation. And that indentation - is totally based on the "context" plus the text in the Command immediate in front - of the ReferenceCommand. - - -.. pyweb/overview.w - -Architecture and Design Overview -================================ - -This application breaks the overall problem into the following sub-problems. - -1. Representation of the Web as Chunks and Commands - -2. Reading and parsing the input. - -3. Weaving a document file. - -4. Tangling the desired program source files. - - -Representation ---------------- - -The basic "parse tree" is actually quite flat. The source document can be -decomposed into a simple sequence of Chunks. Each Chunk is a simple sequence -of Commands. - -Chunks and commands cannot be nested, leading to delightful simplification. - -The overall parse "tree" is contained in the overall Web. The web -includes the sequence of Chunks as well as an index for the Named chunks. - -Note that a named chunk may be created through a number of @d commands. -This means that -Each named chunk may be a sequence of Chunks with a common name. - -Each chunk is composed of a sequence of instances of Command. -Because of this uniform composition, the several operations (particularly -weave and tangle) can be -delegated to each Chunk, and in turn, delegated to each Command that -composes a Chunk. - - -Reading and Parsing --------------------- - -A solution to the reading and parsing problem depends on a convenient -tool for breaking up the input stream and a representation for the chunks of input. -Input decomposition is done with the Python Splitter pattern. - -The Splitter pattern is widely used in text processing, and has a long legacy -in a variety of languages and libraries. A Splitter decomposes a string into -a sequence of strings using the split pattern. There are many variant implementations. -One variant locates only a single occurence (usually the left-most); this is -commonly implemented as a Find or Search string function. Another variant locates all -occurrences of a specific string or character, and discards the matching string or -character. - - -The variation on Splitter that we use in this application -creates each element in the resulting sequence as either (1) an instance of the -split regular expression or (2) the text between split patterns. By preserving -the actual split text, we can define our splitting pattern with the regular -expression ``'@.'``. This will split on any ``@`` followed by a single character. -We can then examine the instances of the split RE to locate pyWeb commands. - -We could be a tad more specific and use the following as a split pattern: -``'@[doOifmu|<>(){}[\]]'``. This would silently ignore unknown commands, -merging them in with the surrounding text. This would leave the ``'@@'`` sequences -completely alone, allowing us to replace ``'@@'`` with ``'@'`` in -every text chunk. - - -Weaving ---------- - -The weaving operation depends on the target document markup language. -There are several approaches to this problem. One is to use a markup language -unique to **pyWeb**, and emit markup in the desired target language. -Another is to use a standard markup language and use converters to transform -the standard markup to the desired target markup. The problem with the second -method is specifying the markup for actual source code elements in the -document. These must be emitted in the proper markup language. - -Since the application must transform input into a specific markup language, -we opt using the Strategy pattern to encapsulate markup language details. -Each alternative markup strategy is then a subclass of **Weaver**. This -simplifies adding additional markup languages without inventing a -markup language unique to **pyWeb**. -The author uses their preferred markup, and their preferred -toolset to convert to other output languages. - - -Tangling ----------- - -The tangling operation produces output files. In earlier tools, -some care was taken to understand the source code context for tangling, and -provide a correct indentation. This required a command-line parameter -to turn off indentation for languages like Fortran, where identation -is not used. In **pyWeb**, the indent of -the actual ``@<`` command is used to set the indent of the -material that follows. If all ``@<`` commands are presented at the -left margin, no indentation will be done. This is helpful simplification, -particularly for users of Python, where indentation is significant. - -The standard **Emitter** class handles this basic indentation. A subclass can be -created, if necessary, to handle more elaborate indentation rules. - -.. pyweb/impl.w - -Implementation -============== - -The implementation is contained in a file that both defines -the base classes and provides an overall ``main()`` function. The ``main()`` -function uses these base classes to weave and tangle the output files. - -The broad outline of the presentation is as follows: - -- `Base Class Definitions`_. - -- `Emitters`_ write various kinds of files. - -- `Chunks`_ are pieces of the source document, built into a Web. - -- `Commands`_ are the items within a ``Chunk``. - -- `The Web Class`_ includes the web and the parser which produces a web. - - - `The WebReader class`_ which parses the Web structure. - - - `The Tokenizer class`_ which tokenizes the raw input. - - - `Error class`_ defines an application-specific Error. - -Additionally there are some relatively minor classes and other parts -of a finished application. - -- `Reference Strategy`_ defines ways to manage cross-references among chunks. - -- `Action class hierarchy`_ defines things this program does. - -- `pyWeb Module File`_, including - ``Application`` class and ``main()`` function. - - -Base Class Definitions ----------------------- - -There are three major class hierarchies that compose the base of this application. These are -families of related classes that express the basic relationships among entities. - -**Emitters**. An ``Emitter`` creates an output file, either tangled code or some kind of markup from -the chunks that make up the source file. Two major subclasses are the ``Weaver``, which -has a focus on markup output, and ``Tangler`` which has a focus on pure source output. - - It's possible to have further specialization of the weavers for HTML or LaTeX. The issue is - generating proper markup to surround the code and include cross-references among code - blocks. - -**Chunks**. A ``Chunk`` is a collection of ``Command`` instances. This can be -either an anonymous chunk that will be sent directly to the output, -or one the classes of named chunks delimited by the -major ``@d`` or ``@o`` commands. - -**Commands**. A ``Command`` contains user input and creates output. -This can be a block of text from the input file, -one of the various kinds of cross reference commands (``@f``, ``@m``, or ``@u``) -or a reference to a chunk (via the ``@<``\ *name*\ ``@>`` sequence.) - -The other class hierarchies are focused on the application functionality, not the -essential data model. - - -.. _`1`: -.. rubric:: Base Class Definitions (1) = -.. parsed-literal:: - :class: code - - - |srarr|\ Error class - defines the errors raised (`92`_) - |srarr|\ Command class hierarchy - used to describe individual commands (`74`_) - |srarr|\ Chunk class hierarchy - used to describe input chunks (`51`_) - |srarr|\ Web class - describes the overall "web" of chunks (`93`_) - |srarr|\ Emitter class hierarchy - used to control output files (`2`_) - |srarr|\ Reference class hierarchy - references to a chunk (`89`_), |srarr|\ (`90`_), |srarr|\ (`91`_) - |srarr|\ Tokenizer class - breaks input into tokens (`111`_) - |srarr|\ WebReader class - parses the input file, building the Web structure (`112`_) - |srarr|\ Action class hierarchy - used to describe basic actions of the application (`131`_) - -.. - - .. class:: small - - |loz| *Base Class Definitions (1)*. Used by: pyweb.py (`148`_) - - -Emitters ---------- - -An ``Emitter`` instance is resposible for control of an output file format. -This includes the necessary file naming, opening, writing and closing operations. -It also includes providing the correct markup for the file type. - -There are several subclasses of the ``Emitter`` superclass, specialized for various file -formats. - - -.. _`2`: -.. rubric:: Emitter class hierarchy - used to control output files (2) = -.. parsed-literal:: - :class: code - - - |srarr|\ Emitter superclass (`3`_) - |srarr|\ Weaver subclass of Emitter to create documentation (`12`_) - |srarr|\ RST subclass of Weaver (`22`_) - |srarr|\ LaTeX subclass of Weaver (`23`_) - |srarr|\ HTML subclass of Weaver (`31`_), |srarr|\ (`32`_) - |srarr|\ Tangler subclass of Emitter to create source files with no markup (`43`_) - |srarr|\ TanglerMake subclass which is make-sensitive (`48`_) - -.. - - .. class:: small - - |loz| *Emitter class hierarchy - used to control output files (2)*. Used by: Base Class Definitions (`1`_); pyweb.py (`148`_) - - -An ``Emitter`` instance is created to contain the various details of -writing an output file. Emitters are created as follows: - -- A ``Web`` object will create a ``Weaver`` to **weave** the final document. - -- A ``Web`` object will create a ``Tangler`` to **tangle** each file. - -Since each ``Emitter`` instance is responsible for the details of one file -type, different subclasses of ``Emitter`` are used when tangling source code files -(``Tangler``) and -weaving files that include source code plus markup (``Weaver``). - -Further specialization is required when weaving HTML or LaTeX. Generally, this is -a matter of providing three things: - -- Boilerplate text to replace various pyWeb constructs, - -- Escape rules to make source code amenable to the markup language, - -- A header to provide overall includes or other setup. - - -An additional part of the escape rules can include using a syntax coloring -toolset instead of simply applying escapes. - -In the case of **tangle**, the following algorithm is used: - - Visit each each output ``Chunk`` (``@o``), doing the following: - - 1. Open the ``Tangler`` instance using the target file name. - - 2. Visit each ``Chunk`` directed to the file, calling the chunk's ``tangle()`` method. - - 1. Call the Tangler's ``docBegin()`` method. This sets the Tangler's indents. - - 2. Visit each ``Command``, call the command's ``tangle()`` method. - For the text of the chunk, the - text is written to the tangler using the ``codeBlock()`` method. For - references to other chunks, the referenced chunk is tangled using the - referenced chunk's ``tangler()`` method. - - 3. Call the Tangler's ``docEnd()`` method. This clears the Tangler's indents. - - -In the case of **weave**, the following algorithm is used: - - 1. Open the ``Weaver`` instance using the source file name. This name is transformed - by the weaver to an output file name appropriate to the language. - - 2. Visit each each sequential ``Chunk`` (anonymous, ``@d`` or ``@o``), doing the following: - - 1. Visit each ``Chunk``, calling the Chunk's ``weave()`` method. - - 1. Call the Weaver's ``docBegin()``, ``fileBegin()`` or ``codeBegin()`` method, - depending on the subclass of Chunk. For - ``fileBegin()`` and ``codeBegin()``, this writes the header for - a code chunk in the weaver's markup language. - - 2. Visit each ``Command``, call the Command's ``weave()`` method. - For ordinary text, the - text is written to the Weaver using the ``codeBlock()`` method. For - references to other chunks, the referenced chunk is woven using - the Weaver's ``referenceTo()`` method. - - 3. Call the Weaver's ``docEnd()``, ``fileEnd()`` or ``codeEnd()`` method. - For ``fileEnd()`` or ``codeEnd()``, this writes a trailer for - a code chunk in the Weaver's markup language. - - -Emitter Superclass -~~~~~~~~~~~~~~~~~~ - -The ``Emitter`` class is not a concrete class; it is never instantiated. It -contains common features factored out of the ``Weaver`` and ``Tangler`` subclasses. - -Inheriting from the Emitter class generally requires overriding one or more -of the core methods: ``doOpen()``, and ``doClose()``. -A subclass of Tangler, might override the code writing methods: -``quote()``, ``codeBlock()`` or ``codeFinish()``. - -The ``Emitter`` class defines the basic -framework used to create and write to an output file. -This class follows the **Template** design pattern. This design pattern -directs us to factor the basic open(), close() and write() methods into two step algorithms. - -.. parsed-literal:: - - def open( self ): - *common preparation* - self.doOpen() *#overridden by subclasses* - return self - -The *common preparation* section is generally internal -housekeeping. The ``doOpen()`` method would be overridden by subclasses to change the -basic behavior. - - **TODO** Adding an ``__enter__()`` and ``__exit__()`` would make an Emitter - into a proper Context Manager which could be used with a ``with`` statement. - -The class has the following attributes: - -:fileName: - the name of the current open file created by the - open method - -:theFile: - the current open file created by the - open method - -:linesWritten: - the total number of ``'\n'`` characters written to the file - -:totalFiles: - count of total number of files - -:totalLines: - count of total number of lines - -Additionally, an emitter tracks an indentation context used by -The ``codeBlock()`` method to indent each line written. - -:context: - the indentation context stack, updated by ``setIndent()``, - ``clrIndent()`` and ``resetIndent()`` methods. - -:lastIndent: - the last indent used after writing a line of source code - -:fragment: - the last line written was a fragment and needs a ``'\n'``. - -:code_indent: - Any initial code indent. RST weavers needs additional code indentation. - Other weavers don't care. Tanglers must have this set to zero. - - -.. _`3`: -.. rubric:: Emitter superclass (3) = -.. parsed-literal:: - :class: code - - - class Emitter: - """Emit an output file; handling indentation context.""" - code\_indent= 0 # for a Tangler - def \_\_init\_\_( self ): - self.fileName= "" - self.theFile= None - self.linesWritten= 0 - self.totalFiles= 0 - self.totalLines= 0 - self.fragment= False - self.logger= logging.getLogger( self.\_\_class\_\_.\_\_qualname\_\_ ) - self.log\_indent= logging.getLogger( "indent." + self.\_\_class\_\_.\_\_qualname\_\_ ) - self.resetIndent( self.code\_indent ) # Create context and initial lastIndent values - def \_\_str\_\_( self ): - return self.\_\_class\_\_.\_\_name\_\_ - |srarr|\ Emitter core open, close and write (`4`_) - |srarr|\ Emitter write a block of code (`7`_), |srarr|\ (`8`_), |srarr|\ (`9`_) - |srarr|\ Emitter indent control: set, clear and reset (`10`_) - - -.. - - .. class:: small - - |loz| *Emitter superclass (3)*. Used by: Emitter class hierarchy - used to control output files (`2`_); Base Class Definitions (`1`_); pyweb.py (`148`_) - - -The core ``open()`` method tracks the open files. -A subclass overrides a ``doOpen()`` method to name the output file, and -then actually open the file. The Weaver will create an output file with -a name that's based on the overall project. The Tangler will open the given file -name. - -The ``close()`` method closes the file. As with ``open()``, a -``doClose()`` method actually closes the file. This allows subclasses -to do overrides on the actual file processing. - -The ``write()`` method is the lowest-level, unadorned write. -This does some additional counting as well as writing the -characters to the file. - - -.. _`4`: -.. rubric:: Emitter core open, close and write (4) = -.. parsed-literal:: - :class: code - - - def open( self, aFile ): - """Open a file.""" - self.fileName= aFile - self.linesWritten= 0 - self.doOpen( aFile ) - return self - |srarr|\ Emitter doOpen, to be overridden by subclasses (`5`_) - def close( self ): - self.codeFinish() # Trailing newline for tangler only. - self.totalFiles += 1 - self.totalLines += self.linesWritten - self.doClose() - |srarr|\ Emitter doClose, to be overridden by subclasses (`6`_) - def write( self, text ): - if text is None: return - self.linesWritten += text.count('\\n') - self.theFile.write( text ) - - -.. - - .. class:: small - - |loz| *Emitter core open, close and write (4)*. Used by: Emitter superclass (`3`_); Emitter class hierarchy - used to control output files (`2`_); Base Class Definitions (`1`_); pyweb.py (`148`_) - - -The ``doOpen()``, and ``doClose()`` -methods are overridden by the various subclasses to -perform the unique operation for the subclass. - - -.. _`5`: -.. rubric:: Emitter doOpen, to be overridden by subclasses (5) = -.. parsed-literal:: - :class: code - - - def doOpen( self, aFile ): - self.logger.debug( "creating {!r}".format(self.fileName) ) - - -.. - - .. class:: small - - |loz| *Emitter doOpen, to be overridden by subclasses (5)*. Used by: Emitter core open, close and write (`4`_); Emitter superclass (`3`_); Emitter class hierarchy - used to control output files (`2`_); Base Class Definitions (`1`_); pyweb.py (`148`_) - - - -.. _`6`: -.. rubric:: Emitter doClose, to be overridden by subclasses (6) = -.. parsed-literal:: - :class: code - - - def doClose( self ): - self.logger.debug( "wrote {:d} lines to {:s}".format( - self.linesWritten, self.fileName) ) - - -.. - - .. class:: small - - |loz| *Emitter doClose, to be overridden by subclasses (6)*. Used by: Emitter core open, close and write (`4`_); Emitter superclass (`3`_); Emitter class hierarchy - used to control output files (`2`_); Base Class Definitions (`1`_); pyweb.py (`148`_) - - -The ``codeBlock()`` method writes several lines of code. It calls -the ``quote()`` method to quote each line of code after doing the correct indentation. -Often, the last line of code is incomplete, so it is left unterminated. -This last line of code also sets the indentation for any -additional code to be tangled into this section. - -.. important:: - - Tab characters confuse the indent algorithm. Tabs are - not expanded to spaces in this application. They should be expanded - prior to creating a ``.w`` file. - -The algorithm is as follows: - -1. Save the topmost value of the context stack as the current indent. - -2. Split the block of text on ``'\n'`` boundaries. - - There are two cases. - - - One line only, no newline. Write this with the saved lastIndent. - The lastIndent is reset to zero since we've only written a fragmentary line. - - - Multiple lines. - - 1. Write the first line with saved lastIndent. - - 2. For each remaining line (except the last), write with the indented text, - ending with a newline. - - #. The string ``split()`` method will put a trailing - zero-length element in the list if the original block ended with a - newline. We drop this zero length piece to prevent writing a useless fragment - of indent-only after the final ``'\n'``. - - #. If the last line has content, write with the indented text, - but do not write a trailing ``'\n'``. Set lastIndent to zero because - the next ``codeBlock()`` will continue this fragmentary line. - - If the last line has no content, write nothing. - Save the length of the last line as the most recent indent for any ``@<``\ *name*\ ``@>`` - reference to. - -This feels a bit too complex. Note that some of this is legacy design from -a previous tokenizer which produced large blocks of text with multiple -lines. - - -.. _`7`: -.. rubric:: Emitter write a block of code (7) = -.. parsed-literal:: - :class: code - - - def codeBlock( self, text ): - """Indented write of a block of code. We buffer - The spaces from the last line to act as the indent for the next line. - """ - indent= self.context[-1] - lines= text.split( '\\n' ) - if len(lines) == 1: # Fragment with no newline. - self.write('{:s}{:s}'.format(self.lastIndent\*' ', lines[0]) ) - self.lastIndent= 0 - self.fragment= True - else: - first, rest= lines[:1], lines[1:] - self.write('{:s}{:s}\\n'.format(self.lastIndent\*' ', first[0]) ) - for l in rest[:-1]: - self.write( '{:s}{:s}\\n'.format(indent\*' ', l) ) - if rest[-1]: - self.write( '{:s}{:s}'.format(indent\*' ', rest[-1]) ) - self.lastIndent= 0 - self.fragment= True - else: - # Buffer a next indent - self.lastIndent= len(rest[-1]) + indent - self.fragment= False - - -.. - - .. class:: small - - |loz| *Emitter write a block of code (7)*. Used by: Emitter superclass (`3`_); Emitter class hierarchy - used to control output files (`2`_); Base Class Definitions (`1`_); pyweb.py (`148`_) - - -The ``quote()`` method quotes a single line of source code. -This is used by Weaver subclasses to transform source into -a form acceptable by the final weave file format. - -In the case of an HTML weaver, the HTML reserved characters -- -``<``, ``>``, ``&``, and ``"`` -- must be replaced in the output -of code with ``<``, ``>``, ``&``, and ``"``. -However, since the author's original document sections contain -HTML these will not be altered. - - -.. _`8`: -.. rubric:: Emitter write a block of code (8) += -.. parsed-literal:: - :class: code - - - quoted\_chars = [ - # Must be empty for tangling. - ] - - def quote( self, aLine ): - """Each individual line of code; often overridden by weavers to quote the code.""" - clean= aLine - for from\_, to\_ in self.quoted\_chars: - clean= clean.replace( from\_, to\_ ) - return clean - - -.. - - .. class:: small - - |loz| *Emitter write a block of code (8)*. Used by: Emitter superclass (`3`_); Emitter class hierarchy - used to control output files (`2`_); Base Class Definitions (`1`_); pyweb.py (`148`_) - - -The ``codeFinish()`` method handles a trailing fragmentary line when tangling. - - -.. _`9`: -.. rubric:: Emitter write a block of code (9) += -.. parsed-literal:: - :class: code - - - def codeFinish( self ): - if self.fragment: - self.write('\\n') - - -.. - - .. class:: small - - |loz| *Emitter write a block of code (9)*. Used by: Emitter superclass (`3`_); Emitter class hierarchy - used to control output files (`2`_); Base Class Definitions (`1`_); pyweb.py (`148`_) - - -These three methods are used when to be sure that the included text is indented correctly with respect to the -surrounding text. - -The ``setIndent()`` method pushes the last indent on the context stack. - -When tangling, a "previous" value is set from the indent left over from the -previous command. This allows ``@<``\ *name*\ ``@>`` references to be indented -properly. A tangle must track all nested ``@d`` contexts to create a proper -global indent. - -Weaving, however, is entirely localized to the block of code. There's no -real context tracking. Just "lastIndent" from the previous command's ``codeBlock()``. - -The ``clrIndent()`` method discards the most recent indent from the context stack. -This is used when finished -tangling a source chunk. This restores the indent to the prevailing indent. - -The ``resetIndent()`` method removes all indent context information and resets the indent -to a default. - -Weaving may use an initial offset. -It's an additional indent for woven code; not used for tangled code. In particular, RST -requires this. ``resetIndent()`` uses this initial offset for weaving. - - -.. _`10`: -.. rubric:: Emitter indent control: set, clear and reset (10) = -.. parsed-literal:: - :class: code - - - def setIndent( self, previous ): - self.lastIndent= self.context[-1]+previous - self.context.append( self.lastIndent ) - self.log\_indent.debug( "setIndent {!s}: {!r}".format(previous, self.context) ) - def clrIndent( self ): - if len(self.context) > 1: - self.context.pop() - self.lastIndent= self.context[-1] - self.log\_indent.debug( "clrIndent {!r}".format(self.context) ) - def resetIndent( self, indent=0 ): - self.lastIndent= indent - self.context= [self.lastIndent] - self.log\_indent.debug( "resetIndent {!s}: {!r}".format(indent, self.context) ) - - -.. - - .. class:: small - - |loz| *Emitter indent control: set, clear and reset (10)*. Used by: Emitter superclass (`3`_); Emitter class hierarchy - used to control output files (`2`_); Base Class Definitions (`1`_); pyweb.py (`148`_) - - -Weaver subclass of Emitter -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -A Weaver is an Emitter that produces the final user-focused document. -This will include the source document with the code blocks surrounded by -markup to present that code properly. In effect, the pyWeb ``@`` commands -are replaced by markup. - -The Weaver class uses a simple set of templates to product RST markup as the default -Subclasses can introduce other templates to produce HTML or LaTeX output. - -Most weaver languages don't rely on special indentation rules. -The woven code samples usually start right on the left margin of -the source document. However, the RST markup language does rely -on extra indentation of code blocks. For that reason, the weavers -have an additional indent for code blocks. This is generally -set to zero, except when generating RST where 4 spaces is good. - -The ``Weaver`` subclass defines an ``Emitter`` used to **weave** the final -documentation. This involves decorating source code to make it -displayable. It also involves creating references and cross -references among the various chunks. - -The ``Weaver`` class adds several methods to the basic ``Emitter`` methods. These -additional methods are also included that are used exclusively when weaving, never when tangling. - -This class hierarch depends heavily on the ``string`` module. - -Class level variables include the following - -:extension: - The filename extension used by this weaver. - - -.. _`11`: -.. rubric:: Imports (11) = -.. parsed-literal:: - :class: code - - import string - - -.. - - .. class:: small - - |loz| *Imports (11)*. Used by: pyweb.py (`148`_) - - - -.. _`12`: -.. rubric:: Weaver subclass of Emitter to create documentation (12) = -.. parsed-literal:: - :class: code - - - class Weaver( Emitter ): - """Format various types of XRef's and code blocks when weaving. - RST format. - Requires \`\`.. include:: \`\` - and \`\`.. include:: \`\` - """ - extension= ".rst" - code\_indent= 4 - |srarr|\ Weaver doOpen, doClose and setIndent overrides (`13`_) - - # Template Expansions. - - |srarr|\ Weaver quoted characters (`14`_) - |srarr|\ Weaver document chunk begin-end (`15`_) - |srarr|\ Weaver reference summary, used by code chunk and file chunk (`16`_) - |srarr|\ Weaver code chunk begin-end (`17`_) - |srarr|\ Weaver file chunk begin-end (`18`_) - |srarr|\ Weaver reference command output (`19`_) - |srarr|\ Weaver cross reference output methods (`20`_), |srarr|\ (`21`_) - - -.. - - .. class:: small - - |loz| *Weaver subclass of Emitter to create documentation (12)*. Used by: Emitter class hierarchy - used to control output files (`2`_); Base Class Definitions (`1`_); pyweb.py (`148`_) - - -The ``doOpen()`` method opens the file for writing. For weavers, the file extension -is specified part of the target markup language being created. - -The `doClose()`` method extends the ``Emitter`` class ``close()`` method by closing the -actual file created by the open() method. - -The ``setIndent()`` reflects the fact that we're not tracking global indents, merely -the local indentation required to weave a code chunk. The "indent" can vary because -we're not always starting a fresh line with ``weaveReferenceTo()``. - - -.. _`13`: -.. rubric:: Weaver doOpen, doClose and setIndent overrides (13) = -.. parsed-literal:: - :class: code - - - def doOpen( self, aFile ): - src, \_ = os.path.splitext( aFile ) - self.fileName= src + self.extension - self.theFile= open( self.fileName, "w" ) - self.logger.info( "Weaving {!r}".format(self.fileName) ) - self.resetIndent( self.code\_indent ) - def doClose( self ): - self.theFile.close() - self.logger.info( "Wrote {:d} lines to {!r}".format( - self.linesWritten, self.fileName) ) - def setIndent( self, previous=None ): - """previous not used.""" - self.context.append( self.context[-1] ) - self.log\_indent.debug( "setIndent {!s}: {!r}".format(self.lastIndent, self.context) ) - def codeFinish( self ): - pass # Not needed when weaving - - -.. - - .. class:: small - - |loz| *Weaver doOpen, doClose and setIndent overrides (13)*. Used by: Weaver subclass of Emitter to create documentation (`12`_); Emitter class hierarchy - used to control output files (`2`_); Base Class Definitions (`1`_); pyweb.py (`148`_) - - -This is an overly simplistic list. We use the ``parsed-literal`` -directive because we're including links and what-not in the code. -We have to quote certain inline markup -- but only when the -characters are paired in a way that might confuse RST. - - -.. _`14`: -.. rubric:: Weaver quoted characters (14) = -.. parsed-literal:: - :class: code - - - quoted\_chars = [ - # prevent some RST markup from being recognized - ('\\\\',r'\\\\'), # Must be first. - ('\`',r'\\\`'), - ('\_',r'\\\_'), - ('\*',r'\\\*'), - ('\|',r'\\\|'), - ] - -.. - - .. class:: small - - |loz| *Weaver quoted characters (14)*. Used by: Weaver subclass of Emitter to create documentation (`12`_); Emitter class hierarchy - used to control output files (`2`_); Base Class Definitions (`1`_); pyweb.py (`148`_) - - -The remaining methods apply a chunk to a template. - -The ``docBegin()`` and ``docEnd()`` -methods are used when weaving a document text chunk. -Typically, nothing is done before emitting these kinds of chunks. -However, putting a ``.. line line number`` RST comment is an example -of possible additional processing. - - - -.. _`15`: -.. rubric:: Weaver document chunk begin-end (15) = -.. parsed-literal:: - :class: code - - - def docBegin( self, aChunk ): - pass - def docEnd( self, aChunk ): - pass - - -.. - - .. class:: small - - |loz| *Weaver document chunk begin-end (15)*. Used by: Weaver subclass of Emitter to create documentation (`12`_); Emitter class hierarchy - used to control output files (`2`_); Base Class Definitions (`1`_); pyweb.py (`148`_) - - -Each code chunk includes the places where the chunk is referenced. - - -.. _`16`: -.. rubric:: Weaver reference summary, used by code chunk and file chunk (16) = -.. parsed-literal:: - :class: code - - - ref\_template = string.Template( "${refList}" ) - ref\_item\_template = string.Template( "$fullName (\`${seq}\`\_)" ) - def references( self, aChunk ): - if aChunk.references\_list: - refList= [ - self.ref\_item\_template.substitute( seq=s, fullName=n ) - for n,s in aChunk.references\_list ] - return self.ref\_template.substitute( refList="; ".join( refList ) ) # RST Separator - return "" - - -.. - - .. class:: small - - |loz| *Weaver reference summary, used by code chunk and file chunk (16)*. Used by: Weaver subclass of Emitter to create documentation (`12`_); Emitter class hierarchy - used to control output files (`2`_); Base Class Definitions (`1`_); pyweb.py (`148`_) - - - -The ``codeBegin()`` method emits the necessary material prior to -a chunk of source code, defined with the ``@d`` command. - -The ``codeEnd()`` method emits the necessary material subsequent to -a chunk of source code, defined with the ``@d`` command. -Links or cross references to chunks that -refer to this chunk can be emitted. - - - -.. _`17`: -.. rubric:: Weaver code chunk begin-end (17) = -.. parsed-literal:: - :class: code - - - cb\_template = string.Template( "\\n.. \_\`${seq}\`:\\n.. rubric:: ${fullName} (${seq}) ${concat}\\n.. parsed-literal::\\n :class: code\\n\\n" ) - - def codeBegin( self, aChunk ): - txt = self.cb\_template.substitute( - seq= aChunk.seq, - lineNumber= aChunk.lineNumber, - fullName= aChunk.fullName, - concat= "=" if aChunk.initial else "+=", # RST Separator - ) - self.write( txt ) - - ce\_template = string.Template( "\\n..\\n\\n .. class:: small\\n\\n \|loz\| \*${fullName} (${seq})\*. Used by: ${references}\\n" ) - - def codeEnd( self, aChunk ): - txt = self.ce\_template.substitute( - seq= aChunk.seq, - lineNumber= aChunk.lineNumber, - fullName= aChunk.fullName, - references= self.references( aChunk ), - ) - self.write(txt) - - -.. - - .. class:: small - - |loz| *Weaver code chunk begin-end (17)*. Used by: Weaver subclass of Emitter to create documentation (`12`_); Emitter class hierarchy - used to control output files (`2`_); Base Class Definitions (`1`_); pyweb.py (`148`_) - - -The ``fileBegin()`` method emits the necessary material prior to -a chunk of source code, defined with the ``@o`` command. -A subclass would override this to provide specific text -for the intended file type. - -The ``fileEnd()`` method emits the necessary material subsequent to -a chunk of source code, defined with the ``@o`` command. - -There shouldn't be a list of references to a file. We assert that this -list is always empty. - - -.. _`18`: -.. rubric:: Weaver file chunk begin-end (18) = -.. parsed-literal:: - :class: code - - - fb\_template = string.Template( "\\n.. \_\`${seq}\`:\\n.. rubric:: ${fullName} (${seq}) ${concat}\\n.. parsed-literal::\\n :class: code\\n\\n" ) - - def fileBegin( self, aChunk ): - txt= self.fb\_template.substitute( - seq= aChunk.seq, - lineNumber= aChunk.lineNumber, - fullName= aChunk.fullName, - concat= "=" if aChunk.initial else "+=", # RST Separator - ) - self.write( txt ) - - fe\_template= string.Template( "\\n..\\n\\n .. class:: small\\n\\n \|loz\| \*${fullName} (${seq})\*.\\n" ) - - def fileEnd( self, aChunk ): - assert len(self.references( aChunk )) == 0 - txt= self.fe\_template.substitute( - seq= aChunk.seq, - lineNumber= aChunk.lineNumber, - fullName= aChunk.fullName, - references= [] ) - self.write( txt ) - - -.. - - .. class:: small - - |loz| *Weaver file chunk begin-end (18)*. Used by: Weaver subclass of Emitter to create documentation (`12`_); Emitter class hierarchy - used to control output files (`2`_); Base Class Definitions (`1`_); pyweb.py (`148`_) - - -The ``referenceTo()`` method emits a reference to -a chunk of source code. There reference is made with a -``@<``\ *name*\ ``@>`` reference within a ``@d`` or ``@o`` chunk. -The references are defined with the ``@d`` or ``@o`` commands. -A subclass would override this to provide specific text -for the intended file type. - - - -.. _`19`: -.. rubric:: Weaver reference command output (19) = -.. parsed-literal:: - :class: code - - - refto\_name\_template= string.Template(r"\|srarr\|\\ ${fullName} (\`${seq}\`\_)") - refto\_seq\_template= string.Template("\|srarr\|\\ (\`${seq}\`\_)") - - def referenceTo( self, aName, seq ): - """Weave a reference to a chunk. - Provide name to get a full reference. - name=None to get a short reference.""" - if aName: - return self.refto\_name\_template.substitute( fullName= aName, seq= seq ) - else: - return self.refto\_seq\_template.substitute( seq= seq ) - - -.. - - .. class:: small - - |loz| *Weaver reference command output (19)*. Used by: Weaver subclass of Emitter to create documentation (`12`_); Emitter class hierarchy - used to control output files (`2`_); Base Class Definitions (`1`_); pyweb.py (`148`_) - - -The ``xrefHead()`` method puts decoration in front of cross-reference -output. A subclass may override this to change the look of the final -woven document. - -The ``xrefFoot()`` method puts decoration after cross-reference -output. A subclass may override this to change the look of the final -woven document. - -The ``xrefLine()`` method is used for both -file and chunk ("macro") cross-references to show a name (either file name -or chunk name) and a list of chunks that reference the file or chunk. - -The ``xrefDefLine()`` method is used for the user identifier cross-reference. -This shows a name and a list of chunks that -reference or define the name. One of the chunks is identified as the -defining chunk, all others are referencing chunks. - -An ``xrefEmpty()`` is used in the rare case of no user identifiers present. - -The default behavior simply writes the Python data structure used -to represent cross reference information. A subclass may override this -to change the look of the final woven document. - - -.. _`20`: -.. rubric:: Weaver cross reference output methods (20) = -.. parsed-literal:: - :class: code - - - xref\_head\_template = string.Template( "\\n" ) - xref\_foot\_template = string.Template( "\\n" ) - xref\_item\_template = string.Template( ":${fullName}:\\n ${refList}\\n" ) - xref\_empty\_template = string.Template( "(None)\\n" ) - - def xrefHead( self ): - txt = self.xref\_head\_template.substitute() - self.write( txt ) - - def xrefFoot( self ): - txt = self.xref\_foot\_template.substitute() - self.write( txt ) - - def xrefLine( self, name, refList ): - refList= [ self.referenceTo( None, r ) for r in refList ] - txt= self.xref\_item\_template.substitute( fullName= name, refList = " ".join(refList) ) # RST Separator - self.write( txt ) - - def xrefEmpty( self ): - self.write( self.xref\_empty\_template.substitute() ) - -.. - - .. class:: small - - |loz| *Weaver cross reference output methods (20)*. Used by: Weaver subclass of Emitter to create documentation (`12`_); Emitter class hierarchy - used to control output files (`2`_); Base Class Definitions (`1`_); pyweb.py (`148`_) - - -Cross-reference definition line - - -.. _`21`: -.. rubric:: Weaver cross reference output methods (21) += -.. parsed-literal:: - :class: code - - - name\_def\_template = string.Template( '[\`${seq}\`\_]' ) - name\_ref\_template = string.Template( '\`${seq}\`\_' ) - - def xrefDefLine( self, name, defn, refList ): - templates = { defn: self.name\_def\_template } - refTxt= [ templates.get(r,self.name\_ref\_template).substitute( seq= r ) - for r in sorted( refList + [defn] ) - ] - # Generic space separator - txt= self.xref\_item\_template.substitute( fullName= name, refList = " ".join(refTxt) ) - self.write( txt ) - - -.. - - .. class:: small - - |loz| *Weaver cross reference output methods (21)*. Used by: Weaver subclass of Emitter to create documentation (`12`_); Emitter class hierarchy - used to control output files (`2`_); Base Class Definitions (`1`_); pyweb.py (`148`_) - - -RST subclass of Weaver -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -A degenerate case. - - -.. _`22`: -.. rubric:: RST subclass of Weaver (22) = -.. parsed-literal:: - :class: code - - - class RST(Weaver): - pass - -.. - - .. class:: small - - |loz| *RST subclass of Weaver (22)*. Used by: Emitter class hierarchy - used to control output files (`2`_); Base Class Definitions (`1`_); pyweb.py (`148`_) - - -This slightly simplifies the configuration and makes the output -look a little nicer. - -LaTeX subclass of Weaver -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Experimental, at best. - -An instance of ``LaTeX`` can be used by the ``Web`` object to -weave an output document. The instance is created outside the Web, and -given to the ``weave()`` method of the Web. - -.. parsed-literal:: - - w= Web() - WebReader().load(w,"somefile.w") - weave_latex= LaTeX() - w.weave( weave_latex ) - -Note that the template language and LaTeX both use ``$``. -This means that all ``$`` that are intended to be output to LaTeX -must appear as ``$$`` in the template. - - -The ``LaTeX`` subclass defines a Weaver that is customized to -produce LaTeX output of code sections and cross reference information. -Its markup is pretty rudimentary, but it's also distinctive enough to -function pretty well in most L\ :sub:`A`\ T\ :sub:`E`\ X documents. - - - -.. _`23`: -.. rubric:: LaTeX subclass of Weaver (23) = -.. parsed-literal:: - :class: code - - - class LaTeX( Weaver ): - """LaTeX formatting for XRef's and code blocks when weaving. - Requires \\\\usepackage{fancyvrb} - """ - extension= ".tex" - code\_indent= 0 - |srarr|\ LaTeX code chunk begin (`24`_) - |srarr|\ LaTeX code chunk end (`25`_) - |srarr|\ LaTeX file output begin (`26`_) - |srarr|\ LaTeX file output end (`27`_) - |srarr|\ LaTeX references summary at the end of a chunk (`28`_) - |srarr|\ LaTeX write a line of code (`29`_) - |srarr|\ LaTeX reference to a chunk (`30`_) - - -.. - - .. class:: small - - |loz| *LaTeX subclass of Weaver (23)*. Used by: Emitter class hierarchy - used to control output files (`2`_); Base Class Definitions (`1`_); pyweb.py (`148`_) - - -The LaTeX ``open()`` method opens the woven file by replacing the -source file's suffix with ``".tex"`` and creating the resulting file. - - -The LaTeX ``codeBegin()`` template writes the header prior to a -chunk of source code. It aligns the block to the left, prints an -italicised header, and opens a preformatted block. - - - -.. _`24`: -.. rubric:: LaTeX code chunk begin (24) = -.. parsed-literal:: - :class: code - - - cb\_template = string.Template( """\\\\label{pyweb${seq}} - \\\\begin{flushleft} - \\\\textit{Code example ${fullName} (${seq})} - \\\\begin{Verbatim}[commandchars=\\\\\\\\\\\\{\\\\},codes={\\\\catcode\`$$=3\\\\catcode\`^=7},frame=single]\\n""") # Prevent indent - - -.. - - .. class:: small - - |loz| *LaTeX code chunk begin (24)*. Used by: LaTeX subclass of Weaver (`23`_); Emitter class hierarchy - used to control output files (`2`_); Base Class Definitions (`1`_); pyweb.py (`148`_) - - - -The LaTeX ``codeEnd()`` template writes the trailer subsequent to -a chunk of source code. This first closes the preformatted block and -then calls the ``references()`` method to write a reference -to the chunk that invokes this chunk; finally, it restores paragraph -indentation. - - -.. _`25`: -.. rubric:: LaTeX code chunk end (25) = -.. parsed-literal:: - :class: code - - - ce\_template= string.Template(""" - \\\\end{Verbatim} - ${references} - \\\\end{flushleft}\\n""") # Prevent indentation - - -.. - - .. class:: small - - |loz| *LaTeX code chunk end (25)*. Used by: LaTeX subclass of Weaver (`23`_); Emitter class hierarchy - used to control output files (`2`_); Base Class Definitions (`1`_); pyweb.py (`148`_) - - - -The LaTeX ``fileBegin()`` template writes the header prior to a -the creation of a tangled file. Its formatting is identical to the -start of a code chunk. - - - -.. _`26`: -.. rubric:: LaTeX file output begin (26) = -.. parsed-literal:: - :class: code - - - fb\_template= cb\_template - - -.. - - .. class:: small - - |loz| *LaTeX file output begin (26)*. Used by: LaTeX subclass of Weaver (`23`_); Emitter class hierarchy - used to control output files (`2`_); Base Class Definitions (`1`_); pyweb.py (`148`_) - - -The LaTeX ``fileEnd()`` template writes the trailer subsequent to -a tangled file. This closes the preformatted block, calls the LaTeX -``references()`` method to write a reference to the chunk that -invokes this chunk, and restores normal indentation. - - -.. _`27`: -.. rubric:: LaTeX file output end (27) = -.. parsed-literal:: - :class: code - - - fe\_template= ce\_template - - -.. - - .. class:: small - - |loz| *LaTeX file output end (27)*. Used by: LaTeX subclass of Weaver (`23`_); Emitter class hierarchy - used to control output files (`2`_); Base Class Definitions (`1`_); pyweb.py (`148`_) - - -The ``references()`` template writes a list of references after a -chunk of code. Each reference includes the example number, the title, -and a reference to the LaTeX section and page numbers on which the -referring block appears. - - -.. _`28`: -.. rubric:: LaTeX references summary at the end of a chunk (28) = -.. parsed-literal:: - :class: code - - - ref\_item\_template = string.Template( """ - \\\\item Code example ${fullName} (${seq}) (Sect. \\\\ref{pyweb${seq}}, p. \\\\pageref{pyweb${seq}})\\n""") - ref\_template = string.Template( """ - \\\\footnotesize - Used by: - \\\\begin{list}{}{} - ${refList} - \\\\end{list} - \\\\normalsize\\n""") - - -.. - - .. class:: small - - |loz| *LaTeX references summary at the end of a chunk (28)*. Used by: LaTeX subclass of Weaver (`23`_); Emitter class hierarchy - used to control output files (`2`_); Base Class Definitions (`1`_); pyweb.py (`148`_) - - -The ``quote()`` method quotes a single line of code to the -weaver; since these lines are always in preformatted blocks, no -special formatting is needed, except to avoid ending the preformatted -block. Our one compromise is a thin space if the phrase -``\\end{Verbatim}`` is used in a code block. - - - -.. _`29`: -.. rubric:: LaTeX write a line of code (29) = -.. parsed-literal:: - :class: code - - - quoted\_chars = [ - ("\\\\end{Verbatim}", "\\\\end\\,{Verbatim}"), # Allow \\end{Verbatim} - ("\\\\{","\\\\\\,{"), # Prevent unexpected commands in Verbatim - ("$","\\\\$"), # Prevent unexpected math in Verbatim - ] - - -.. - - .. class:: small - - |loz| *LaTeX write a line of code (29)*. Used by: LaTeX subclass of Weaver (`23`_); Emitter class hierarchy - used to control output files (`2`_); Base Class Definitions (`1`_); pyweb.py (`148`_) - - -The ``referenceTo()`` template writes a reference to another chunk of -code. It uses write directly as to follow the current indentation on -the current line of code. - - - -.. _`30`: -.. rubric:: LaTeX reference to a chunk (30) = -.. parsed-literal:: - :class: code - - - refto\_name\_template= string.Template("""$$\\\\triangleright$$ Code Example ${fullName} (${seq})""") - refto\_seq\_template= string.Template("""(${seq})""") - - -.. - - .. class:: small - - |loz| *LaTeX reference to a chunk (30)*. Used by: LaTeX subclass of Weaver (`23`_); Emitter class hierarchy - used to control output files (`2`_); Base Class Definitions (`1`_); pyweb.py (`148`_) - - -HTML subclasses of Weaver -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -This works, but, it's not clear that it should be kept. - -An instance of ``HTML`` can be used by the ``Web`` object to -weave an output document. The instance is created outside the Web, and -given to the ``weave()`` method of the Web. - -.. parsed-literal:: - - - w= Web() - WebReader().load(w,"somefile.w") - weave_html= HTML() - w.weave( weave_html ) - - -Variations in the output formatting are accomplished by having -variant subclasses of HTML. In this implementation, we have two -variations: full path references, and short references. The base class -produces complete reference paths; a subclass produces abbreviated references. - - -The ``HTML`` subclass defines a Weaver that is customized to -produce HTML output of code sections and cross reference information. - -All HTML chunks are identified by anchor names of the form ``pyweb*n*``. Each -*n* is the unique chunk number, in sequential order. - -An ``HTMLShort`` subclass defines a Weaver that produces HTML output -with abbreviated (no name) cross references at the end of the chunk. - - -.. _`31`: -.. rubric:: HTML subclass of Weaver (31) = -.. parsed-literal:: - :class: code - - - class HTML( Weaver ): - """HTML formatting for XRef's and code blocks when weaving.""" - extension= ".html" - code\_indent= 0 - |srarr|\ HTML code chunk begin (`33`_) - |srarr|\ HTML code chunk end (`34`_) - |srarr|\ HTML output file begin (`35`_) - |srarr|\ HTML output file end (`36`_) - |srarr|\ HTML references summary at the end of a chunk (`37`_) - |srarr|\ HTML write a line of code (`38`_) - |srarr|\ HTML reference to a chunk (`39`_) - |srarr|\ HTML simple cross reference markup (`40`_) - - -.. - - .. class:: small - - |loz| *HTML subclass of Weaver (31)*. Used by: Emitter class hierarchy - used to control output files (`2`_); Base Class Definitions (`1`_); pyweb.py (`148`_) - - - -.. _`32`: -.. rubric:: HTML subclass of Weaver (32) += -.. parsed-literal:: - :class: code - - - class HTMLShort( HTML ): - """HTML formatting for XRef's and code blocks when weaving with short references.""" - |srarr|\ HTML short references summary at the end of a chunk (`42`_) - - -.. - - .. class:: small - - |loz| *HTML subclass of Weaver (32)*. Used by: Emitter class hierarchy - used to control output files (`2`_); Base Class Definitions (`1`_); pyweb.py (`148`_) - - -The ``codeBegin()`` template starts a chunk of code, defined with ``@d``, providing a label -and HTML tags necessary to set the code off visually. - - - -.. _`33`: -.. rubric:: HTML code chunk begin (33) = -.. parsed-literal:: - :class: code - - - cb\_template= string.Template(""" - - -

${fullName} (${seq}) ${concat}

-
\\n""")
-    
-
-..
-
-    ..  class:: small
-
-        |loz| *HTML code chunk begin (33)*. Used by: HTML subclass of Weaver (`31`_); Emitter class hierarchy - used to control output files (`2`_); Base Class Definitions (`1`_); pyweb.py (`148`_)
-
-
-The ``codeEnd()`` template ends a chunk of code, providing a HTML tags necessary 
-to finish the code block visually.  This calls the references method to
-write the list of chunks that reference this chunk.
-
-
-..  _`34`:
-..  rubric:: HTML code chunk end (34) =
-..  parsed-literal::
-    :class: code
-
-    
-    ce\_template= string.Template("""
-    
-

${fullName} (${seq}). - ${references} -

\\n""") - - -.. - - .. class:: small - - |loz| *HTML code chunk end (34)*. Used by: HTML subclass of Weaver (`31`_); Emitter class hierarchy - used to control output files (`2`_); Base Class Definitions (`1`_); pyweb.py (`148`_) - - -The ``fileBegin()`` template starts a chunk of code, defined with ``@o``, providing a label -and HTML tags necessary to set the code off visually. - - -.. _`35`: -.. rubric:: HTML output file begin (35) = -.. parsed-literal:: - :class: code - - - fb\_template= string.Template(""" - -

\`\`${fullName}\`\` (${seq}) ${concat}

-
\\n""") # Prevent indent
-    
-
-..
-
-    ..  class:: small
-
-        |loz| *HTML output file begin (35)*. Used by: HTML subclass of Weaver (`31`_); Emitter class hierarchy - used to control output files (`2`_); Base Class Definitions (`1`_); pyweb.py (`148`_)
-
-
-The ``fileEnd()`` template ends a chunk of code, providing a HTML tags necessary 
-to finish the code block visually.  This calls the references method to
-write the list of chunks that reference this chunk.
-
-
-..  _`36`:
-..  rubric:: HTML output file end (36) =
-..  parsed-literal::
-    :class: code
-
-    
-    fe\_template= string.Template( """
-

◊ \`\`${fullName}\`\` (${seq}). - ${references} -

\\n""") - - -.. - - .. class:: small - - |loz| *HTML output file end (36)*. Used by: HTML subclass of Weaver (`31`_); Emitter class hierarchy - used to control output files (`2`_); Base Class Definitions (`1`_); pyweb.py (`148`_) - - -The ``references()`` template writes the list of chunks that refer to this chunk. -Note that this list could be rather long because of the possibility of -transitive references. - - -.. _`37`: -.. rubric:: HTML references summary at the end of a chunk (37) = -.. parsed-literal:: - :class: code - - - ref\_item\_template = string.Template( - '${fullName} (${seq})' - ) - ref\_template = string.Template( ' Used by ${refList}.' ) - - -.. - - .. class:: small - - |loz| *HTML references summary at the end of a chunk (37)*. Used by: HTML subclass of Weaver (`31`_); Emitter class hierarchy - used to control output files (`2`_); Base Class Definitions (`1`_); pyweb.py (`148`_) - - -The ``quote()`` method quotes an individual line of code for HTML purposes. -This encodes the four basic HTML entities (``<``, ``>``, ``&``, ``"``) to prevent code from being interpreted -as HTML. - - -.. _`38`: -.. rubric:: HTML write a line of code (38) = -.. parsed-literal:: - :class: code - - - quoted\_chars = [ - ("&", "&"), # Must be first - ("<", "<"), - (">", ">"), - ('"', """), - ] - - -.. - - .. class:: small - - |loz| *HTML write a line of code (38)*. Used by: HTML subclass of Weaver (`31`_); Emitter class hierarchy - used to control output files (`2`_); Base Class Definitions (`1`_); pyweb.py (`148`_) - - -The ``referenceTo()`` template writes a reference to another chunk. It uses the -direct ``write()`` method so that the reference is indented properly with the -surrounding source code. - - -.. _`39`: -.. rubric:: HTML reference to a chunk (39) = -.. parsed-literal:: - :class: code - - - refto\_name\_template = string.Template( - '${fullName} (${seq})' - ) - refto\_seq\_template = string.Template( - '(${seq})' - ) - - -.. - - .. class:: small - - |loz| *HTML reference to a chunk (39)*. Used by: HTML subclass of Weaver (`31`_); Emitter class hierarchy - used to control output files (`2`_); Base Class Definitions (`1`_); pyweb.py (`148`_) - - -The ``xrefHead()`` method writes the heading for any of the cross reference blocks created by -``@f``, ``@m``, or ``@u``. In this implementation, the cross references are simply unordered lists. - -The ``xrefFoot()`` method writes the footing for any of the cross reference blocks created by -``@f``, ``@m``, or ``@u``. In this implementation, the cross references are simply unordered lists. - -The ``xrefLine()`` method writes a line for the file or macro cross reference blocks created by -``@f`` or ``@m``. In this implementation, the cross references are simply unordered lists. - - -.. _`40`: -.. rubric:: HTML simple cross reference markup (40) = -.. parsed-literal:: - :class: code - - - xref\_head\_template = string.Template( "
\\n" ) - xref\_foot\_template = string.Template( "
\\n" ) - xref\_item\_template = string.Template( "
${fullName}
${refList}
\\n" ) - |srarr|\ HTML write user id cross reference line (`41`_) - - -.. - - .. class:: small - - |loz| *HTML simple cross reference markup (40)*. Used by: HTML subclass of Weaver (`31`_); Emitter class hierarchy - used to control output files (`2`_); Base Class Definitions (`1`_); pyweb.py (`148`_) - - -The ``xrefDefLine()`` method writes a line for the user identifier cross reference blocks created by -@u. In this implementation, the cross references are simply unordered lists. The defining instance -is included in the correct order with the other instances, but is bold and marked with a bullet (•). - - - -.. _`41`: -.. rubric:: HTML write user id cross reference line (41) = -.. parsed-literal:: - :class: code - - - name\_def\_template = string.Template( '•${seq}' ) - name\_ref\_template = string.Template( '${seq}' ) - - -.. - - .. class:: small - - |loz| *HTML write user id cross reference line (41)*. Used by: HTML simple cross reference markup (`40`_); HTML subclass of Weaver (`31`_); Emitter class hierarchy - used to control output files (`2`_); Base Class Definitions (`1`_); pyweb.py (`148`_) - - -The HTMLShort subclass enhances the HTML class to provide short -cross references. -The ``references()`` method writes the list of chunks that refer to this chunk. -Note that this list could be rather long because of the possibility of -transitive references. - - -.. _`42`: -.. rubric:: HTML short references summary at the end of a chunk (42) = -.. parsed-literal:: - :class: code - - - ref\_item\_template = string.Template( '(${seq})' ) - - -.. - - .. class:: small - - |loz| *HTML short references summary at the end of a chunk (42)*. Used by: HTML subclass of Weaver (`32`_); Emitter class hierarchy - used to control output files (`2`_); Base Class Definitions (`1`_); pyweb.py (`148`_) - - -Tangler subclass of Emitter -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The ``Tangler`` class is concrete, and can tangle source files. An -instance of ``Tangler`` is given to the ``Web`` class ``tangle()`` method. - -.. parsed-literal:: - - w= Web() - WebReader().load(w,"somefile.w") - t= Tangler() - w.tangle( t ) - - -The ``Tangler`` subclass defines an Emitter used to **tangle** the various -program source files. The superclass is used to simply emit correctly indented -source code and do very little else that could corrupt or alter the output. - -Language-specific subclasses could be used to provide additional decoration. -For example, inserting ``#line`` directives showing the line number -in the original source file. - -For Python, where indentation matters, the indent rules are relatively -simple. The whitespace berfore a ``@<`` command is preserved as -the prevailing indent for the block tangled as a replacement for the ``@<``\ *name*\ ``@>``. - - -.. _`43`: -.. rubric:: Tangler subclass of Emitter to create source files with no markup (43) = -.. parsed-literal:: - :class: code - - - class Tangler( Emitter ): - """Tangle output files.""" - def \_\_init\_\_( self ): - super().\_\_init\_\_() - self.comment\_start= "" - self.comment\_end= "" - self.debug= False - |srarr|\ Tangler doOpen, and doClose overrides (`44`_) - |srarr|\ Tangler code chunk begin (`45`_) - |srarr|\ Tangler code chunk end (`46`_) - - -.. - - .. class:: small - - |loz| *Tangler subclass of Emitter to create source files with no markup (43)*. Used by: Emitter class hierarchy - used to control output files (`2`_); Base Class Definitions (`1`_); pyweb.py (`148`_) - - -The default for all tanglers is to create the named file. -In order to handle paths, we will examine the file name for any ``"/"`` -characters and perform the required ``os.makedirs`` functions to -allow creation of files with a path. We don't use Windows ``"\"`` -characters, but rely on Python to handle this automatically. - -This ``doClose()`` method overrides the ``Emitter`` class ``doClose()`` method by closing the -actual file created by open. - - -.. _`44`: -.. rubric:: Tangler doOpen, and doClose overrides (44) = -.. parsed-literal:: - :class: code - - - def checkPath( self ): - if "/" in self.fileName: - dirname, \_, \_ = self.fileName.rpartition("/") - try: - os.makedirs( dirname ) - self.logger.info( "Creating {!r}".format(dirname) ) - except OSError as e: - # Already exists. Could check for errno.EEXIST. - self.logger.debug( "Exception {!r} creating {!r}".format(e, dirname) ) - def doOpen( self, aFile ): - self.fileName= aFile - self.checkPath() - self.theFile= open( aFile, "w" ) - self.logger.info( "Tangling {!r}".format(aFile) ) - def doClose( self ): - self.theFile.close() - self.logger.info( "Wrote {:d} lines to {!r}".format( - self.linesWritten, self.fileName) ) - - -.. - - .. class:: small - - |loz| *Tangler doOpen, and doClose overrides (44)*. Used by: Tangler subclass of Emitter to create source files with no markup (`43`_); Emitter class hierarchy - used to control output files (`2`_); Base Class Definitions (`1`_); pyweb.py (`148`_) - - -The ``codeBegin()`` method starts emitting a new chunk of code. -It does this by setting the Tangler's indent to the -prevailing indent at the start of the ``@<`` reference command. - - -.. _`45`: -.. rubric:: Tangler code chunk begin (45) = -.. parsed-literal:: - :class: code - - - def codeBegin( self, aChunk ): - self.log\_indent.debug( "{:s}".format(aChunk.fullName) ) - - -.. - - .. class:: small - - |loz| *Tangler code chunk end (46)*. Used by: Tangler subclass of Emitter to create source files with no markup (`43`_); Emitter class hierarchy - used to control output files (`2`_); Base Class Definitions (`1`_); pyweb.py (`148`_) - - -TanglerMake subclass of Tangler -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The ``TanglerMake`` class is can tangle source files. An -instance of ``TanglerMake`` is given to the ``Web`` class ``tangle()`` method. - -.. parsed-literal:: - - w= Web() - WebReader().load(w,"somefile.w") - t= TanglerMake() - w.tangle( t ) - -The ``TanglerMake`` subclass makes the ``Tangler`` used to tangle the various -program source files more make-friendly. This subclass of ``Tangler`` -does not *touch* an output file -where there is no change. This is helpful when *pyWeb*\ 's output is -sent to *make*. Using ``TanglerMake`` assures that only files with real changes -are rewritten, minimizing recompilation of an application for changes to -the associated documentation. - -This subclass of ``Tangler`` changes how files -are opened and closed. - - -.. _`47`: -.. rubric:: Imports (47) += -.. parsed-literal:: - :class: code - - import tempfile - import filecmp - - -.. - - .. class:: small - - |loz| *Imports (47)*. Used by: pyweb.py (`148`_) - - - -.. _`48`: -.. rubric:: TanglerMake subclass which is make-sensitive (48) = -.. parsed-literal:: - :class: code - - - class TanglerMake( Tangler ): - """Tangle output files, leaving files untouched if there are no changes.""" - def \_\_init\_\_( self ): - Tangler.\_\_init\_\_( self ) - self.tempname= None - |srarr|\ TanglerMake doOpen override, using a temporary file (`49`_) - |srarr|\ TanglerMake doClose override, comparing temporary to original (`50`_) - - -.. - - .. class:: small - - |loz| *TanglerMake subclass which is make-sensitive (48)*. Used by: Emitter class hierarchy - used to control output files (`2`_); Base Class Definitions (`1`_); pyweb.py (`148`_) - - -A ``TanglerMake`` creates a temporary file to collect the -tangled output. When this file is completed, we can compare -it with the original file in this directory, avoiding -a "touch" if the new file is the same as the original. - - - -.. _`49`: -.. rubric:: TanglerMake doOpen override, using a temporary file (49) = -.. parsed-literal:: - :class: code - - - def doOpen( self, aFile ): - fd, self.tempname= tempfile.mkstemp( dir=os.curdir ) - self.theFile= os.fdopen( fd, "w" ) - self.logger.info( "Tangling {!r}".format(aFile) ) - - -.. - - .. class:: small - - |loz| *TanglerMake doOpen override, using a temporary file (49)*. Used by: TanglerMake subclass which is make-sensitive (`48`_); Emitter class hierarchy - used to control output files (`2`_); Base Class Definitions (`1`_); pyweb.py (`148`_) - - - This includes a fix for - the `OSError: [Errno 18] Invalid cross-device link `_ - bug - -If there is a previous file: compare the temporary file and the previous file. -If there was previous file or the files are different: rename temporary to replace previous; -else: unlink temporary and discard it. This preserves the original (with the original date -and time) if nothing has changed. - - - -.. _`50`: -.. rubric:: TanglerMake doClose override, comparing temporary to original (50) = -.. parsed-literal:: - :class: code - - - def doClose( self ): - self.theFile.close() - try: - same= filecmp.cmp( self.tempname, self.fileName ) - except OSError as e: - same= False # Doesn't exist. Could check for errno.ENOENT - if same: - self.logger.info( "No change to {!r}".format(self.fileName) ) - os.remove( self.tempname ) - else: - # Windows requires the original file name be removed first. - self.checkPath() - try: - os.remove( self.fileName ) - except OSError as e: - pass # Doesn't exist. Could check for errno.ENOENT - os.rename( self.tempname, self.fileName ) - self.logger.info( "Wrote {:d} lines to {!r}".format( - self.linesWritten, self.fileName) ) - - -.. - - .. class:: small - - |loz| *TanglerMake doClose override, comparing temporary to original (50)*. Used by: TanglerMake subclass which is make-sensitive (`48`_); Emitter class hierarchy - used to control output files (`2`_); Base Class Definitions (`1`_); pyweb.py (`148`_) - - -Chunks --------- - -A ``Chunk`` is a piece of the input file. It is a collection of ``Command`` instances. -A chunk can be woven or tangled to create output. - -The two most important methods are the ``weave()`` and ``tangle()`` methods. These -visit the commands of this chunk, producing the required output file. - -Additional methods (``startswith()``, ``searchForRE()`` and ``usedBy()``) -are used to examine the text of the ``Command`` instances within -the chunk. - -A ``Chunk`` instance is created by the ``WebReader`` as the input file is parsed. -Each ``Chunk`` instance has one or more pieces of the original input text. -This text can be program source, a reference command, or the documentation source. - - -.. _`51`: -.. rubric:: Chunk class hierarchy - used to describe input chunks (51) = -.. parsed-literal:: - :class: code - - - |srarr|\ Chunk class (`52`_) - |srarr|\ NamedChunk class (`62`_) - |srarr|\ OutputChunk class (`67`_) - |srarr|\ NamedDocumentChunk class (`71`_) - -.. - - .. class:: small - - |loz| *Chunk class hierarchy - used to describe input chunks (51)*. Used by: Base Class Definitions (`1`_); pyweb.py (`148`_) - - -The ``Chunk`` class is both the superclass for this hierarchy and the implementation -for anonymous chunks. An anonymous chunk is always documentation in the -target markup language. No transformation is ever done on anonymous chunks. - -A ``NamedChunk`` is a chunk created with a ``@d`` command. -This is a chunk of source programming language, bracketed with ``@{`` and ``@}``. - -An ``OutputChunk`` is a named chunk created with a ``@o`` command. -This must be a chunk of source programming language, bracketed with ``@{`` and ``@}``. - -A ``NamedDocumentChunk`` is a named chunk created with a ``@d`` command. -This is a chunk of documentation in the target markup language, -bracketed with ``@[`` and ``@]``. - - -Chunk Superclass -~~~~~~~~~~~~~~~~~ - -An instance of the ``Chunk`` class has a life that includes four important events: - -- creation, - -- cross-reference, - -- weave, - -- and tangle. - -A ``Chunk`` is created by a ``WebReader``, and associated with a ``Web``. -There are several web append methods, depending on the exact subclass of ``Chunk``. -The ``WebReader`` calls the chunk's ``webAdd()`` method select the correct method -for appending and indexing the chunk. -Individual instances of ``Command`` are appended to the chunk. -The basic outline for creating a ``Chunk`` instance is as follows: - -.. parsed-literal:: - - w= Web( ) - c= Chunk() - c.webAdd( w ) - c.append( *...some Command...* ) - c.append( *...some Command...* ) - -Before weaving or tangling, a cross reference is created for all -user identifiers in all of the ``Chunk`` instances. -This is done by: (1) visit each ``Chunk`` and call the -``getUserIDRefs()`` method to gather all identifiers; (2) for each identifier, -visit each ``Chunk`` and call the ``searchForRE()`` method to find uses of -the identifier. - -.. parsed-literal:: - - ident= [] - for c in *the Web's named chunk list*: - ident.extend( c.getUserIDRefs() ) - for i in ident: - pattern= re.compile('\W{:s}\W'.format(i) ) - for c in *the Web's named chunk list*: - c.searchForRE( pattern ) - -A ``Chunk`` is woven or tangled by the ``Web``. The basic outline for weaving is -as follows. The tangling action is essentially the same. - -.. parsed-literal:: - - for c in *the Web's chunk list*: - c.weave( aWeaver ) - -The ``Chunk`` class contains the overall definitions for all of the -various specialized subclasses. In particular, it contains the ``append()``, -and ``appendText()`` methods used by all of the various ``Chunk`` subclasses. - - -When a ``@@`` construct is located in the input stream, the stream contains -three text tokens: material before the ``@@``, the ``@@``, -and the material after the ``@@``. -These three tokens are reassembled into a single block of text. This reassembly -is accomplished by changing the chunk's state so that the next ``TextCommand`` is -appended onto the previous ``TextCommand``. - -The ``appendText()`` method either: - -- appends to a previous ``TextCommand`` instance, - -- or finds that there are no commands at all, and creates a ``TextCommand`` instance, - -- or finds that the last Command isn't a subclass of ``TextCommand`` - and creates a ``TextCommand`` instance. - -Each subclass of ``Chunk`` has a particular type of text that it will process. Anonymous chunks -only handle document text. The ``NamedChunk`` subclass that handles program source -will override this method to create a different command type. The ``makeContent()`` method -creates the appropriate ``Command`` instance for this ``Chunk`` subclass. - -The ``weave()`` method of an anonymous ``Chunk`` uses the weaver's -``docBegin()`` and ``docEnd()`` -methods to insert text that is source markup. Other subclasses will override this to -use different ``Weaver`` methods for different kinds of text. - -A Chunk has a ``Strategy`` object which is a subclass of Reference. This is -either an instance of SimpleReference or TransitiveReference. -A SimpleRerence does no additional processing, and locates the proximate reference to -this chunk. The TransitiveReference walks "up" the web toward top-level file -definitions that reference this ``Chunk``. - - -The ``Chunk`` constructor initializes the following instance variables: - -:commands: - is a sequence of the various ``Command`` instances the comprise this - chunk. - -:user_id_list: - is used the list of user identifiers associated with - this chunk. This attribute is always ``None`` for this class. - The ``NamedChunk`` subclass, however, can have user identifiers. - -:initial: - is True if this is the first - definition (display with ``'='``) or a subsequent definition (display with ``'+='``). - -:name: - has the name of the chunk. This is '' for anonymous chunks. - -:seq: - has the sequence number associated with this chunk. This is None - for anonymous chunks. - -:referencedBy: - is the list of Chunks which reference this chunk. - -:references: - is the list of Chunks this chunk references. - - -.. _`52`: -.. rubric:: Chunk class (52) = -.. parsed-literal:: - :class: code - - - class Chunk: - """Anonymous piece of input file: will be output through the weaver only.""" - # construction and insertion into the web - def \_\_init\_\_( self ): - self.commands= [ ] # The list of children of this chunk - self.user\_id\_list= None - self.initial= None - self.name= '' - self.fullName= None - self.seq= None - self.referencedBy= [] # Chunks which reference this chunk. Ideally just one. - self.references= [] # Names that this chunk references - - self.reference\_style= None # Instance of Reference - - def \_\_str\_\_( self ): - return "\\n".join( map( str, self.commands ) ) - def \_\_repr\_\_( self ): - return "{:s}('{:s}')".format( self.\_\_class\_\_.\_\_name\_\_, self.name ) - |srarr|\ Chunk append a command (`53`_) - |srarr|\ Chunk append text (`54`_) - |srarr|\ Chunk add to the web (`55`_) - |srarr|\ Chunk generate references from this Chunk (`58`_) - |srarr|\ Chunk superclass make Content definition (`56`_) - |srarr|\ Chunk examination: starts with, matches pattern (`57`_) - |srarr|\ Chunk references to this Chunk (`59`_) - |srarr|\ Chunk weave this Chunk into the documentation (`60`_) - |srarr|\ Chunk tangle this Chunk into a code file (`61`_) - - -.. - - .. class:: small - - |loz| *Chunk class (52)*. Used by: Chunk class hierarchy - used to describe input chunks (`51`_); Base Class Definitions (`1`_); pyweb.py (`148`_) - - -The ``append()`` method simply appends a ``Command`` instance to this chunk. - - -.. _`53`: -.. rubric:: Chunk append a command (53) = -.. parsed-literal:: - :class: code - - - def append( self, command ): - """Add another Command to this chunk.""" - self.commands.append( command ) - command.chunk= self - - -.. - - .. class:: small - - |loz| *Chunk append a command (53)*. Used by: Chunk class (`52`_); Chunk class hierarchy - used to describe input chunks (`51`_); Base Class Definitions (`1`_); pyweb.py (`148`_) - - -The ``appendText()`` method appends a ``TextCommand`` to this chunk, -or it concatenates it to the most recent ``TextCommand``. - -When an ``@@`` construct is located, the ``appendText()`` method is -used to accumulate this character. This means that it will be appended to -any previous TextCommand, or new TextCommand will be built. - -The reason for appending is that a TextCommand has an implicit indentation. The "@" cannot -be a separate TextCommand because it will wind up indented. - - -.. _`54`: -.. rubric:: Chunk append text (54) = -.. parsed-literal:: - :class: code - - - def appendText( self, text, lineNumber=0 ): - """Append a single character to the most recent TextCommand.""" - try: - # Works for TextCommand, otherwise breaks - self.commands[-1].text += text - except IndexError as e: - # First command? Then the list will have been empty. - self.commands.append( self.makeContent(text,lineNumber) ) - except AttributeError as e: - # Not a TextCommand? Then there won't be a text attribute. - self.commands.append( self.makeContent(text,lineNumber) ) - - -.. - - .. class:: small - - |loz| *Chunk append text (54)*. Used by: Chunk class (`52`_); Chunk class hierarchy - used to describe input chunks (`51`_); Base Class Definitions (`1`_); pyweb.py (`148`_) - - -The ``webAdd()`` method adds this chunk to the given document web. -Each subclass of the ``Chunk`` class must override this to be sure that the various -``Chunk`` subclasses are indexed properly. The -``Chunk`` class uses the ``add()`` method -of the ``Web`` class to append an anonymous, unindexed chunk. - - -.. _`55`: -.. rubric:: Chunk add to the web (55) = -.. parsed-literal:: - :class: code - - - def webAdd( self, web ): - """Add self to a Web as anonymous chunk.""" - web.add( self ) - - -.. - - .. class:: small - - |loz| *Chunk add to the web (55)*. Used by: Chunk class (`52`_); Chunk class hierarchy - used to describe input chunks (`51`_); Base Class Definitions (`1`_); pyweb.py (`148`_) - - -This superclass creates a specific Command for a given piece of content. -A subclass can override this to change the underlying assumptions of that Chunk. -The generic chunk doesn't contain code, it contains text and can only be woven, -never tangled. A Named Chunk using ``@{`` and ``@}`` creates code. -A Named Chunk using ``@[`` and ``@]`` creates text. - - - -.. _`56`: -.. rubric:: Chunk superclass make Content definition (56) = -.. parsed-literal:: - :class: code - - - def makeContent( self, text, lineNumber=0 ): - return TextCommand( text, lineNumber ) - - -.. - - .. class:: small - - |loz| *Chunk superclass make Content definition (56)*. Used by: Chunk class (`52`_); Chunk class hierarchy - used to describe input chunks (`51`_); Base Class Definitions (`1`_); pyweb.py (`148`_) - - -The ``startsWith()`` method examines a the first ``Command`` instance this -``Chunk`` instance to see if it starts -with the given prefix string. - -The ``lineNumber()`` method returns the line number of the first -``Command`` in this chunk. This provides some context for where the chunk -occurs in the original input file. - -A ``NamedChunk`` instance may define one or more identifiers. This parent class -provides a dummy version of the ``getUserIDRefs`` method. The ``NamedChunk`` -subclass overrides this to provide actual results. By providing this -at the superclass level, the ``Web`` can easily gather identifiers without -knowing the actual subclass of ``Chunk``. - -The ``searchForRE()`` method examines each ``Command`` instance to see if it matches -with the given regular expression. If so, this can be reported to the Web instance -and accumulated as part of a cross reference for this ``Chunk``. - - -.. _`57`: -.. rubric:: Chunk examination: starts with, matches pattern (57) = -.. parsed-literal:: - :class: code - - - def startswith( self, prefix ): - """Examine the first command's starting text.""" - return len(self.commands) >= 1 and self.commands[0].startswith( prefix ) - - def searchForRE( self, rePat ): - """Visit each command, applying the pattern.""" - for c in self.commands: - if c.searchForRE( rePat ): - return self - return None - - @property - def lineNumber( self ): - """Return the first command's line number or None.""" - return self.commands[0].lineNumber if len(self.commands) >= 1 else None - - def getUserIDRefs( self ): - return [] - - -.. - - .. class:: small - - |loz| *Chunk examination: starts with, matches pattern (57)*. Used by: Chunk class (`52`_); Chunk class hierarchy - used to describe input chunks (`51`_); Base Class Definitions (`1`_); pyweb.py (`148`_) - - -The chunk search in the ``searchForRE()`` method parallels weaving and tangling a ``Chunk``. -The operation is delegated to each ``Command`` instance within the ``Chunk`` instance. - -The ``genReferences()`` method visits each ``Command`` instance inside this chunk; -a ``Command`` will yield the references. - -Note that an exception may be raised by this operation if a referenced -``Chunk`` does not actually exist. If a reference ``Command`` does raise an error, -we append this ``Chunk`` information and reraise the error with the additional -context information. - - - -.. _`58`: -.. rubric:: Chunk generate references from this Chunk (58) = -.. parsed-literal:: - :class: code - - - def genReferences( self, aWeb ): - """Generate references from this Chunk.""" - try: - for t in self.commands: - ref= t.ref( aWeb ) - if ref is not None: - yield ref - except Error as e: - raise - - -.. - - .. class:: small - - |loz| *Chunk generate references from this Chunk (58)*. Used by: Chunk class (`52`_); Chunk class hierarchy - used to describe input chunks (`51`_); Base Class Definitions (`1`_); pyweb.py (`148`_) - - -The list of references to a Chunk uses a ``Strategy`` plug-in -to either generate a simple parent or a transitive closure of all parents. - - - -.. _`59`: -.. rubric:: Chunk references to this Chunk (59) = -.. parsed-literal:: - :class: code - - - @property - def references\_list( self ): - """This should return chunks themselves, not (name,seq) pairs.""" - return self.reference\_style.chunkReferencedBy( self ) - -.. - - .. class:: small - - |loz| *Chunk references to this Chunk (59)*. Used by: Chunk class (`52`_); Chunk class hierarchy - used to describe input chunks (`51`_); Base Class Definitions (`1`_); pyweb.py (`148`_) - - -The ``weave()`` method weaves this chunk into the final document as follows: - - 1. call the ``Weaver`` class ``docBegin()`` method. This method does nothing for document content. - - 2. visit each ``Command`` instance: call the ``Command`` instance ``weave()`` method to - emit the content of the ``Command`` instance - - 3. call the ``Weaver`` class ``docEnd()`` method. This method does nothing for document content. - -Note that an exception may be raised by this action if a referenced -``Chunk`` does not actually exist. If a reference ``Command`` does raise an error, -we append this ``Chunk`` information and reraise the error with the additional -context information. - - - -.. _`60`: -.. rubric:: Chunk weave this Chunk into the documentation (60) = -.. parsed-literal:: - :class: code - - - def weave( self, aWeb, aWeaver ): - """Create the nicely formatted document from an anonymous chunk.""" - aWeaver.docBegin( self ) - try: - for cmd in self.commands: - cmd.weave( aWeb, aWeaver ) - except Error as e: - raise - aWeaver.docEnd( self ) - def weaveReferenceTo( self, aWeb, aWeaver ): - """Create a reference to this chunk -- except for anonymous chunks.""" - raise Exception( "Cannot reference an anonymous chunk.""") - def weaveShortReferenceTo( self, aWeb, aWeaver ): - """Create a short reference to this chunk -- except for anonymous chunks.""" - raise Exception( "Cannot reference an anonymous chunk.""") - - -.. - - .. class:: small - - |loz| *Chunk weave this Chunk into the documentation (60)*. Used by: Chunk class (`52`_); Chunk class hierarchy - used to describe input chunks (`51`_); Base Class Definitions (`1`_); pyweb.py (`148`_) - - -Anonymous chunks cannot be tangled. Any attempt indicates a serious -problem with this program or the input file. - - -.. _`61`: -.. rubric:: Chunk tangle this Chunk into a code file (61) = -.. parsed-literal:: - :class: code - - - def tangle( self, aWeb, aTangler ): - """Create source code -- except anonymous chunks should not be tangled""" - raise Error( 'Cannot tangle an anonymous chunk', self ) - - -.. - - .. class:: small - - |loz| *Chunk tangle this Chunk into a code file (61)*. Used by: Chunk class (`52`_); Chunk class hierarchy - used to describe input chunks (`51`_); Base Class Definitions (`1`_); pyweb.py (`148`_) - - -NamedChunk class -~~~~~~~~~~~~~~~~ - -A ``NamedChunk`` is created and used almost identically to an anonymous ``Chunk``. -The most significant difference is that a name is provided when the ``NamedChunk`` is created. -This name is used by the ``Web`` to organize the chunks. - -A ``NamedChunk`` is created with a ``@d`` or ``@o`` command. -A ``NamedChunk`` contains programming language source -when the brackets are ``@{`` and ``@}``. A -separate subclass of ``NamedDocumentChunk`` is used when -the brackets are ``@[`` and ``@]``. - -A ``NamedChunk`` can be both tangled into the output program files, and -woven into the output document file. - -The ``weave()`` method of a ``NamedChunk`` uses the Weaver's -``codeBegin()`` and ``codeEnd()`` -methods to insert text that is program source and requires additional -markup to make it stand out from documentation. Other subclasses can override this to -use different ``Weaver`` methods for different kinds of text. - - -This class introduces some additional attributes. - -:fullName: - is the full name of the chunk. It's possible for a - chunk to be an abbreviated forward reference; full names cannot be resolved - until all chunks have been seen. - -:user_id_list: - is the list of user identifiers associated with this chunk. - -:refCount: - is the count of references to this chunk. If this is - zero, the chunk is unused; if this is more than one, this chunk is - multiply used. Either of these conditions is a possible error in the input. - This is set by the ``usedBy()`` method. - -:name: - has the name of the chunk. Names can be abbreviated. - -:seq: - has the sequence number associated with this chunk. This - is set by the Web by the ``webAdd()`` method. - - - -.. _`62`: -.. rubric:: NamedChunk class (62) = -.. parsed-literal:: - :class: code - - - class NamedChunk( Chunk ): - """Named piece of input file: will be output as both tangler and weaver.""" - def \_\_init\_\_( self, name ): - Chunk.\_\_init\_\_( self ) - self.name= name - self.user\_id\_list= [] - self.refCount= 0 - def \_\_str\_\_( self ): - return "{!r}: {:s}".format( self.name, Chunk.\_\_str\_\_(self) ) - def makeContent( self, text, lineNumber=0 ): - return CodeCommand( text, lineNumber ) - |srarr|\ NamedChunk user identifiers set and get (`63`_) - |srarr|\ NamedChunk add to the web (`64`_) - |srarr|\ NamedChunk weave into the documentation (`65`_) - |srarr|\ NamedChunk tangle into the source file (`66`_) - - -.. - - .. class:: small - - |loz| *NamedChunk class (62)*. Used by: Chunk class hierarchy - used to describe input chunks (`51`_); Base Class Definitions (`1`_); pyweb.py (`148`_) - - -The ``setUserIDRefs()`` method accepts a list of user identifiers that are -associated with this chunk. These are provided after the ``@|`` separator -in a ``@d`` named chunk. These are used by the ``@u`` cross reference generator. - - -.. _`63`: -.. rubric:: NamedChunk user identifiers set and get (63) = -.. parsed-literal:: - :class: code - - - def setUserIDRefs( self, text ): - """Save user ID's associated with this chunk.""" - self.user\_id\_list= text.split() - def getUserIDRefs( self ): - return self.user\_id\_list - - -.. - - .. class:: small - - |loz| *NamedChunk user identifiers set and get (63)*. Used by: NamedChunk class (`62`_); Chunk class hierarchy - used to describe input chunks (`51`_); Base Class Definitions (`1`_); pyweb.py (`148`_) - - -The ``webAdd()`` method adds this chunk to the given document ``Web`` instance. -Each class of ``Chunk`` must override this to be sure that the various -``Chunk`` classes are indexed properly. This class uses the ``addNamed()`` method -of the ``Web`` class to append a named chunk. - - -.. _`64`: -.. rubric:: NamedChunk add to the web (64) = -.. parsed-literal:: - :class: code - - - def webAdd( self, web ): - """Add self to a Web as named chunk, update xrefs.""" - web.addNamed( self ) - - -.. - - .. class:: small - - |loz| *NamedChunk add to the web (64)*. Used by: NamedChunk class (`62`_); Chunk class hierarchy - used to describe input chunks (`51`_); Base Class Definitions (`1`_); pyweb.py (`148`_) - - -The ``weave()`` method weaves this chunk into the final document as follows: - -1. call the ``Weaver`` class ``codeBegin()`` method. This method emits the necessary markup - for code appearing in the woven output. - -2. visit each ``Command``, calling the command's ``weave()`` method to emit the command's content. - -3. call the ``Weaver`` class ``CodeEnd()`` method. This method emits the necessary markup - for code appearing in the woven output. - - -For an RST weaver this becomes a ``parsed-literal``, which requires a extra indent. -For an HTML weaver this becomes a ``
`` in a different-colored box.
-
-References generate links in a woven document. In a tangled document, they create the actual
-code. The ``weaveRefenceTo()`` method weaves a reference to a chunk using both name and sequence number.
-The ``weaveShortReferenceTo()`` method weaves a reference to a chunk using only the sequence number.
-These references are created by ``ReferenceCommand`` instances within a chunk being woven.
-
-The woven references simply follow whatever preceded them on the line; the indent
-(if any) doesn't change from the default.
-
-
-
-..  _`65`:
-..  rubric:: NamedChunk weave into the documentation (65) =
-..  parsed-literal::
-    :class: code
-
-    
-    def weave( self, aWeb, aWeaver ):
-        """Create the nicely formatted document from a chunk of code."""
-        self.fullName= aWeb.fullNameFor( self.name )
-        aWeaver.setIndent()
-        aWeaver.codeBegin( self )
-        for cmd in self.commands:
-            try:
-                cmd.weave( aWeb, aWeaver )
-            except Error as e:
-                raise
-        aWeaver.clrIndent( )
-        aWeaver.codeEnd( self )
-    def weaveReferenceTo( self, aWeb, aWeaver ):
-        """Create a reference to this chunk."""
-        self.fullName= aWeb.fullNameFor( self.name )
-        txt= aWeaver.referenceTo( self.fullName, self.seq )
-        aWeaver.codeBlock( txt )
-    def weaveShortReferenceTo( self, aWeb, aWeaver ):
-        """Create a shortened reference to this chunk."""
-        txt= aWeaver.referenceTo( None, self.seq )
-        aWeaver.codeBlock( txt )
-    
-
-..
-
-    ..  class:: small
-
-        |loz| *NamedChunk weave into the documentation (65)*. Used by: NamedChunk class (`62`_); Chunk class hierarchy - used to describe input chunks (`51`_); Base Class Definitions (`1`_); pyweb.py (`148`_)
-
-
-The ``tangle()`` method tangles this chunk into the final document as follows:
-
-1.  call the ``Tangler`` class ``codeBegin()`` method to set indents properly.
-
-2.  visit each Command, calling the Command's ``tangle()`` method to emit the Command's content.
-
-3.  call the ``Tangler`` class ``codeEnd()`` method to restore indents.
-
-If a ``ReferenceCommand`` does raise an error during tangling,
-we append this Chunk information and reraise the error with the additional 
-context information.
-
-
-
-..  _`66`:
-..  rubric:: NamedChunk tangle into the source file (66) =
-..  parsed-literal::
-    :class: code
-
-    
-    def tangle( self, aWeb, aTangler ):
-        """Create source code."""
-        # use aWeb to resolve @
-        # format as correctly indented source text
-        self.previous\_command= TextCommand( "", self.commands[0].lineNumber )
-        aTangler.codeBegin( self )
-        for t in self.commands:
-            try:
-                t.tangle( aWeb, aTangler )
-            except Error as e:
-                raise
-            self.previous\_command= t
-        aTangler.codeEnd( self )
-    
-
-..
-
-    ..  class:: small
-
-        |loz| *NamedChunk tangle into the source file (66)*. Used by: NamedChunk class (`62`_); Chunk class hierarchy - used to describe input chunks (`51`_); Base Class Definitions (`1`_); pyweb.py (`148`_)
-
-
-OutputChunk class
-~~~~~~~~~~~~~~~~~~~
-
-A ``OutputChunk`` is created and used identically to a ``NamedChunk``.
-The difference between this class and the parent class is the decoration of 
-the markup when weaving.
-
-The ``OutputChunk`` class is a subclass of ``NamedChunk`` that handles 
-file output chunks defined with ``@o``. 
-
-The ``weave()`` method of a ``OutputChunk`` uses the Weaver's 
-``fileBegin()`` and ``fileEnd()``
-methods to insert text that is program source and requires additional
-markup to make it stand out from documentation.  Other subclasses could override this to 
-use different ``Weaver`` methods for different kinds of text.
-
-All other methods, including the tangle method are identical to ``NamedChunk``.
-
-
-..  _`67`:
-..  rubric:: OutputChunk class (67) =
-..  parsed-literal::
-    :class: code
-
-    
-    class OutputChunk( NamedChunk ):
-        """Named piece of input file, defines an output tangle."""
-        def \_\_init\_\_( self, name, comment\_start="", comment\_end="" ):
-            super().\_\_init\_\_( name )
-            self.comment\_start= comment\_start
-            self.comment\_end= comment\_end
-        |srarr|\ OutputChunk add to the web (`68`_)
-        |srarr|\ OutputChunk weave (`69`_)
-        |srarr|\ OutputChunk tangle (`70`_)
-    
-
-..
-
-    ..  class:: small
-
-        |loz| *OutputChunk class (67)*. Used by: Chunk class hierarchy - used to describe input chunks (`51`_); Base Class Definitions (`1`_); pyweb.py (`148`_)
-
-
-The ``webAdd()`` method adds this chunk to the given document ``Web``.
-Each class of ``Chunk`` must override this to be sure that the various
-``Chunk`` classes are indexed properly.  This class uses the ``addOutput()`` method
-of the ``Web`` class to append a file output chunk.
-
-
-..  _`68`:
-..  rubric:: OutputChunk add to the web (68) =
-..  parsed-literal::
-    :class: code
-
-    
-    def webAdd( self, web ):
-        """Add self to a Web as output chunk, update xrefs."""
-        web.addOutput( self )
-    
-
-..
-
-    ..  class:: small
-
-        |loz| *OutputChunk add to the web (68)*. Used by: OutputChunk class (`67`_); Chunk class hierarchy - used to describe input chunks (`51`_); Base Class Definitions (`1`_); pyweb.py (`148`_)
-
-
-The ``weave()`` method weaves this chunk into the final document as follows:
-
-1.  call the ``Weaver`` class ``codeBegin()`` method to emit proper markup for an output file chunk.
-
-2.  visit each ``Command``, call the Command's ``weave()`` method to emit the Command's content.
-
-3.  call the ``Weaver`` class ``codeEnd()`` method to emit proper markup for an output file chunk.
-
-These chunks of documentation are never tangled.  Any attempt is an
-error.
-
-If a ``ReferenceCommand`` does raise an error during weaving,
-we append this ``Chunk`` information and reraise the error with the additional 
-context information.
-
-
-
-..  _`69`:
-..  rubric:: OutputChunk weave (69) =
-..  parsed-literal::
-    :class: code
-
-    
-    def weave( self, aWeb, aWeaver ):
-        """Create the nicely formatted document from a chunk of code."""
-        self.fullName= aWeb.fullNameFor( self.name )
-        aWeaver.fileBegin( self )
-        try:
-            for cmd in self.commands:
-                cmd.weave( aWeb, aWeaver )
-        except Error as e:
-            raise
-        aWeaver.fileEnd( self )
-    
-
-..
-
-    ..  class:: small
-
-        |loz| *OutputChunk weave (69)*. Used by: OutputChunk class (`67`_); Chunk class hierarchy - used to describe input chunks (`51`_); Base Class Definitions (`1`_); pyweb.py (`148`_)
-
-
-
-..  _`70`:
-..  rubric:: OutputChunk tangle (70) =
-..  parsed-literal::
-    :class: code
-
-    
-    def tangle( self, aWeb, aTangler ):
-        aTangler.comment\_start= self.comment\_start
-        aTangler.comment\_end= self.comment\_end
-        super().tangle( aWeb, aTangler )
-
-..
-
-    ..  class:: small
-
-        |loz| *OutputChunk tangle (70)*. Used by: OutputChunk class (`67`_); Chunk class hierarchy - used to describe input chunks (`51`_); Base Class Definitions (`1`_); pyweb.py (`148`_)
-
-
-NamedDocumentChunk class
-~~~~~~~~~~~~~~~~~~~~~~~~~
-
-A ``NamedDocumentChunk`` is created and used identically to a ``NamedChunk``.
-The difference between this class and the parent class is that this chunk
-is only woven when referenced.  The original definition is silently skipped.
-
-The ``NamedDocumentChunk`` class is a subclass of ``NamedChunk`` that handles 
-named chunks defined with ``@d`` and the ``@[``...``@]`` delimiters.  
-These are woven slightly
-differently, since they are document source, not programming language source.
-
-We're not as interested in the cross reference of named document chunks.
-They can be used multiple times or never.  They are often referenced
-by anonymous chunks.  While this chunk subclass participates in this data 
-gathering, it is ignored for reporting purposes.
-
-All other methods, including the tangle method are identical to ``NamedChunk``.
-
-
-
-..  _`71`:
-..  rubric:: NamedDocumentChunk class (71) =
-..  parsed-literal::
-    :class: code
-
-    
-    class NamedDocumentChunk( NamedChunk ):
-        """Named piece of input file with document source, defines an output tangle."""
-        def makeContent( self, text, lineNumber=0 ):
-            return TextCommand( text, lineNumber )
-        |srarr|\ NamedDocumentChunk weave (`72`_)
-        |srarr|\ NamedDocumentChunk tangle (`73`_)
-    
-
-..
-
-    ..  class:: small
-
-        |loz| *NamedDocumentChunk class (71)*. Used by: Chunk class hierarchy - used to describe input chunks (`51`_); Base Class Definitions (`1`_); pyweb.py (`148`_)
-
-
-The ``weave()`` method quietly ignores this chunk in the document.
-A named document chunk is only included when it is referenced 
-during weaving of another chunk (usually an anonymous document
-chunk).
-
-The ``weaveReferenceTo()`` method inserts the content of this
-chunk into the output document.  This is done in response to a
-``ReferenceCommand`` in another chunk.  
-The ``weaveShortReferenceTo()`` method calls the ``weaveReferenceTo()``
-to insert the entire chunk.
-
-
-
-..  _`72`:
-..  rubric:: NamedDocumentChunk weave (72) =
-..  parsed-literal::
-    :class: code
-
-    
-    def weave( self, aWeb, aWeaver ):
-        """Ignore this when producing the document."""
-        pass
-    def weaveReferenceTo( self, aWeb, aWeaver ):
-        """On a reference to this chunk, expand the body in place."""
-        try:
-            for cmd in self.commands:
-                cmd.weave( aWeb, aWeaver )
-        except Error as e:
-            raise
-    def weaveShortReferenceTo( self, aWeb, aWeaver ):
-        """On a reference to this chunk, expand the body in place."""
-        self.weaveReferenceTo( aWeb, aWeaver )
-    
-
-..
-
-    ..  class:: small
-
-        |loz| *NamedDocumentChunk weave (72)*. Used by: NamedDocumentChunk class (`71`_); Chunk class hierarchy - used to describe input chunks (`51`_); Base Class Definitions (`1`_); pyweb.py (`148`_)
-
-
-
-..  _`73`:
-..  rubric:: NamedDocumentChunk tangle (73) =
-..  parsed-literal::
-    :class: code
-
-    
-    def tangle( self, aWeb, aTangler ):
-        """Raise an exception on an attempt to tangle."""
-        raise Error( "Cannot tangle a chunk defined with @[.""" )
-    
-
-..
-
-    ..  class:: small
-
-        |loz| *NamedDocumentChunk tangle (73)*. Used by: NamedDocumentChunk class (`71`_); Chunk class hierarchy - used to describe input chunks (`51`_); Base Class Definitions (`1`_); pyweb.py (`148`_)
-
-
-Commands
---------
-
-The input stream is broken into individual commands, based on the
-various ``@*x*`` strings in the file.  There are several subclasses of ``Command``,
-each used to describe a different command or block of text in the input.
-
-
-All instances of the ``Command`` class are created by a ``WebReader`` instance.  
-In this case, a ``WebReader`` can be thought of as a factory for ``Command`` instances.
-Each ``Command`` instance is appended to the sequence of commands that
-belong to a ``Chunk``.  A chunk may be as small as a single command, or a long sequence
-of commands.
-
-Each command instance responds to methods to examine the content, gather 
-cross reference information and tangle a file or weave the final document.
-
-
-
-..  _`74`:
-..  rubric:: Command class hierarchy - used to describe individual commands (74) =
-..  parsed-literal::
-    :class: code
-
-    
-    |srarr|\ Command superclass (`75`_)
-    |srarr|\ TextCommand class to contain a document text block (`78`_)
-    |srarr|\ CodeCommand class to contain a program source code block (`79`_)
-    |srarr|\ XrefCommand superclass for all cross-reference commands (`80`_)
-    |srarr|\ FileXrefCommand class for an output file cross-reference (`81`_)
-    |srarr|\ MacroXrefCommand class for a named chunk cross-reference (`82`_)
-    |srarr|\ UserIdXrefCommand class for a user identifier cross-reference (`83`_)
-    |srarr|\ ReferenceCommand class for chunk references (`84`_)
-
-..
-
-    ..  class:: small
-
-        |loz| *Command class hierarchy - used to describe individual commands (74)*. Used by: Base Class Definitions (`1`_); pyweb.py (`148`_)
-
-
-Command Superclass
-~~~~~~~~~~~~~~~~~~~~
-
-A ``Command`` is created by the ``WebReader``, and attached to a ``Chunk``.
-The Command participates in cross reference creation, weaving and tangling.
-
-The ``Command`` superclass is abstract, and has default methods factored out
-of the various subclasses.  When a subclass is created, it will override some
-of the methods provided in this superclass.
-
-..  parsed-literal::
-
-    class MyNewCommand( Command ):
-        *... overrides for various methods ...*
-
-Additionally, a subclass of ``WebReader`` must be defined to parse the new command
-syntax.  The main ``process()`` function must also be updated to use this new subclass
-of ``WebReader``.
-
-
-The ``Command`` superclass provides the parent class definition
-for all of the various command types.  The most common command
-is a block of text, which is woven or tangled.  The next most
-common command is a reference to a chunk, which is woven as a 
-mark-up reference, but tangled as an expansion of the source 
-code.
-
-
--   The ``startswith()`` method examines any source text to see if
-    it begins with the given prefix text.
-
--   The ``searchForRE()`` method examines any source text to see if
-    it matches the given regular expression, usually a match for a user identifier.
-
--   The ``ref()`` method is ignored by all but the ``Reference`` subclass,
-    which returns reference made by the command to the parent chunk.
-
--   The ``weave()`` method weaves this into the output.  If a document text
-    command, it is emitted directly; if a program source code command, 
-    markup is applied.  In the case of cross-reference commands,
-    the actual cross-reference content is emitted.  In the case of 
-    reference commands, they are woven as a reference to a named
-    chunk.
-
--   The ``tangle()`` method tangles this into the output.  If a
-    this is a document text command, it is ignored; if a this is a
-    program source code
-    command, it is indented and emitted.  In the case of cross-reference
-    commands, no output is produced.  In the case of reference
-    commands, the named chunk is indented and emitted.
-
-
-The attributes of a ``Command`` instance includes the line number on which
-the command began, in *lineNumber*.
-
-
-..  _`75`:
-..  rubric:: Command superclass (75) =
-..  parsed-literal::
-    :class: code
-
-    
-    class Command:
-        """A Command is the lowest level of granularity in the input stream."""
-        def \_\_init\_\_( self, fromLine=0 ):
-            self.lineNumber= fromLine+1 # tokenizer is zero-based
-            self.chunk= None
-            self.logger= logging.getLogger( self.\_\_class\_\_.\_\_qualname\_\_ )
-        def \_\_str\_\_( self ):
-            return "at {!r}".format(self.lineNumber)
-        |srarr|\ Command analysis features: starts-with and Regular Expression search (`76`_)
-        |srarr|\ Command tangle and weave functions (`77`_)
-    
-
-..
-
-    ..  class:: small
-
-        |loz| *Command superclass (75)*. Used by: Command class hierarchy - used to describe individual commands (`74`_); Base Class Definitions (`1`_); pyweb.py (`148`_)
-
-
-
-..  _`76`:
-..  rubric:: Command analysis features: starts-with and Regular Expression search (76) =
-..  parsed-literal::
-    :class: code
-
-    
-    def startswith( self, prefix ):
-        return None
-    def searchForRE( self, rePat ):
-        return None
-    def indent( self ):
-        return None
-    
-
-..
-
-    ..  class:: small
-
-        |loz| *Command analysis features: starts-with and Regular Expression search (76)*. Used by: Command superclass (`75`_); Command class hierarchy - used to describe individual commands (`74`_); Base Class Definitions (`1`_); pyweb.py (`148`_)
-
-
-
-..  _`77`:
-..  rubric:: Command tangle and weave functions (77) =
-..  parsed-literal::
-    :class: code
-
-    
-    def ref( self, aWeb ):
-        return None
-    def weave( self, aWeb, aWeaver ):
-        pass
-    def tangle( self, aWeb, aTangler ):
-        pass
-    
-
-..
-
-    ..  class:: small
-
-        |loz| *Command tangle and weave functions (77)*. Used by: Command superclass (`75`_); Command class hierarchy - used to describe individual commands (`74`_); Base Class Definitions (`1`_); pyweb.py (`148`_)
-
-
-TextCommand class
-~~~~~~~~~~~~~~~~~~
-
-A ``TextCommand`` is created by a ``Chunk`` or a ``NamedDocumentChunk`` when a 
-``WebReader`` calls the chunk's ``appendText()`` method.
-
-This Command participates in cross reference creation, weaving and tangling.  When it is
-created, the source line number is provided so that this text can be tied back
-to the source document. 
-
-An instance of the ``TextCommand`` class is a block of document text.  It can originate
-in an anonymous block or a named chunk delimited with ``@[`` and ``@]``.
-
-This subclass provides a concrete implementation for all of the methods.  Since
-text is the author's original markup language, it is emitted directly to the weaver
-or tangler.
-
-
-
-..  _`78`:
-..  rubric:: TextCommand class to contain a document text block (78) =
-..  parsed-literal::
-    :class: code
-
-    
-    class TextCommand( Command ):
-        """A piece of document source text."""
-        def \_\_init\_\_( self, text, fromLine=0 ):
-            super().\_\_init\_\_( fromLine )
-            self.text= text
-        def \_\_str\_\_( self ):
-            return "at {!r}: {!r}...".format(self.lineNumber,self.text[:32])
-        def startswith( self, prefix ):
-            return self.text.startswith( prefix )
-        def searchForRE( self, rePat ):
-            return rePat.search( self.text )
-        def indent( self ):
-            if self.text.endswith('\\n'):
-                return 0
-            try:
-                last\_line = self.text.splitlines()[-1]
-                return len(last\_line)
-            except IndexError:
-                return 0
-        def weave( self, aWeb, aWeaver ):
-            aWeaver.write( self.text )
-        def tangle( self, aWeb, aTangler ):
-            aTangler.write( self.text )
-    
-
-..
-
-    ..  class:: small
-
-        |loz| *TextCommand class to contain a document text block (78)*. Used by: Command class hierarchy - used to describe individual commands (`74`_); Base Class Definitions (`1`_); pyweb.py (`148`_)
-
-
-CodeCommand class
-~~~~~~~~~~~~~~~~~~
-
-A ``CodeCommand`` is created by a ``NamedChunk`` when a 
-``WebReader`` calls the ``appendText()`` method.
-The Command participates in cross reference creation, weaving and tangling.  When it is
-created, the source line number is provided so that this text can be tied back
-to the source document. 
-
-
-An instance of the ``CodeCommand`` class is a block of program source code text.
-It can originate in a named chunk (``@d``) with a ``@{`` and ``@}`` delimiter.
-Or it can be a file output chunk (``@o``).
-
-
-It uses the ``codeBlock()`` methods of a ``Weaver`` or ``Tangler``.  The weaver will 
-insert appropriate markup for this code.  The tangler will assure that the prevailing
-indentation is maintained.
-
-
-
-..  _`79`:
-..  rubric:: CodeCommand class to contain a program source code block (79) =
-..  parsed-literal::
-    :class: code
-
-    
-    class CodeCommand( TextCommand ):
-        """A piece of program source code."""
-        def weave( self, aWeb, aWeaver ):
-            aWeaver.codeBlock( aWeaver.quote( self.text ) )
-        def tangle( self, aWeb, aTangler ):
-            aTangler.codeBlock( self.text )
-    
-
-..
-
-    ..  class:: small
-
-        |loz| *CodeCommand class to contain a program source code block (79)*. Used by: Command class hierarchy - used to describe individual commands (`74`_); Base Class Definitions (`1`_); pyweb.py (`148`_)
-
-
-XrefCommand superclass
-~~~~~~~~~~~~~~~~~~~~~~~
-
-An ``XrefCommand`` is created by the ``WebReader`` when any of the 
-``@f``, ``@m``, ``@u`` commands are found in the input stream.
-The Command is then appended to the current Chunk being built by the WebReader.
-
-The ``XrefCommand`` superclass defines any common features of the
-various cross-reference commands (``@f``, ``@m``, ``@u``).
-
-The ``formatXref()`` method creates the body of a cross-reference
-by the following algorithm:
-
-1. Use the ``Weaver`` class ``xrefHead()`` method to emit the cross-reference header.
-
-2. Sort the keys in the cross-reference mapping.
-
-3. Use the ``Weaver`` class ``xrefLine()`` method to emit each line of the cross-reference mapping.
-
-4. Use the ``Weaver`` class ``xrefFoot()`` method to emit the cross-reference footer.
-
-If this command winds up in a tangle action, that use
-is illegal.  An exception is raised and processing stops.
-
- 
-
-..  _`80`:
-..  rubric:: XrefCommand superclass for all cross-reference commands (80) =
-..  parsed-literal::
-    :class: code
-
-    
-    class XrefCommand( Command ):
-        """Any of the Xref-goes-here commands in the input."""
-        def \_\_str\_\_( self ):
-            return "at {!r}: cross reference".format(self.lineNumber)
-        def formatXref( self, xref, aWeaver ):
-            aWeaver.xrefHead()
-            for n in sorted(xref):
-                aWeaver.xrefLine( n, xref[n] )
-            aWeaver.xrefFoot()
-        def tangle( self, aWeb, aTangler ):
-            raise Error('Illegal tangling of a cross reference command.')
-    
-
-..
-
-    ..  class:: small
-
-        |loz| *XrefCommand superclass for all cross-reference commands (80)*. Used by: Command class hierarchy - used to describe individual commands (`74`_); Base Class Definitions (`1`_); pyweb.py (`148`_)
-
-
-FileXrefCommand class
-~~~~~~~~~~~~~~~~~~~~~~~
-
-A ``FileXrefCommand`` is created by the ``WebReader`` when the 
-``@f`` command is found in the input stream.
-The Command is then appended to the current Chunk being built by the WebReader.
-
-The ``FileXrefCommand`` class weave method gets the
-file cross reference from the overall web instance, and uses
-the  ``formatXref()`` method of the ``XrefCommand`` superclass for format this result.
-
-
-
-..  _`81`:
-..  rubric:: FileXrefCommand class for an output file cross-reference (81) =
-..  parsed-literal::
-    :class: code
-
-    
-    class FileXrefCommand( XrefCommand ):
-        """A FileXref command."""
-        def weave( self, aWeb, aWeaver ):
-            """Weave a File Xref from @o commands."""
-            self.formatXref( aWeb.fileXref(), aWeaver )
-    
-
-..
-
-    ..  class:: small
-
-        |loz| *FileXrefCommand class for an output file cross-reference (81)*. Used by: Command class hierarchy - used to describe individual commands (`74`_); Base Class Definitions (`1`_); pyweb.py (`148`_)
-
-
-MacroXrefCommand class
-~~~~~~~~~~~~~~~~~~~~~~
-
-A ``MacroXrefCommand`` is created by the ``WebReader`` when the 
-``@m`` command is found in the input stream.
-The Command is then appended to the current Chunk being built by the WebReader.
-
-The ``MacroXrefCommand`` class weave method gets the
-named chunk (macro) cross reference from the overall web instance, and uses
-the ``formatXref()`` method of the ``XrefCommand`` superclass method for format this result.
-
-
-
-..  _`82`:
-..  rubric:: MacroXrefCommand class for a named chunk cross-reference (82) =
-..  parsed-literal::
-    :class: code
-
-    
-    class MacroXrefCommand( XrefCommand ):
-        """A MacroXref command."""
-        def weave( self, aWeb, aWeaver ):
-            """Weave the Macro Xref from @d commands."""
-            self.formatXref( aWeb.chunkXref(), aWeaver )
-    
-
-..
-
-    ..  class:: small
-
-        |loz| *MacroXrefCommand class for a named chunk cross-reference (82)*. Used by: Command class hierarchy - used to describe individual commands (`74`_); Base Class Definitions (`1`_); pyweb.py (`148`_)
-
-
-UserIdXrefCommand class
-~~~~~~~~~~~~~~~~~~~~~~~
-
-A ``MacroXrefCommand`` is created by the ``WebReader`` when the 
-``@u`` command is found in the input stream.
-The Command is then appended to the current Chunk being built by the WebReader.
-
-The ``UserIdXrefCommand`` class weave method gets the
-user identifier cross reference information from the 
-overall web instance.  It then formats this line using the following 
-algorithm, which is similar to the algorithm in the ``XrefCommand`` superclass.
-
-1.  Use the ``Weaver`` class ``xrefHead()`` method to emit the cross-reference header.
-
-2.  Sort the keys in the cross-reference mapping.
-
-3.  Use the ``Weaver`` class ``xrefDefLine()`` method to emit each line of the cross-reference definition mapping.
-
-4.  Use the ``Weaver`` class ``xrefFoor()`` method to emit the cross-reference footer.
-
-
-
-..  _`83`:
-..  rubric:: UserIdXrefCommand class for a user identifier cross-reference (83) =
-..  parsed-literal::
-    :class: code
-
-    
-    class UserIdXrefCommand( XrefCommand ):
-        """A UserIdXref command."""
-        def weave( self, aWeb, aWeaver ):
-            """Weave a user identifier Xref from @d commands."""
-            ux= aWeb.userNamesXref()
-            if len(ux) == 0:
-                aWeaver.xrefEmpty()
-            else:
-                aWeaver.xrefHead()
-                for u in sorted(ux):
-                    defn, refList= ux[u]
-                    aWeaver.xrefDefLine( u, defn, refList )
-                aWeaver.xrefFoot()
-    
-
-..
-
-    ..  class:: small
-
-        |loz| *UserIdXrefCommand class for a user identifier cross-reference (83)*. Used by: Command class hierarchy - used to describe individual commands (`74`_); Base Class Definitions (`1`_); pyweb.py (`148`_)
-
-
-ReferenceCommand class
-~~~~~~~~~~~~~~~~~~~~~~~
-
-A ``ReferenceCommand`` instance is created by a ``WebReader`` when
-a ``@<``\ *name*\ ``@>`` construct in is found in the input stream.  This is attached
-to the current ``Chunk`` being built by the WebReader.  
- 
-
-During a weave, this creates a markup reference to
-another ``NamedChunk``.  During tangle, this actually includes the ``NamedChunk`` 
-at this point in the tangled output file.
-
-
-The constructor creates several attributes of an instance
-of a ``ReferenceCommand``.
-
-
-:refTo:
-    the name of the chunk to which this refers, possibly 
-    elided with a trailing ``'...'``.
-
-:fullName:
-    the full name of the chunk to which this refers.
-
-:chunkList:
-    the list of the chunks to which the name refers.
-
-
-
-..  _`84`:
-..  rubric:: ReferenceCommand class for chunk references (84) =
-..  parsed-literal::
-    :class: code
-
-    
-    class ReferenceCommand( Command ):
-        """A reference to a named chunk, via @."""
-        def \_\_init\_\_( self, refTo, fromLine=0 ):
-            Command.\_\_init\_\_( self, fromLine )
-            self.refTo= refTo
-            self.fullname= None
-            self.sequenceList= None
-            self.chunkList= []
-        def \_\_str\_\_( self ):
-            return "at {!r}: reference to chunk {!r}".format(self.lineNumber,self.refTo)
-        |srarr|\ ReferenceCommand resolve a referenced chunk name (`85`_)
-        |srarr|\ ReferenceCommand refers to a chunk (`86`_)
-        |srarr|\ ReferenceCommand weave a reference to a chunk (`87`_)
-        |srarr|\ ReferenceCommand tangle a referenced chunk (`88`_)
-    
-
-..
-
-    ..  class:: small
-
-        |loz| *ReferenceCommand class for chunk references (84)*. Used by: Command class hierarchy - used to describe individual commands (`74`_); Base Class Definitions (`1`_); pyweb.py (`148`_)
-
-
-The ``resolve()`` method queries the overall ``Web`` instance for the full
-name and sequence number for this chunk reference.  This is used
-by the ``Weaver`` class ``referenceTo()`` method to write the markup reference
-to the chunk.
-
-
-
-..  _`85`:
-..  rubric:: ReferenceCommand resolve a referenced chunk name (85) =
-..  parsed-literal::
-    :class: code
-
-    
-    def resolve( self, aWeb ):
-        """Expand the referenced chunk name into a full name and list of parts"""
-        self.fullName= aWeb.fullNameFor( self.refTo )
-        self.chunkList= [ c.seq for c in aWeb.getchunk( self.refTo ) ]
-    
-
-..
-
-    ..  class:: small
-
-        |loz| *ReferenceCommand resolve a referenced chunk name (85)*. Used by: ReferenceCommand class for chunk references (`84`_); Command class hierarchy - used to describe individual commands (`74`_); Base Class Definitions (`1`_); pyweb.py (`148`_)
-
-
-The ``ref()`` method is a request that is delegated by a ``Chunk``;
-it resolves the reference this Command makes within the containing Chunk.
-When the Chunk iterates through the Commands, it can accumulate a list of 
-Chinks to which it refers.
-
-
-
-..  _`86`:
-..  rubric:: ReferenceCommand refers to a chunk (86) =
-..  parsed-literal::
-    :class: code
-
-    
-    def ref( self, aWeb ):
-        """Find and return the full name for this reference."""
-        self.resolve( aWeb )
-        return self.fullName
-    
-
-..
-
-    ..  class:: small
-
-        |loz| *ReferenceCommand refers to a chunk (86)*. Used by: ReferenceCommand class for chunk references (`84`_); Command class hierarchy - used to describe individual commands (`74`_); Base Class Definitions (`1`_); pyweb.py (`148`_)
-
-
-The ``weave()`` method inserts a markup reference to a named
-chunk.  It uses the ``Weaver`` class ``referenceTo()`` method to format
-this appropriately for the document type being woven.
-
-
-
-..  _`87`:
-..  rubric:: ReferenceCommand weave a reference to a chunk (87) =
-..  parsed-literal::
-    :class: code
-
-    
-    def weave( self, aWeb, aWeaver ):
-        """Create the nicely formatted reference to a chunk of code."""
-        self.resolve( aWeb )
-        aWeb.weaveChunk( self.fullName, aWeaver )
-    
-
-..
-
-    ..  class:: small
-
-        |loz| *ReferenceCommand weave a reference to a chunk (87)*. Used by: ReferenceCommand class for chunk references (`84`_); Command class hierarchy - used to describe individual commands (`74`_); Base Class Definitions (`1`_); pyweb.py (`148`_)
-
-
-The ``tangle()`` method inserts the resolved chunk in this
-place.  When a chunk is tangled, it sets the indent,
-inserts the chunk and resets the indent.
-
-
-
-..  _`88`:
-..  rubric:: ReferenceCommand tangle a referenced chunk (88) =
-..  parsed-literal::
-    :class: code
-
-    
-    def tangle( self, aWeb, aTangler ):
-        """Create source code."""
-        self.resolve( aWeb )
-        # Update indent based on last line of previous command. 
-        if self.chunk is None or self.chunk.previous\_command is None:
-            self.logger.error( "Command disconnected from Chunk." )
-            raise Error( "Serious problem in WebReader." )
-        self.logger.debug( "Indent {!r} + {!r}".format(aTangler.context, self.chunk.previous\_command.indent()) )
-        aTangler.setIndent( self.chunk.previous\_command.indent() )
-        aWeb.tangleChunk( self.fullName, aTangler )
-        aTangler.clrIndent()
-    
-
-..
-
-    ..  class:: small
-
-        |loz| *ReferenceCommand tangle a referenced chunk (88)*. Used by: ReferenceCommand class for chunk references (`84`_); Command class hierarchy - used to describe individual commands (`74`_); Base Class Definitions (`1`_); pyweb.py (`148`_)
-
-
-Reference Strategy
----------------------------------
-
-The Reference Strategy has two implementations.  An instance
-of this is injected into each Chunk by the Web.  The transitive closure
-of references requires walking through the web.  By injecting this
-algorithm, we assure that
-that (1) each Chunk can produce all relevant information and (2) a
-simple configuration change can be applied to the document.
-
-Reference Superclass
-~~~~~~~~~~~~~~~~~~~~~
-
-The superclass is an abstract class that defines the interface for
-this object.
-
-
-
-..  _`89`:
-..  rubric:: Reference class hierarchy - references to a chunk (89) =
-..  parsed-literal::
-    :class: code
-
-    
-    class Reference:
-        def \_\_init\_\_( self, aWeb ):
-            self.web = aWeb
-            self.logger= logging.getLogger( self.\_\_class\_\_.\_\_qualname\_\_ )
-        def chunkReferencedBy( self, aChunk ):
-            """Return a list of Chunks."""
-            pass
-
-..
-
-    ..  class:: small
-
-        |loz| *Reference class hierarchy - references to a chunk (89)*. Used by: Base Class Definitions (`1`_); pyweb.py (`148`_)
-
-
-SimpleReference Class
-~~~~~~~~~~~~~~~~~~~~~
-
-The SimpleReference subclass does the simplest version of resolution.
-
-    **TODO** Returns the chunks, not a sequence of (chunk name, sequence)
-    pairs
-    
-
-..  _`90`:
-..  rubric:: Reference class hierarchy - references to a chunk (90) +=
-..  parsed-literal::
-    :class: code
-
-    
-    class SimpleReference( Reference ):
-        def \_\_init\_\_( self, aWeb ):
-            super().\_\_init\_\_( aWeb )
-        def chunkReferencedBy( self, aChunk ):
-            """:todo: Return the chunks themselves."""
-            refBy= aChunk.referencedBy
-            return [ (c.fullName, c.seq) for c in refBy ]
-
-..
-
-    ..  class:: small
-
-        |loz| *Reference class hierarchy - references to a chunk (90)*. Used by: Base Class Definitions (`1`_); pyweb.py (`148`_)
-
-
-TransitiveReference Class
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-The TransitiveReference subclass does a transitive closure of all
-references to this Chunk.
-
-    **TODO** Returns the chunks, not a sequence of (chunk name, sequence)
-    pairs
-
-
-..  _`91`:
-..  rubric:: Reference class hierarchy - references to a chunk (91) +=
-..  parsed-literal::
-    :class: code
-
-    
-    class TransitiveReference( Reference ):
-        def \_\_init\_\_( self, aWeb ):
-            super().\_\_init\_\_( aWeb )
-        def chunkReferencedBy( self, aChunk ):
-            """:todo: Return the chunks themselves."""
-            refBy= aChunk.referencedBy
-            self.logger.debug( "References: {:s}({:d}) {!r}".format(aChunk.name, aChunk.seq, refBy) )
-            closure= self.allParentsOf( refBy )
-            return [ (c.fullName, c.seq) for c in closure ]
-        def allParentsOf( self, chunkList, depth=0 ):
-            """Transitive closure of parents.
-            :todo: Return the chunks themselves.
-            """
-            final = []
-            for c in chunkList:
-                final.append( c )
-                final.extend( self.allParentsOf( c.referencedBy, depth+1 ) )
-            self.logger.debug( "References: {0:>{indent}s} {1:s}".format('--', final, indent=2\*depth) )
-            return final
-
-..
-
-    ..  class:: small
-
-        |loz| *Reference class hierarchy - references to a chunk (91)*. Used by: Base Class Definitions (`1`_); pyweb.py (`148`_)
-
-
-
-Error class
-------------
-
-An ``Error`` is raised whenever processing cannot continue.  Since it
-is a subclass of Exception, it takes an arbitrary number of arguments.  The
-first should be the basic message text.  Subsequent arguments provide 
-additional details.  We will try to be sure that
-all of our internal exceptions reference a specific chunk, if possible.
-This means either including the chunk as an argument, or catching the 
-exception and appending the current chunk to the exception's arguments.
-
-The
-Python ``raise`` statement takes an instance of Error and passes it
-to the enclosing ``try/except`` statement for processing.
-
-The typical creation is as follows:
-
-..  parsed-literal::
-
-    raise Error("No full name for {!r}".format(chunk.name), chunk)
-
-A typical exception-handling suite might look like this:
-
-..  parsed-literal::
-
-    try:
-        *...something that may raise an Error or Exception...*
-    except Error as e:
-        print( e.args ) # this is a pyWeb internal Error
-    except Exception as w:
-        print( w.args ) # this is some other Python Exception
-
-The ``Error`` class is a subclass of ``Exception`` used to differentiate 
-application-specific
-exceptions from other Python exceptions.  It does no additional processing,
-but merely creates a distinct class to facilitate writing ``except`` statements.
-
-
-
-..  _`92`:
-..  rubric:: Error class - defines the errors raised (92) =
-..  parsed-literal::
-    :class: code
-
-    
-    class Error( Exception ): pass
-
-..
-
-    ..  class:: small
-
-        |loz| *Error class - defines the errors raised (92)*. Used by: Base Class Definitions (`1`_); pyweb.py (`148`_)
-
-
-The Web Class
---------------
-
-The overall web of chunks is carried in a 
-single instance of the ``Web`` class that drives the weaving and tangling actions.  
-Broadly, the functionality of a Web can be separated into several areas.
-Fundamentally, a Web is a hybrid list-dictionary.  It's a list of chunks that also offers a 
-moderately sophisticated
-lookup, including exact match for a chunk name and an approximate match for a chunk name. It's a
-dictionary that also retains anonymous chunks in order.
-
-Additionally, there are some methods that can be refactored into the ``WebReader`` for 
-resolve references among chunks.
-
--   construction methods used by ``Chunks`` and ``WebReader``
-
--   ``Chunk`` name resolution methods
-
--   enrichment of the web, once all the Chunks are known; 
-    each Chunk is updated with Chunk references it makes as well as Chunks which reference it.
-
--   ``Chunk`` cross reference methods
-
--   miscellaneous access
-
--   tangle
-
--   weave
-
-
-A web instance has a number of attributes.
-
-:webFileName:
-    the name of the original .w file.
-
-:chunkSeq:
-    the sequence of ``Chunk`` instances as seen in the input file.
-    To support anonymous chunks, and to assure that the original input document order
-    is preserved, we keep all chunks in a master sequential list.
-
-:output:
-    the ``@o`` named ``OutputChunk`` chunks.  
-    Each element of this  dictionary is a sequence of chunks that have the same name. 
-    The first is the initial definition (marked with "="), all others a second definitions
-    (marked with "+=").
-
-:named:
-    the ``@d`` named ``NamedChunk`` chunks.  Each element of this 
-    dictionary is a sequence of chunks that have the same name.  The first is the
-    initial definition (marked with "="), all others a second definitions
-    (marked with "+=").
-
-:usedBy:
-    the cross reference of chunks referenced by commands in other
-    chunks.
-
-:sequence:
-    is used to assign a unique sequence number to each
-    named chunk.
-
-:reference_style:
-    Either an instance of ``TransitiveReference(self)`` or ``SimpleReference(self)``
-    
-
-..  _`93`:
-..  rubric:: Web class - describes the overall "web" of chunks (93) =
-..  parsed-literal::
-    :class: code
-
-    
-    class Web:
-        """The overall Web of chunks."""
-        def \_\_init\_\_( self ):
-            self.webFileName= None
-            self.chunkSeq= [] 
-            self.output= {} # Map filename to Chunk
-            self.named= {} # Map chunkname to Chunk
-            self.sequence= 0
-            self.reference\_style = TransitiveReference(self) # or SimpleReference(self)
-            self.logger= logging.getLogger( self.\_\_class\_\_.\_\_qualname\_\_ )
-        def \_\_str\_\_( self ):
-            return "Web {!r}".format( self.webFileName, )
-        |srarr|\ Web construction methods used by Chunks and WebReader (`94`_)
-        |srarr|\ Web Chunk name resolution methods (`99`_), |srarr|\ (`100`_)
-        |srarr|\ Web Chunk cross reference methods (`101`_), |srarr|\ (`103`_), |srarr|\ (`104`_), |srarr|\ (`105`_)
-        |srarr|\ Web determination of the language from the first chunk (`108`_)
-        |srarr|\ Web tangle the output files (`109`_)
-        |srarr|\ Web weave the output document (`110`_)
-    
-
-..
-
-    ..  class:: small
-
-        |loz| *Web class - describes the overall "web" of chunks (93)*. Used by: Base Class Definitions (`1`_); pyweb.py (`148`_)
-
-
-During web construction, it is convenient to capture
-information about the individual ``Chunk`` instances being appended to
-the web.  This done using a *Callback* design pattern.
-Each subclass of ``Chunk`` provides an override for the ``Chunk`` class
-``webAdd()`` method.  This override calls one of the appropriate
-web construction methods.
-
-Also note that the full name for a chunk can be given
-either as part of the definition, or as part a reference.
-Typically, the first reference has the full name and the definition
-has the elided name.  This allows a reference to a chunk
-to contain a more complete description of the chunk.
-
-
-
-..  _`94`:
-..  rubric:: Web construction methods used by Chunks and WebReader (94) =
-..  parsed-literal::
-    :class: code
-
-    
-    |srarr|\ Web add full chunk names, ignoring abbreviated names (`95`_)
-    |srarr|\ Web add an anonymous chunk (`96`_)
-    |srarr|\ Web add a named macro chunk (`97`_)
-    |srarr|\ Web add an output file definition chunk (`98`_)
-
-..
-
-    ..  class:: small
-
-        |loz| *Web construction methods used by Chunks and WebReader (94)*. Used by: Web class - describes the overall "web" of chunks (`93`_); Base Class Definitions (`1`_); pyweb.py (`148`_)
-
-
-A name is only added to the known names when it is
-a full name, not an abbreviation ending with ``"..."``.
-Abbreviated names are quietly skipped until the full name
-is seen.
-
-
-The algorithm for the ``addDefName()`` method, then is as follows:
-
-1.  Use the ``fullNameFor()`` method to locate the full name.
-
-2.  If no full name was found (the result of ``fullNameFor()`` ends with ``'...'``), 
-    ignore this name as an abbreviation with no definition.
-
-3.  If this is a full name and the name was not in the  *named* mapping, add this full name to the mapping.
-
-
-
-This name resolution approach presents a problem when a chunk is
-defined before it is referenced and the first definition
-uses an abbreviated name.  This is an atypical construction
-of an input document, however, since the intent is to provide
-high-level summaries that have forward references to supporting
-details.
-
-
-
-..  _`95`:
-..  rubric:: Web add full chunk names, ignoring abbreviated names (95) =
-..  parsed-literal::
-    :class: code
-
-    
-    def addDefName( self, name ):
-        """Reference to or definition of a chunk name."""
-        nm= self.fullNameFor( name )
-        if nm is None: return None
-        if nm[-3:] == '...':
-            self.logger.debug( "Abbreviated reference {!r}".format(name) )
-            return None # first occurance is a forward reference using an abbreviation
-        if nm not in self.named:
-            self.named[nm]= []
-            self.logger.debug( "Adding empty chunk {!r}".format(name) )
-        return nm
-    
-
-..
-
-    ..  class:: small
-
-        |loz| *Web add full chunk names, ignoring abbreviated names (95)*. Used by: Web construction methods used by Chunks and WebReader (`94`_); Web class - describes the overall "web" of chunks (`93`_); Base Class Definitions (`1`_); pyweb.py (`148`_)
-
-
-An anonymous ``Chunk`` is kept in a sequence of chunks, used for
-tangling.
-
-
-
-..  _`96`:
-..  rubric:: Web add an anonymous chunk (96) =
-..  parsed-literal::
-    :class: code
-
-    
-    def add( self, chunk ):
-        """Add an anonymous chunk."""
-        self.chunkSeq.append( chunk )
-    
-
-..
-
-    ..  class:: small
-
-        |loz| *Web add an anonymous chunk (96)*. Used by: Web construction methods used by Chunks and WebReader (`94`_); Web class - describes the overall "web" of chunks (`93`_); Base Class Definitions (`1`_); pyweb.py (`148`_)
-
-
-A named ``Chunk`` is defined with a ``@d`` command.
-It is collected into a mapping of ``NamedChunk`` instances.
-An entry in the mapping is a sequence of chunks that have the
-same name.  This sequence of chunks is used to produce the
-weave or tangle output.
-
-
-All chunks are also placed in the overall sequence of chunks.
-This overall sequence is used for weaving the document.
-
-
-The ``addDefName()`` method is used to resolve this name if
-it is an abbreviation, or add it to the mapping if this
-is the first occurance of the name.  If the name cannot be
-added, an instance of our ``Error`` class is raised.  If the name exists or 
-was added, the chunk is appended to the chunk list associated
-with this name.
-
-
-The web's sequence counter is incremented, and this 
-unique sequence number sets the  *seq* attribute of the ``Chunk``.
-If the chunk list was empty, this is the first chunk, the
-*initial* flag is set to True when there's only one element
-in the list.  Otherwise, it's false.
-
-
-
-..  _`97`:
-..  rubric:: Web add a named macro chunk (97) =
-..  parsed-literal::
-    :class: code
-
-    
-    def addNamed( self, chunk ):
-        """Add a named chunk to a sequence with a given name."""
-        chunk.reference\_style= self.reference\_style
-        self.chunkSeq.append( chunk )
-        nm= self.addDefName( chunk.name )
-        if nm:
-            # We found the full name for this chunk
-            self.sequence += 1
-            chunk.seq= self.sequence
-            chunk.fullName= nm
-            self.named[nm].append( chunk )
-            chunk.initial= len(self.named[nm]) == 1
-            self.logger.debug( "Extending chunk {!r} from {!r}".format(nm, chunk.name) )
-        else:
-            raise Error("No full name for {!r}".format(chunk.name), chunk)
-    
-
-..
-
-    ..  class:: small
-
-        |loz| *Web add a named macro chunk (97)*. Used by: Web construction methods used by Chunks and WebReader (`94`_); Web class - describes the overall "web" of chunks (`93`_); Base Class Definitions (`1`_); pyweb.py (`148`_)
-
-
-An output file definition ``Chunk`` is defined with an ``@o``
-command.  It is collected into a mapping of ``OutputChunk`` instances.
-An entry in the mapping is a sequence of chunks that have the
-same name.  This sequence of chunks is used to produce the
-weave or tangle output.
-
-
-Note that file names cannot be abbreviated.
-
-All chunks are also placed in overall sequence of chunks.
-This overall sequence is used for weaving the document.
-
-
-If the name does not exist in the *output* mapping,
-the name is added with an empty sequence of chunks.
-In all cases, the chunk is 
-appended to the chunk list associated
-with this name.
-
-
-The web's sequence counter is incremented, and this 
-unique sequence number sets the Chunk's *seq* attribute.
-If the chunk list was empty, this is the first chunk, the
-*initial* flag is True if this is the first chunk.
-
-
-
-
-..  _`98`:
-..  rubric:: Web add an output file definition chunk (98) =
-..  parsed-literal::
-    :class: code
-
-    
-    def addOutput( self, chunk ):
-        """Add an output chunk to a sequence with a given name."""
-        chunk.reference\_style= self.reference\_style
-        self.chunkSeq.append( chunk )
-        if chunk.name not in self.output:
-            self.output[chunk.name] = []
-            self.logger.debug( "Adding chunk {!r}".format(chunk.name) )
-        self.sequence += 1
-        chunk.seq= self.sequence
-        chunk.fullName= chunk.name
-        self.output[chunk.name].append( chunk )
-        chunk.initial = len(self.output[chunk.name]) == 1
-    
-
-..
-
-    ..  class:: small
-
-        |loz| *Web add an output file definition chunk (98)*. Used by: Web construction methods used by Chunks and WebReader (`94`_); Web class - describes the overall "web" of chunks (`93`_); Base Class Definitions (`1`_); pyweb.py (`148`_)
-
-
-Web chunk name resolution has three aspects.  The first
-is resolving elided names (those ending with ``...``) to their
-actual full names.  The second is finding the named chunk
-in the web structure.  The third is returning a reference
-to a specific chunk including the name and sequence number.
-
-Note that a chunk name actually refers to a sequence
-of chunks.  Multiple definitions for a chunk are allowed, and
-all of the definitions are concatenated to create the complete
-chunk.  This complexity makes it unwise to return the sequence
-of same-named chunks; therefore, we put the burden on the Web to 
-process all chunks with a given name, in sequence.
-
-The ``fullNameFor()`` method resolves full name for a chunk as follows:
-
-1.  If the string is already in the *named* mapping, this is the full name
-
-2.  If the string ends in ``'...'``, visit each key in the dictionary to see if the key starts with the string up to the trailing ``'...'``.  If a match is found, the dictionary key is the full name.
-
-3.  Otherwise, treat this as a full name.
-
-
-
-..  _`99`:
-..  rubric:: Web Chunk name resolution methods (99) =
-..  parsed-literal::
-    :class: code
-
-    
-    def fullNameFor( self, name ):
-        """Resolve "..." names into the full name."""
-        if name in self.named: return name
-        if name[-3:] == '...':
-            best= [ n for n in self.named.keys()
-                if n.startswith( name[:-3] ) ]
-            if len(best) > 1:
-                raise Error("Ambiguous abbreviation {!r}, matches {!r}".format( name, list(sorted(best)) ) )
-            elif len(best) == 1: 
-                return best[0]
-        return name
-    
-
-..
-
-    ..  class:: small
-
-        |loz| *Web Chunk name resolution methods (99)*. Used by: Web class - describes the overall "web" of chunks (`93`_); Base Class Definitions (`1`_); pyweb.py (`148`_)
-
-
-The ``getchunk()`` method locates a named sequence of chunks by first determining the full name
-for the identifying string.  If full name is in the *named* mapping, the sequence
-of chunks is returned.  Otherwise, an instance of our ``Error`` class is raised because the name
-is unresolvable.
-
-
-It might be more helpful for debugging to emit this as an error in the
-weave and tangle results and keep processing.  This would allow an author to
-catch multiple errors in a single run of pyWeb.
- 
-
-..  _`100`:
-..  rubric:: Web Chunk name resolution methods (100) +=
-..  parsed-literal::
-    :class: code
-
-    
-    def getchunk( self, name ):
-        """Locate a named sequence of chunks."""
-        nm= self.fullNameFor( name )
-        if nm in self.named:
-            return self.named[nm]
-        raise Error( "Cannot resolve {!r} in {!r}".format(name,self.named.keys()) )
-    
-
-..
-
-    ..  class:: small
-
-        |loz| *Web Chunk name resolution methods (100)*. Used by: Web class - describes the overall "web" of chunks (`93`_); Base Class Definitions (`1`_); pyweb.py (`148`_)
-
-
-Cross-reference support includes creating and reporting
-on the various cross-references available in a web.  This includes
-creating the list of chunks that reference a given chunk;
-and returning the file, macro and user identifier cross references.
-
-
-Each ``Chunk`` has a list ``Reference`` commands that shows the chunks
-to which a chunk refers.  These relationships must be reversed to show
-the chunks that refer to a given chunk.  This is done by traversing
-the entire web of named chunks and recording each chunk-to-chunk reference.
-This mapping has the referred-to chunk as 
-the key, and a sequence of referring chunks as the value.
-
-
-The accumulation is initiated by the web's ``createUsedBy()`` method.  This
-method visits a ``Chunk``, calling the ``genReferences()`` method, 
-passing in the ``Web`` instance
-as an argument.  Each ``Chunk`` class ``genReferences()`` method, in turn, 
-invokes the ``usedBy()`` method
-of each ``Command`` instance in the chunk.  Most commands do nothing, 
-but a ``ReferenceCommand``
-will resolve the name to which it refers.
-
-
-When the ``createUsedBy()`` method has accumulated the entire cross 
-reference, it also assures that all chunks are used exactly once.
-
-
-..  _`101`:
-..  rubric:: Web Chunk cross reference methods (101) =
-..  parsed-literal::
-    :class: code
-
-    
-    def createUsedBy( self ):
-        """Update every piece of a Chunk to show how the chunk is referenced.
-        Each piece can then report where it's used in the web.
-        """
-        for aChunk in self.chunkSeq:
-            #usage = (self.fullNameFor(aChunk.name), aChunk.seq)
-            for aRefName in aChunk.genReferences( self ):
-                for c in self.getchunk( aRefName ):
-                    c.referencedBy.append( aChunk )
-                    c.refCount += 1
-        |srarr|\ Web Chunk check reference counts are all one (`102`_)
-    
-
-..
-
-    ..  class:: small
-
-        |loz| *Web Chunk cross reference methods (101)*. Used by: Web class - describes the overall "web" of chunks (`93`_); Base Class Definitions (`1`_); pyweb.py (`148`_)
-
-
-We verify that the reference count for a
-chunk is exactly one.  We don't gracefully tolerate multiple references to
-a chunk or unreferenced chunks.
-
-
-..  _`102`:
-..  rubric:: Web Chunk check reference counts are all one (102) =
-..  parsed-literal::
-    :class: code
-
-    
-    for nm in self.no\_reference():
-        self.logger.warn( "No reference to {!r}".format(nm) )
-    for nm in self.multi\_reference():
-        self.logger.warn( "Multiple references to {!r}".format(nm) )
-    for nm in self.no\_definition():
-        self.logger.warn( "No definition for {!r}".format(nm) )
-
-..
-
-    ..  class:: small
-
-        |loz| *Web Chunk check reference counts are all one (102)*. Used by: Web Chunk cross reference methods (`101`_); Web class - describes the overall "web" of chunks (`93`_); Base Class Definitions (`1`_); pyweb.py (`148`_)
-
-
-The one-pass version
-
-..  parsed-literal::
-
-    for nm,cl in self.named.items():
-        if len(cl) > 0:
-            if cl[0].refCount == 0:
-               self.logger.warn( "No reference to {!r}".format(nm) )
-            elif cl[0].refCount > 1:
-               self.logger.warn( "Multiple references to {!r}".format(nm) )
-        else:
-            self.logger.warn( "No definition for {!r}".format(nm) )
-
-
-We use three methods to filter chunk names into 
-the various warning categories.  The ``no_reference`` list
-is a list of chunks defined by never referenced.
-The ``multi_reference`` list
-is a list of chunks defined by never referenced.
-The ``no_definition`` list
-is a list of chunks referenced but not defined.
-
-
-
-..  _`103`:
-..  rubric:: Web Chunk cross reference methods (103) +=
-..  parsed-literal::
-    :class: code
-
-    
-    def no\_reference( self ):
-        return [ nm for nm,cl in self.named.items() if len(cl)>0 and cl[0].refCount == 0 ]
-    def multi\_reference( self ):
-        return [ nm for nm,cl in self.named.items() if len(cl)>0 and cl[0].refCount > 1 ]
-    def no\_definition( self ):
-        return [ nm for nm,cl in self.named.items() if len(cl) == 0 ] 
-    
-
-..
-
-    ..  class:: small
-
-        |loz| *Web Chunk cross reference methods (103)*. Used by: Web class - describes the overall "web" of chunks (`93`_); Base Class Definitions (`1`_); pyweb.py (`148`_)
-
-
-The ``fileXref()`` method visits all named file output chunks in *output* and
-collects the sequence numbers of each section in the sequence of chunks.
-
-
-The ``chunkXref()`` method uses the same algorithm as a the ``fileXref()`` method,
-but applies it to the *named* mapping.
-
-
-
-..  _`104`:
-..  rubric:: Web Chunk cross reference methods (104) +=
-..  parsed-literal::
-    :class: code
-
-    
-    def fileXref( self ):
-        fx= {}
-        for f,cList in self.output.items():
-            fx[f]= [ c.seq for c in cList ]
-        return fx
-    def chunkXref( self ):
-        mx= {}
-        for n,cList in self.named.items():
-            mx[n]= [ c.seq for c in cList ]
-        return mx
-    
-
-..
-
-    ..  class:: small
-
-        |loz| *Web Chunk cross reference methods (104)*. Used by: Web class - describes the overall "web" of chunks (`93`_); Base Class Definitions (`1`_); pyweb.py (`148`_)
-
-
-The ``userNamesXref()`` method creates a mapping for each
-user identifier.  The value for this mapping is a tuple
-with the chunk that defined the identifer (via a ``@|`` command), 
-and a sequence of chunks that reference the identifier. 
-
-
-For example:
-``{ 'Web': ( 87, (88,93,96,101,102,104) ), 'Chunk': ( 53, (54,55,56,60,57,58,59) ) }``, 
-shows that the identifier
-``'Web'`` is defined in chunk with a sequence number of 87, and referenced
-in the sequence of chunks that follow.
-
-
-This works in two passes:
-
-1.  ``_gatherUserId()`` gathers all user identifiers
-
-2.  ``_updateUserId()`` searches all text commands for the identifiers 
-    and updates the ``Web`` class cross reference information.
-
-
-
-
-..  _`105`:
-..  rubric:: Web Chunk cross reference methods (105) +=
-..  parsed-literal::
-    :class: code
-
-    
-    def userNamesXref( self ):
-        ux= {}
-        self.\_gatherUserId( self.named, ux )
-        self.\_gatherUserId( self.output, ux )
-        self.\_updateUserId( self.named, ux )
-        self.\_updateUserId( self.output, ux )
-        return ux
-    def \_gatherUserId( self, chunkMap, ux ):
-        |srarr|\ collect all user identifiers from a given map into ux (`106`_)
-    def \_updateUserId( self, chunkMap, ux ):
-        |srarr|\ find user identifier usage and update ux from the given map (`107`_)
-    
-
-..
-
-    ..  class:: small
-
-        |loz| *Web Chunk cross reference methods (105)*. Used by: Web class - describes the overall "web" of chunks (`93`_); Base Class Definitions (`1`_); pyweb.py (`148`_)
-
-
-User identifiers are collected by visiting each of the sequence of 
-``Chunks`` that share the
-same name; within each component chunk, if chunk has identifiers assigned
-by the ``@|`` command, these are seeded into the dictionary.
-If the chunk does not permit identifiers, it simply returns an empty
-list as a default action.
-
- 
-
-..  _`106`:
-..  rubric:: collect all user identifiers from a given map into ux (106) =
-..  parsed-literal::
-    :class: code
-
-    
-    for n,cList in chunkMap.items():
-        for c in cList:
-            for id in c.getUserIDRefs():
-                ux[id]= ( c.seq, [] )
-
-..
-
-    ..  class:: small
-
-        |loz| *collect all user identifiers from a given map into ux (106)*. Used by: Web Chunk cross reference methods (`105`_); Web class - describes the overall "web" of chunks (`93`_); Base Class Definitions (`1`_); pyweb.py (`148`_)
-
-
-User identifiers are cross-referenced by visiting 
-each of the sequence of ``Chunks`` that share the
-same name; within each component chunk, visit each user identifier;
-if the ``Chunk`` class ``searchForRE()`` method matches an identifier, 
-this is appended to the sequence of chunks that reference the original user identifier.
-
-
-
-..  _`107`:
-..  rubric:: find user identifier usage and update ux from the given map (107) =
-..  parsed-literal::
-    :class: code
-
-    
-    # examine source for occurances of all names in ux.keys()
-    for id in ux.keys():
-        self.logger.debug( "References to {!r}".format(id) )
-        idpat= re.compile( r'\\W{:s}\\W'.format(id) )
-        for n,cList in chunkMap.items():
-            for c in cList:
-                if c.seq != ux[id][0] and c.searchForRE( idpat ):
-                    ux[id][1].append( c.seq )
-
-..
-
-    ..  class:: small
-
-        |loz| *find user identifier usage and update ux from the given map (107)*. Used by: Web Chunk cross reference methods (`105`_); Web class - describes the overall "web" of chunks (`93`_); Base Class Definitions (`1`_); pyweb.py (`148`_)
-
-
-The ``language()`` method makes a stab at determining the output language.
-The determination of the language can be done a variety of ways.
-One is to use command line parameters, another is to use the filename
-extension on the input file.
-
-We examine the first few characters of input.  A proper HTML, XHTML or
-XML file begins with '>> pat.split( "@{hi mom@}")
-    ['', '@{', 'hi mom', '@}', '']
-    
-We can safely filter these via a generator expression.
-
-Since the tokenizer is a proper iterator, we can use ``tokens= iter(Tokenizer(source))``
-and ``next(tokens)`` to step through the sequence of tokens until we raise a StopIteration
-exception.
-
-
-..  _`111`:
-..  rubric:: Tokenizer class - breaks input into tokens (111) =
-..  parsed-literal::
-    :class: code
-
-    
-    class Tokenizer:
-        def \_\_init\_\_( self, stream, command\_char='@' ):
-            self.command= command\_char
-            self.parsePat= re.compile( r'({:s}.\|\\n)'.format(self.command) )
-            self.token\_iter= (t for t in self.parsePat.split( stream.read() ) if len(t) != 0)
-            self.lineNumber= 0
-        def \_\_next\_\_( self ):
-            token= next(self.token\_iter)
-            self.lineNumber += token.count('\\n')
-            return token
-        def \_\_iter\_\_( self ):
-            return self
-    
-
-..
-
-    ..  class:: small
-
-        |loz| *Tokenizer class - breaks input into tokens (111)*. Used by: Base Class Definitions (`1`_); pyweb.py (`148`_)
-
-
-The WebReader Class
-~~~~~~~~~~~~~~~~~~~~~~
-
-There are two forms of the constructor for a ``WebReader``.  The 
-initial ``WebReader`` instance is created with code like the following:
-
-
-..  parsed-literal::
-
-    p= WebReader( aFileName, command=aCommandCharacter )
-
-
-
-This will define the initial input file and the command character, both
-of which are command-line parameters to the application.
-
-When processing an include file (with the ``@i`` command), a child ``WebReader``
-instance is created with code like the following:
-
-
-..  parsed-literal::
-
-    c= WebReader( anIncludeName, parent=parentWebReader )
-
-
-
-This will define the included file, but will inherit the command 
-character from the parent ``WebReader``.  This will also include a 
-reference from child to parent so that embedded Python expressions
-can view the entire input context.
-
-
-The ``WebReader`` class parses the input file into command blocks.
-These are assembled into ``Chunks``, and the ``Chunks`` are assembled into the document
-``Web``.  Once this input pass is complete, the resulting ``Web`` can be tangled or
-woven.
-
-
-"Major" commands define the structure of the ``Chunks``.  The major structural commands 
-are ``@d`` and ``@o``, as well as the ``@{``, ``@}``, ``@[``, ``@]`` brackets, 
-and the ``@i`` command to include another file.
-
-
-"Minor" commands are inline within a ``Chunk``: they define internal ``Commands``.  
-Blocks of text are minor commands, as well as the ``@<``\ *name*\ ``@>`` references, 
-the various cross-reference commands (``@f``, ``@m`` and ``@u``).  
-The ``@@`` escape is also
-handled here so that all further processing is independent of any parsing.
-
-
-The class has the following attributes:
-
-:fileName:
-    is used to pass the file name to the Web instance.
-
-:tokenList:
-    is the completely tokenized input file.
-
-:token:
-    is the most recently examined token.
-
-:lineNumber:
-    is the count of ``'\n'`` characters seen in the tokens.
-
-:aChunk:
-    is the current open Chunk.
-
-:parent:
-    is the outer ``WebReader`` when processing a ``@i`` command.
-
-:theWeb:
-    is the current open Web.
-
-:permitList:
-    is the list of commands that are permitted to fail.  This is generally 
-    an empty list or ``('@i',)``.
-
-:command:
-    is the command character; a WebReader will use the parent command 
-    character if the parent is not ``None``.
-
-:parsePat:
-    is generated from the command character, and is used to parse the input into tokens.
-
-
-
-..  _`112`:
-..  rubric:: WebReader class - parses the input file, building the Web structure (112) =
-..  parsed-literal::
-    :class: code
-
-    
-    class WebReader:
-        """Parse an input file, creating Commands and Chunks."""
-        def \_\_init\_\_( self, parent=None, command='@', permit=None ):
-            # Configuration of this reader.
-            self.\_source= None
-            self.fileName= None
-            self.parent= parent
-            self.theWeb= None
-            if self.parent: 
-                self.command= self.parent.command
-                self.permitList= self.parent.permitList
-            else:
-                self.command= command
-                self.permitList= [] if permit is None else permit
-                
-            self.logger= logging.getLogger( self.\_\_class\_\_.\_\_qualname\_\_ )
-    
-            # State of reading and parsing.
-            self.tokenizer= None
-            self.aChunk= None
-            # Summary
-            self.totalLines= 0
-            self.totalFiles= 0
-            |srarr|\ WebReader command literals (`130`_)
-        def \_\_str\_\_( self ):
-            return self.\_\_class\_\_.\_\_name\_\_
-        |srarr|\ WebReader fluent setter methods (`128`_)
-        |srarr|\ WebReader location in the input stream (`127`_)
-        |srarr|\ WebReader load the web (`129`_)
-        |srarr|\ WebReader handle a command string (`113`_), |srarr|\ (`126`_)
-
-..
-
-    ..  class:: small
-
-        |loz| *WebReader class - parses the input file, building the Web structure (112)*. Used by: Base Class Definitions (`1`_); pyweb.py (`148`_)
-
-
-Command recognition is done via a **Chain of Command**-like design.
-There are two conditions: the command string is recognized or it is not recognized.
-If the command is recognized, ``handleCommand()`` either:
-
-    -   (for major commands) attaches the current ``Chunk`` (*self.aChunk*) to the 
-        current ``Web`` (*self.aWeb*), **or**
-
-    -   (for minor commands) create a ``Command``, attach it to the current 
-        ``Chunk`` (*self.aChunk*)
-
-and returns a true result.
-
-If the command is not recognized, ``handleCommand()`` returns false.
-
-
-A subclass can override ``handleCommand()`` to (1) call this superclass version;
-(2) if the command is unknown to the superclass, 
-then the subclass can attempt to process it;
-(3) if the command is unknown to both classes, 
-then return false.  Either a subclass will handle it, or the default activity taken
-by ``load()`` is to treat the command a text, but also issue a warning.
-
-
-
-..  _`113`:
-..  rubric:: WebReader handle a command string (113) =
-..  parsed-literal::
-    :class: code
-
-    
-    def handleCommand( self, token ):
-        self.logger.debug( "Reading {!r}".format(token) )
-        |srarr|\ major commands segment the input into separate Chunks (`114`_)
-        |srarr|\ minor commands add Commands to the current Chunk (`120`_)
-        elif token[:2] in (self.cmdlcurl,self.cmdlbrak):
-            # These should be consumed as part of @o and @d parsing
-            raise Error('Extra {!r} (possibly missing chunk name)'.format(token), self.aChunk)
-        else:
-            return None # did not recogize the command
-        return True # did recognize the command
-    
-
-..
-
-    ..  class:: small
-
-        |loz| *WebReader handle a command string (113)*. Used by: WebReader class - parses the input file, building the Web structure (`112`_); Base Class Definitions (`1`_); pyweb.py (`148`_)
-
-
-The following sequence of ``if``-``elif`` statements identifies
-the major commands that partition the input into separate ``Chunks``.
-
-
-..  _`114`:
-..  rubric:: major commands segment the input into separate Chunks (114) =
-..  parsed-literal::
-    :class: code
-
-    
-    if token[:2] == self.cmdo:
-        |srarr|\ start an OutputChunk, adding it to the web (`116`_)
-    elif token[:2] == self.cmdd:
-        |srarr|\ start a NamedChunk or NamedDocumentChunk, adding it to the web (`117`_)
-    elif token[:2] == self.cmdi:
-        |srarr|\ import another file (`118`_)
-    elif token[:2] in (self.cmdrcurl,self.cmdrbrak):
-        |srarr|\ finish a chunk, start a new Chunk adding it to the web (`119`_)
-
-..
-
-    ..  class:: small
-
-        |loz| *major commands segment the input into separate Chunks (114)*. Used by: WebReader handle a command string (`113`_); WebReader class - parses the input file, building the Web structure (`112`_); Base Class Definitions (`1`_); pyweb.py (`148`_)
-
-
-An output chunk has the form ``@o`` *name* ``@{`` *content* ``@}``.
-We use the first two tokens to name the ``OutputChunk``.  We simply expect
-the ``@{`` separator.  We then attach all subsequent commands
-to this chunk while waiting for the final ``@}`` token to end the chunk.
-
-
-    **TODO** The file name information can be split into parts on a ``' '``.
-    We can add escaping (``'\ '``) and quoting to allow more flexibility.
-    If there's one part, it's the file name.  If there is more than one part, it
-    will provide comment characters.  The ``shlex`` module
-    will handle the parsing into quoted fields.
-
-
-
-..  _`115`:
-..  rubric:: Imports (115) +=
-..  parsed-literal::
-    :class: code
-
-    import shlex
-    
-
-..
-
-    ..  class:: small
-
-        |loz| *Imports (115)*. Used by: pyweb.py (`148`_)
-
-
-
-..  _`116`:
-..  rubric:: start an OutputChunk, adding it to the web (116) =
-..  parsed-literal::
-    :class: code
-
-    
-    args= next(self.tokenizer).strip()
-    values = shlex.split( args )
-    if len(values) == 1:
-        self.aChunk= OutputChunk( values[0], "", "" )
-    elif len(values) == 2:
-        self.aChunk= OutputChunk( values[0], values[1], "" )
-    else:
-        self.aChunk= OutputChunk( values[0], values[1], values[2] )
-    self.aChunk.webAdd( self.theWeb )
-    self.expect( (self.cmdlcurl,) )
-    # capture an OutputChunk up to @}
-
-..
-
-    ..  class:: small
-
-        |loz| *start an OutputChunk, adding it to the web (116)*. Used by: major commands segment the input into separate Chunks (`114`_); WebReader handle a command string (`113`_); WebReader class - parses the input file, building the Web structure (`112`_); Base Class Definitions (`1`_); pyweb.py (`148`_)
-
-
-A named chunk has the form ``@d`` *name* ``@{`` *content* ``@}`` for
-code and ``@d`` *name* ``@[`` *content* ``@]`` for document source.
-We use the first two tokens to name the ``NamedChunk`` or ``NamedDocumentChunk``.  
-We expect either the ``@{`` or ``@[`` separator, and use the actual
-token found to choose which subclass of ``Chunk`` to create.
-We then attach all subsequent commands
-to this chunk while waiting for the final ``@}`` or ``@]`` token to 
-end the chunk.
-
-
-
-..  _`117`:
-..  rubric:: start a NamedChunk or NamedDocumentChunk, adding it to the web (117) =
-..  parsed-literal::
-    :class: code
-
-    
-    name= next(self.tokenizer).strip()
-    # next token is @{ or @[
-    brack= self.expect( (self.cmdlcurl,self.cmdlbrak) )
-    if brack == self.cmdlcurl: 
-        self.aChunk= NamedChunk( name )
-    else: 
-        self.aChunk= NamedDocumentChunk( name )
-    self.aChunk.webAdd( self.theWeb )
-    # capture a NamedChunk up to @} or @]
-
-..
-
-    ..  class:: small
-
-        |loz| *start a NamedChunk or NamedDocumentChunk, adding it to the web (117)*. Used by: major commands segment the input into separate Chunks (`114`_); WebReader handle a command string (`113`_); WebReader class - parses the input file, building the Web structure (`112`_); Base Class Definitions (`1`_); pyweb.py (`148`_)
-
-
-An import command has the unusual form of ``@i`` *name*, with no trailing
-separator.  When we encounter the ``@i`` token, the next token will start with the
-file name, but may continue with an anonymous chunk.  We require that all ``@i`` commands
-occur at the end of a line, and break on the ``'\n'`` which must occur after the file name.
-This permits file names with embedded spaces. It also permits arguments and options,
-if really necessary.
-
-Once we have split the file name away from the rest of the following anonymous chunk,
-we push the following token back into the token stream, so that it will be the 
-first token examined at the top of the ``load()`` loop.
-
-We create a child ``WebReader`` instance to process the included file.  The entire file 
-is loaded into the current ``Web`` instance.  A new, empty ``Chunk`` is created at the end
-of the file so that processing can resume with an anonymous ``Chunk``.
-
-The reader has a ``permitList`` attribute.
-This lists any commands where failure is permitted.  Currently, only the ``@i`` command
-can be set to permit failure; this allows a ``.w`` to include
-a file that does not yet exist.  
- 
-The primary use case for this feature is when weaving test output.
-The first pass of **pyWeb** tangles the program source files; they are
-then run to create test output; the second pass of **pyWeb** weaves this
-test output into the final document via the ``@i`` command.
-
-
-
-..  _`118`:
-..  rubric:: import another file (118) =
-..  parsed-literal::
-    :class: code
-
-    
-    incFile= next(self.tokenizer).strip()
-    try:
-        self.logger.info( "Including {!r}".format(incFile) )
-        include= WebReader( parent=self )
-        with open(incFile,"r") as source:
-            include.load( self.theWeb, incFile, source )
-        self.totalLines += include.tokenizer.lineNumber
-        self.totalFiles += include.totalFiles
-    except (Error,IOError) as e:
-        self.logger.error( 
-            "Problems with included file {!s}, output is incomplete.".format(
-            incFile) )
-        # Discretionary - sometimes we want total failure
-        if self.cmdi in self.permitList: pass
-        else: raise
-    self.aChunk= Chunk()
-    self.aChunk.webAdd( self.theWeb )
-
-..
-
-    ..  class:: small
-
-        |loz| *import another file (118)*. Used by: major commands segment the input into separate Chunks (`114`_); WebReader handle a command string (`113`_); WebReader class - parses the input file, building the Web structure (`112`_); Base Class Definitions (`1`_); pyweb.py (`148`_)
-
-
-When a ``@}`` or ``@]`` are found, this finishes a named chunk.  The next
-text is therefore part of an anonymous chunk.
-
-
-Note that no check is made to assure that the previous ``Chunk`` was indeed a named
-chunk or output chunk started with ``@{`` or ``@[``.  
-To do this, an attribute would be
-needed for each ``Chunk`` subclass that indicated if a trailing bracket was necessary.
-For the base ``Chunk`` class, this would be false, but for all other subclasses of
-``Chunk``, this would be true.
-
-
-
-..  _`119`:
-..  rubric:: finish a chunk, start a new Chunk adding it to the web (119) =
-..  parsed-literal::
-    :class: code
-
-    
-    self.aChunk= Chunk()
-    self.aChunk.webAdd( self.theWeb )
-
-..
-
-    ..  class:: small
-
-        |loz| *finish a chunk, start a new Chunk adding it to the web (119)*. Used by: major commands segment the input into separate Chunks (`114`_); WebReader handle a command string (`113`_); WebReader class - parses the input file, building the Web structure (`112`_); Base Class Definitions (`1`_); pyweb.py (`148`_)
-
-
-The following sequence of ``elif`` statements identifies
-the minor commands that add ``Command`` instances to the current open ``Chunk``. 
-
-
-
-..  _`120`:
-..  rubric:: minor commands add Commands to the current Chunk (120) =
-..  parsed-literal::
-    :class: code
-
-    
-    elif token[:2] == self.cmdpipe:
-        |srarr|\ assign user identifiers to the current chunk (`121`_)
-    elif token[:2] == self.cmdf:
-        self.aChunk.append( FileXrefCommand(self.tokenizer.lineNumber) )
-    elif token[:2] == self.cmdm:
-        self.aChunk.append( MacroXrefCommand(self.tokenizer.lineNumber) )
-    elif token[:2] == self.cmdu:
-        self.aChunk.append( UserIdXrefCommand(self.tokenizer.lineNumber) )
-    elif token[:2] == self.cmdlangl:
-        |srarr|\ add a reference command to the current chunk (`122`_)
-    elif token[:2] == self.cmdlexpr:
-        |srarr|\ add an expression command to the current chunk (`124`_)
-    elif token[:2] == self.cmdcmd:
-        |srarr|\ double at-sign replacement, append this character to previous TextCommand (`125`_)
-
-..
-
-    ..  class:: small
-
-        |loz| *minor commands add Commands to the current Chunk (120)*. Used by: WebReader handle a command string (`113`_); WebReader class - parses the input file, building the Web structure (`112`_); Base Class Definitions (`1`_); pyweb.py (`148`_)
-
-
-User identifiers occur after a ``@|`` in a ``NamedChunk``.
-
-Note that no check is made to assure that the previous ``Chunk`` was indeed a named
-chunk or output chunk started with ``@{``.  
-To do this, an attribute would be
-needed for each ``Chunk`` subclass that indicated if user identifiers are permitted.
-For the base ``Chunk`` class, this would be false, but for the ``NamedChunk`` class and
-``OutputChunk`` class, this would be true.
-
-User Identifiers are name references at the end of a NamedChunk
-These are accumulated and expanded by @u reference
-
-
-..  _`121`:
-..  rubric:: assign user identifiers to the current chunk (121) =
-..  parsed-literal::
-    :class: code
-
-    
-    try:
-        self.aChunk.setUserIDRefs( next(self.tokenizer).strip() )
-    except AttributeError:
-        # Out of place user identifier command
-        raise Error("Unexpected references near {:s}: {:s}".format(self.location(),token) )
-
-..
-
-    ..  class:: small
-
-        |loz| *assign user identifiers to the current chunk (121)*. Used by: minor commands add Commands to the current Chunk (`120`_); WebReader handle a command string (`113`_); WebReader class - parses the input file, building the Web structure (`112`_); Base Class Definitions (`1`_); pyweb.py (`148`_)
-
-
-A reference command has the form ``@<``\ *name*\ ``@>``.  We accept three
-tokens from the input, the middle token is the referenced name.
-
-
-
-..  _`122`:
-..  rubric:: add a reference command to the current chunk (122) =
-..  parsed-literal::
-    :class: code
-
-    
-    # get the name, introduce into the named Chunk dictionary
-    expand= next(self.tokenizer).strip()
-    closing= self.expect( (self.cmdrangl,) )
-    self.theWeb.addDefName( expand )
-    self.aChunk.append( ReferenceCommand( expand, self.tokenizer.lineNumber ) )
-    self.aChunk.appendText( "", self.tokenizer.lineNumber ) # to collect following text
-    self.logger.debug( "Reading {!r} {!r}".format(expand, closing) )
-
-..
-
-    ..  class:: small
-
-        |loz| *add a reference command to the current chunk (122)*. Used by: minor commands add Commands to the current Chunk (`120`_); WebReader handle a command string (`113`_); WebReader class - parses the input file, building the Web structure (`112`_); Base Class Definitions (`1`_); pyweb.py (`148`_)
-
-
-An expression command has the form ``@(``\ *Python Expression*\ ``@)``.  
-We accept three
-tokens from the input, the middle token is the expression.
-
-There are two alternative semantics for an embedded expression.
-
--   Deferred Execution.  This requires definition of a new subclass of ``Command``, 
-    ``ExpressionCommand``, and appends it into the current ``Chunk``.  At weave and
-    tangle time, this expression is evaluated.  The insert might look something like this:
-    ``aChunk.append( ExpressionCommand( expression, self.tokenizer.lineNumber ) )``.
-
--   Immediate Execution.  This simply creates a context and evaluates
-    the Python expression.  The output from the expression becomes a TextCommand, and
-    is append to the current ``Chunk``.
-
-We use the Immediate Execution semantics.
-
-Note that we've removed the blanket ``os``.  We only provide ``os.path``.
-An ``os.getcwd()`` must be changed to ``os.path.realpath('.')``.
-
-
-..  _`123`:
-..  rubric:: Imports (123) +=
-..  parsed-literal::
-    :class: code
-
-    
-    import builtins
-
-..
-
-    ..  class:: small
-
-        |loz| *Imports (123)*. Used by: pyweb.py (`148`_)
-
-
-
-..  _`124`:
-..  rubric:: add an expression command to the current chunk (124) =
-..  parsed-literal::
-    :class: code
-
-    
-    # get the Python expression, create the expression command
-    expression= next(self.tokenizer)
-    self.expect( (self.cmdrexpr,) )
-    try:
-        # Build Context
-        safe= types.SimpleNamespace( \*\*dict( (name,obj) 
-            for name,obj in builtins.\_\_dict\_\_.items() 
-            if name not in ('eval', 'exec', 'open', '\_\_import\_\_')))
-        globals= dict(
-            \_\_builtins\_\_= safe, 
-            os= types.SimpleNamespace(path=os.path),
-            datetime= datetime,
-            platform= platform,
-            theLocation= self.location(),
-            theWebReader= self,
-            theFile= self.theWeb.webFileName,
-            thisApplication= sys.argv[0],
-            \_\_version\_\_= \_\_version\_\_,
-            )
-        # Evaluate
-        result= str(eval(expression, globals))
-    except Exception as e:
-        self.logger.exception( 'Failure to process {!r}: result is {!r}'.format(expression, e) )
-        result= "@({!r}: Error {!r}@)".format(expression, e)
-    self.aChunk.appendText( result, self.tokenizer.lineNumber )
-
-..
-
-    ..  class:: small
-
-        |loz| *add an expression command to the current chunk (124)*. Used by: minor commands add Commands to the current Chunk (`120`_); WebReader handle a command string (`113`_); WebReader class - parses the input file, building the Web structure (`112`_); Base Class Definitions (`1`_); pyweb.py (`148`_)
-
-
-A double command sequence (``'@@'``, when the command is an ``'@'``) has the
-usual meaning of ``'@'`` in the input stream.  We do this via 
-the ``appendText()`` method of the current ``Chunk``.  This will append the 
-character on the end of the most recent ``TextCommand``; if this fails, it will
-create a new, empty ``TextCommand``.
-
-We replace with '@' here and now! This is put this at the end of the previous chunk.
-And we make sure the next chunk will be appended to this so that it's 
-largely seamless.
-
-
-..  _`125`:
-..  rubric:: double at-sign replacement, append this character to previous TextCommand (125) =
-..  parsed-literal::
-    :class: code
-
-    
-    self.aChunk.appendText( self.command, self.tokenizer.lineNumber )
-
-..
-
-    ..  class:: small
-
-        |loz| *double at-sign replacement, append this character to previous TextCommand (125)*. Used by: minor commands add Commands to the current Chunk (`120`_); WebReader handle a command string (`113`_); WebReader class - parses the input file, building the Web structure (`112`_); Base Class Definitions (`1`_); pyweb.py (`148`_)
-
-
-The ``expect()`` method examines the 
-next token to see if it is the expected item. ``'\n'`` are absorbed.  
-If this is not found, a standard type of error message is raised. 
-This is used by ``handleCommand()``.
-
-
-..  _`126`:
-..  rubric:: WebReader handle a command string (126) +=
-..  parsed-literal::
-    :class: code
-
-    
-    def expect( self, tokens ):
-        try:
-            t= next(self.tokenizer)
-            while t == '\\n':
-                t= next(self.tokenizer)
-        except StopIteration:
-            raise Error("At {!r}: end of input, {!r} not found".format(self.location(),tokens) )
-        if t not in tokens:
-            raise Error("At {!r}: expected {!r}, found {!r}".format(self.location(),tokens,t) )
-        return t
-    
-
-..
-
-    ..  class:: small
-
-        |loz| *WebReader handle a command string (126)*. Used by: WebReader class - parses the input file, building the Web structure (`112`_); Base Class Definitions (`1`_); pyweb.py (`148`_)
-
-
-The ``location()`` provides the file name and line number.
-This allows error messages as well as tangled or woven output 
-to correctly reference the original input files.
-
-
-..  _`127`:
-..  rubric:: WebReader location in the input stream (127) =
-..  parsed-literal::
-    :class: code
-
-    
-    def location( self ):
-        return ( self.fileName, self.tokenizer.lineNumber+1 )
-    
-
-..
-
-    ..  class:: small
-
-        |loz| *WebReader location in the input stream (127)*. Used by: WebReader class - parses the input file, building the Web structure (`112`_); Base Class Definitions (`1`_); pyweb.py (`148`_)
-
-
-These two fluent methods to set the attributes of a WebReader.
-We can do ``WebReader().load(web,filename)`` instead.
-
-
-..  _`128`:
-..  rubric:: WebReader fluent setter methods (128) =
-..  parsed-literal::
-    :class: code
-
-    
-    def web( self, aWeb ):
-        self.theWeb= aWeb
-        if self.fileName:
-            self.theWeb.webFileName= self.fileName        
-        return self
-    def source( self, name, source=None ):
-        """Set a name to display with error messages; also set the actual file-like source.
-        if no source is given, the name is treated as a filename and opened.
-        """
-        self.fileName= name
-        self.\_source= source
-        if self.theWeb:
-            self.theWeb.webFileName= self.fileName
-        return self
-
-..
-
-    ..  class:: small
-
-        |loz| *WebReader fluent setter methods (128)*. Used by: WebReader class - parses the input file, building the Web structure (`112`_); Base Class Definitions (`1`_); pyweb.py (`148`_)
-
-
-The ``load()`` method reads the entire input file as a sequence
-of tokens, split up by the ``Tokenizer``.  Each token that appears
-to be a command is passed to the ``handleCommand()`` method.  If
-the ``handleCommand()`` method returns a True result, the command was recognized
-and placed in the ``Web``.  If ``handleCommand()`` returns a False result, the command
-was unknown, and we write a warning but treat it as text.
-
-The ``load()`` method is used recursively to handle the ``@i`` command. The issue
-is that it's always loading a single top-level web.
-
-
-..  _`129`:
-..  rubric:: WebReader load the web (129) =
-..  parsed-literal::
-    :class: code
-
-    
-    def load( self, web, filename, source=None ):
-        # with open( self.fileName, "r" ) as self.\_source:
-        self.filename= filename
-        self.\_source= source
-        self.theWeb= web
-        self.theWeb.webFileName= self.fileName
-        
-        if self.\_source is None:
-            self.\_source= open( self.fileName, "r" )
-        self.tokenizer= Tokenizer( self.\_source, self.command )
-        self.totalFiles += 1
-    
-        self.aChunk= Chunk() # Initial anonymous chunk of text.
-        self.aChunk.webAdd( self.theWeb )
-    
-        for token in self.tokenizer:
-            if len(token) >= 2 and token.startswith(self.command):
-                if self.handleCommand( token ):
-                    continue
-                else:
-                    self.logger.warn( 'Unknown @-command in input: {!r}'.format(token) )
-                    self.aChunk.appendText( token, self.tokenizer.lineNumber )
-            elif token:
-                # Accumulate a non-empty block of text in the current chunk.
-                self.aChunk.appendText( token, self.tokenizer.lineNumber )
-    
-
-..
-
-    ..  class:: small
-
-        |loz| *WebReader load the web (129)*. Used by: WebReader class - parses the input file, building the Web structure (`112`_); Base Class Definitions (`1`_); pyweb.py (`148`_)
-
-
-The command character can be changed to permit
-some flexibility when working with languages that make extensive
-use of the ``@`` symbol, i.e., PERL.
-The initialization of the ``WebReader`` is based on the selected 
-command character.
-
-
-
-..  _`130`:
-..  rubric:: WebReader command literals (130) =
-..  parsed-literal::
-    :class: code
-
-    
-    # structural ("major") commands
-    self.cmdo= self.command+'o'
-    self.cmdd= self.command+'d'
-    self.cmdlcurl= self.command+'{'
-    self.cmdrcurl= self.command+'}'
-    self.cmdlbrak= self.command+'['
-    self.cmdrbrak= self.command+']'
-    self.cmdi= self.command+'i'
-    # inline ("minor") commands
-    self.cmdlangl= self.command+'<'
-    self.cmdrangl= self.command+'>'
-    self.cmdpipe= self.command+'\|'
-    self.cmdlexpr= self.command+'('
-    self.cmdrexpr= self.command+')'
-    self.cmdf= self.command+'f'
-    self.cmdm= self.command+'m'
-    self.cmdu= self.command+'u'
-    self.cmdcmd= self.command+self.command
-
-..
-
-    ..  class:: small
-
-        |loz| *WebReader command literals (130)*. Used by: WebReader class - parses the input file, building the Web structure (`112`_); Base Class Definitions (`1`_); pyweb.py (`148`_)
-
-
-Action Class Hierarchy
------------------------
-
-This application performs three major actions: loading the document web, 
-weaving and tangling.  Generally,
-the use case is to perform a load, weave and tangle.  However, a less common use case
-is to first load and tangle output files, run a regression test and then 
-load and weave a result that includes the test output file.
-
-The ``-x`` option excludes one of the two output actions.  The ``-xw`` 
-excludes the weave pass, doing only the tangle action.  The ``-xt`` excludes
-the tangle pass, doing the weave action.
-
-This two pass action might be embedded in the following type of Python program.
-
-..  parsed-literal::
-
-    import pyweb, os, runpy, sys
-    pyweb.tangle( "source.w" )
-    with open("source.log", "w") as target:
-        sys.stdout= target
-        runpy.run_path( 'source.py' )
-        sys.stdout= sys.__stdout__
-    pyweb.weave( "source.w" )
-
-
-The first step runs **pyWeb**, excluding the final weaving pass.  The second
-step runs the tangled program, ``source.py``, and produces test results in
-some log file, ``source.log``.  The third step runs pyWeb excluding the
-tangle pass.  This produces a final document that includes the ``source.log`` 
-test results.
-
-
-To accomplish this, we provide a class hierarchy that defines the various
-actions of the pyWeb application.  This class hierarchy defines an extensible set of 
-fundamental actions.  This gives us the flexibility to create a simple sequence
-of actions and execute any combination of these.  It eliminates the need for a 
-forest of ``if``-statements to determine precisely what will be done.
-
-Each action has the potential to update the state of the overall
-application.   A partner with this command hierarchy is the Application class
-that defines the application options, inputs and results. 
-
-
-..  _`131`:
-..  rubric:: Action class hierarchy - used to describe basic actions of the application (131) =
-..  parsed-literal::
-    :class: code
-
-    
-    |srarr|\ Action superclass has common features of all actions (`132`_)
-    |srarr|\ ActionSequence subclass that holds a sequence of other actions (`135`_)
-    |srarr|\ WeaveAction subclass initiates the weave action (`139`_)
-    |srarr|\ TangleAction subclass initiates the tangle action (`142`_)
-    |srarr|\ LoadAction subclass loads the document web (`145`_)
-
-..
-
-    ..  class:: small
-
-        |loz| *Action class hierarchy - used to describe basic actions of the application (131)*. Used by: Base Class Definitions (`1`_); pyweb.py (`148`_)
-
-
-Action Class
-~~~~~~~~~~~~~
-
-The ``Action`` class embodies the basic operations of pyWeb.
-The intent of this hierarchy is to both provide an easily expanded method of
-adding new actions, but an easily specified list of actions for a particular
-run of **pyWeb**.
-
-The overall process of the application is defined by an instance of ``Action``.
-This instance may be the ``WeaveAction`` instance, the ``TangleAction`` instance
-or a ``ActionSequence`` instance.
-
-The instance is constructed during parsing of the input parameters.  Then the 
-``Action`` class ``perform()`` method is called to actually perform the
-action.  There are three standard ``Action`` instances available: an instance
-that is a macro and does both tangling and weaving, an instance that excludes tangling,
-and an instance that excludes weaving.  These correspond to the command-line options.
-
-..  parsed-literal::
-
-    anOp= SomeAction( *parameters* )
-    anOp.options= *argparse.Namespace*
-    anOp.web = *Current web*
-    anOp()
-
-
-The ``Action`` is the superclass for all actions.
-An ``Action`` has a number of common attributes.
-
-:name:
-    A name for this action.
-    
-:options:
-    The ``argparse.Namespace`` object.
-    
-:web:
-    The current web that's being processed.
-    
-:start:
-    The time at which the action started.
-
-
-
-
-..  _`132`:
-..  rubric:: Action superclass has common features of all actions (132) =
-..  parsed-literal::
-    :class: code
-
-    
-    class Action:
-        """An action performed by pyWeb."""
-        def \_\_init\_\_( self, name ):
-            self.name= name
-            self.web= None
-            self.options= None
-            self.start= None
-            self.logger= logging.getLogger( self.\_\_class\_\_.\_\_qualname\_\_ )
-        def \_\_str\_\_( self ):
-            return "{:s} [{:s}]".format( self.name, self.web )
-        |srarr|\ Action call method actually does the real work (`133`_)
-        |srarr|\ Action final summary of what was done (`134`_)
-    
-
-..
-
-    ..  class:: small
-
-        |loz| *Action superclass has common features of all actions (132)*. Used by: Action class hierarchy - used to describe basic actions of the application (`131`_); Base Class Definitions (`1`_); pyweb.py (`148`_)
-
-
-The ``__call__()`` method does the real work of the action.
-For the superclass, it merely logs a message.  This is overridden 
-by a subclass.
-
-
-..  _`133`:
-..  rubric:: Action call method actually does the real work (133) =
-..  parsed-literal::
-    :class: code
-
-    
-    def \_\_call\_\_( self ):
-        self.logger.info( "Starting {!s}".format(self.\_\_class\_\_.\_\_name\_\_) )
-        self.start= time.process\_time()
-    
-
-..
-
-    ..  class:: small
-
-        |loz| *Action call method actually does the real work (133)*. Used by: Action superclass has common features of all actions (`132`_); Action class hierarchy - used to describe basic actions of the application (`131`_); Base Class Definitions (`1`_); pyweb.py (`148`_)
-
-
-The ``summary()`` method returns some basic processing
-statistics for this action.
-
-
-
-..  _`134`:
-..  rubric:: Action final summary of what was done (134) =
-..  parsed-literal::
-    :class: code
-
-    
-    def duration( self ):
-        """Return duration of the action."""
-        return (self.start and time.process\_time()-self.start) or 0
-    def summary( self ):
-        return "{:s} in {:0.2f} sec.".format( self.name, self.duration() )
-    
-
-..
-
-    ..  class:: small
-
-        |loz| *Action final summary of what was done (134)*. Used by: Action superclass has common features of all actions (`132`_); Action class hierarchy - used to describe basic actions of the application (`131`_); Base Class Definitions (`1`_); pyweb.py (`148`_)
-
-
-ActionSequence Class
-~~~~~~~~~~~~~~~~~~~~
-
-A ``ActionSequence`` defines a composite action; it is a sequence of
-other actions.  When the macro is performed, it delegates to the 
-sub-actions.
-
-The instance is created during parsing of input parameters.  An instance of
-this class is one of
-the three standard actions available; it generally is the default, "do everything" 
-action.
-
-This class overrides the ``perform()`` method of the superclass.  It also adds
-an ``append()`` method that is used to construct the sequence of actions.
-
-
-
-..  _`135`:
-..  rubric:: ActionSequence subclass that holds a sequence of other actions (135) =
-..  parsed-literal::
-    :class: code
-
-    
-    class ActionSequence( Action ):
-        """An action composed of a sequence of other actions."""
-        def \_\_init\_\_( self, name, opSequence=None ):
-            super().\_\_init\_\_( name )
-            if opSequence: self.opSequence= opSequence
-            else: self.opSequence= []
-        def \_\_str\_\_( self ):
-            return "; ".join( [ str(x) for x in self.opSequence ] )
-        |srarr|\ ActionSequence call method delegates the sequence of ations (`136`_)
-        |srarr|\ ActionSequence append adds a new action to the sequence (`137`_)
-        |srarr|\ ActionSequence summary summarizes each step (`138`_)
-    
-
-..
-
-    ..  class:: small
-
-        |loz| *ActionSequence subclass that holds a sequence of other actions (135)*. Used by: Action class hierarchy - used to describe basic actions of the application (`131`_); Base Class Definitions (`1`_); pyweb.py (`148`_)
-
-
-Since the macro ``__call__()`` method delegates to other Actions,
-it is possible to short-cut argument processing by using the Python
-``*args`` construct to accept all arguments and pass them to each
-sub-action.
-
-
-..  _`136`:
-..  rubric:: ActionSequence call method delegates the sequence of ations (136) =
-..  parsed-literal::
-    :class: code
-
-    
-    def \_\_call\_\_( self ):
-        for o in self.opSequence:
-            o.web= self.web
-            o.options= self.options
-            o()
-    
-
-..
-
-    ..  class:: small
-
-        |loz| *ActionSequence call method delegates the sequence of ations (136)*. Used by: ActionSequence subclass that holds a sequence of other actions (`135`_); Action class hierarchy - used to describe basic actions of the application (`131`_); Base Class Definitions (`1`_); pyweb.py (`148`_)
-
-
-Since this class is essentially a wrapper around the built-in sequence type, 
-we delegate sequence related actions directly to the underlying sequence.
-
-
-..  _`137`:
-..  rubric:: ActionSequence append adds a new action to the sequence (137) =
-..  parsed-literal::
-    :class: code
-
-    
-    def append( self, anAction ):
-        self.opSequence.append( anAction )
-    
-
-..
-
-    ..  class:: small
-
-        |loz| *ActionSequence append adds a new action to the sequence (137)*. Used by: ActionSequence subclass that holds a sequence of other actions (`135`_); Action class hierarchy - used to describe basic actions of the application (`131`_); Base Class Definitions (`1`_); pyweb.py (`148`_)
-
-
-The ``summary()`` method returns some basic processing
-statistics for each step of this action.
-
-
-..  _`138`:
-..  rubric:: ActionSequence summary summarizes each step (138) =
-..  parsed-literal::
-    :class: code
-
-    
-    def summary( self ):
-        return ", ".join( [ o.summary() for o in self.opSequence ] )
-    
-
-..
-
-    ..  class:: small
-
-        |loz| *ActionSequence summary summarizes each step (138)*. Used by: ActionSequence subclass that holds a sequence of other actions (`135`_); Action class hierarchy - used to describe basic actions of the application (`131`_); Base Class Definitions (`1`_); pyweb.py (`148`_)
-
-
-WeaveAction Class
-~~~~~~~~~~~~~~~~~~
-
-The ``WeaveAction`` defines the action of weaving.  This action
-logs a message, and invokes the ``weave()`` method of the ``Web`` instance.
-This method also includes the basic decision on which weaver to use.  If a ``Weaver`` was
-specified on the command line, this instance is used.  Otherwise, the first few characters
-are examined and a weaver is selected.
-
-This class overrides the ``__call__()`` method of the superclass.
-
-If the options include ``theWeaver``, that ``Weaver`` instance will be used.
-Otherwise, the ``web.language()`` method function is used to guess what weaver to use.
-
-
-..  _`139`:
-..  rubric:: WeaveAction subclass initiates the weave action (139) =
-..  parsed-literal::
-    :class: code
-
-    
-    class WeaveAction( Action ):
-        """An action that weaves a document."""
-        def \_\_init\_\_( self ):
-            super().\_\_init\_\_( "Weave" )
-        def \_\_str\_\_( self ):
-            return "{:s} [{:s}, {:s}]".format( self.name, self.web, self.theWeaver )
-    
-        |srarr|\ WeaveAction call method to pick the language (`140`_)
-        |srarr|\ WeaveAction summary of language choice (`141`_)
-    
-
-..
-
-    ..  class:: small
-
-        |loz| *WeaveAction subclass initiates the weave action (139)*. Used by: Action class hierarchy - used to describe basic actions of the application (`131`_); Base Class Definitions (`1`_); pyweb.py (`148`_)
-
-
-The language is picked just prior to weaving.  It is either (1) the language
-specified on the command line, or, (2) if no language was specified, a language
-is selected based on the first few characters of the input.
-
-Weaving can only raise an exception when there is a reference to a chunk that
-is never defined.
-
-
-..  _`140`:
-..  rubric:: WeaveAction call method to pick the language (140) =
-..  parsed-literal::
-    :class: code
-
-    
-    def \_\_call\_\_( self ):
-        super().\_\_call\_\_()
-        if not self.options.theWeaver: 
-            # Examine first few chars of first chunk of web to determine language
-            self.options.theWeaver= self.web.language() 
-        try:
-            self.web.weave( self.options.theWeaver )
-        except Error as e:
-            self.logger.error(
-                "Problems weaving document from {:s} (weave file is faulty).".format(
-                self.web.webFileName) )
-            raise
-    
-
-..
-
-    ..  class:: small
-
-        |loz| *WeaveAction call method to pick the language (140)*. Used by: WeaveAction subclass initiates the weave action (`139`_); Action class hierarchy - used to describe basic actions of the application (`131`_); Base Class Definitions (`1`_); pyweb.py (`148`_)
-
-
-The ``summary()`` method returns some basic processing
-statistics for the weave action.
-
-
-
-..  _`141`:
-..  rubric:: WeaveAction summary of language choice (141) =
-..  parsed-literal::
-    :class: code
-
-    
-    def summary( self ):
-        if self.options.theWeaver and self.options.theWeaver.linesWritten > 0:
-            return "{:s} {:d} lines in {:0.2f} sec.".format( self.name, 
-            self.options.theWeaver.linesWritten, self.duration() )
-        return "did not {:s}".format( self.name, )
-    
-
-..
-
-    ..  class:: small
-
-        |loz| *WeaveAction summary of language choice (141)*. Used by: WeaveAction subclass initiates the weave action (`139`_); Action class hierarchy - used to describe basic actions of the application (`131`_); Base Class Definitions (`1`_); pyweb.py (`148`_)
-
-
-TangleAction Class
-~~~~~~~~~~~~~~~~~~~
-
-The ``TangleAction`` defines the action of tangling.  This operation
-logs a message, and invokes the ``weave()`` method of the ``Web`` instance.
-This method also includes the basic decision on which weaver to use.  If a ``Weaver`` was
-specified on the command line, this instance is used.  Otherwise, the first few characters
-are examined and a weaver is selected.
-
-This class overrides the ``__call__()`` method of the superclass.
-
-The options **must** include ``theTangler``, with the ``Tangler`` instance to be used.
-
-
-..  _`142`:
-..  rubric:: TangleAction subclass initiates the tangle action (142) =
-..  parsed-literal::
-    :class: code
-
-    
-    class TangleAction( Action ):
-        """An action that weaves a document."""
-        def \_\_init\_\_( self ):
-            super().\_\_init\_\_( "Tangle" )
-        |srarr|\ TangleAction call method does tangling of the output files (`143`_)
-        |srarr|\ TangleAction summary method provides total lines tangled (`144`_)
-    
-
-..
-
-    ..  class:: small
-
-        |loz| *TangleAction subclass initiates the tangle action (142)*. Used by: Action class hierarchy - used to describe basic actions of the application (`131`_); Base Class Definitions (`1`_); pyweb.py (`148`_)
-
-
-Tangling can only raise an exception when a cross reference request (``@f``, ``@m`` or ``@u``)
-occurs in a program code chunk.  Program code chunks are defined 
-with any of ``@d`` or ``@o``  and use ``@{`` ``@}`` brackets.
-
-
-
-..  _`143`:
-..  rubric:: TangleAction call method does tangling of the output files (143) =
-..  parsed-literal::
-    :class: code
-
-    
-    def \_\_call\_\_( self ):
-        super().\_\_call\_\_()
-        try:
-            self.web.tangle( self.options.theTangler )
-        except Error as e:
-            self.logger.error( 
-                "Problems tangling outputs from {!r} (tangle files are faulty).".format(
-                self.web.webFileName) )
-            raise
-    
-
-..
-
-    ..  class:: small
-
-        |loz| *TangleAction call method does tangling of the output files (143)*. Used by: TangleAction subclass initiates the tangle action (`142`_); Action class hierarchy - used to describe basic actions of the application (`131`_); Base Class Definitions (`1`_); pyweb.py (`148`_)
-
-
-The ``summary()`` method returns some basic processing
-statistics for the tangle action.
-
-
-..  _`144`:
-..  rubric:: TangleAction summary method provides total lines tangled (144) =
-..  parsed-literal::
-    :class: code
-
-    
-    def summary( self ):
-        if self.options.theTangler and self.options.theTangler.linesWritten > 0:
-            return "{:s} {:d} lines in {:0.2f} sec.".format( self.name, 
-            self.options.theTangler.linesWritten, self.duration() )
-        return "did not {!r}".format( self.name, )
-    
-
-..
-
-    ..  class:: small
-
-        |loz| *TangleAction summary method provides total lines tangled (144)*. Used by: TangleAction subclass initiates the tangle action (`142`_); Action class hierarchy - used to describe basic actions of the application (`131`_); Base Class Definitions (`1`_); pyweb.py (`148`_)
-
-
-
-LoadAction Class
-~~~~~~~~~~~~~~~~~~
-
-The ``LoadAction`` defines the action of loading the web structure.  This action
-uses the application's ``webReader`` to actually do the load.
-
-An instance is created during parsing of the input parameters.  An instance of
-this class is part of any of the weave, tangle and "do everything" action.
-
-This class overrides the ``__call__()`` method of the superclass.
-
-The options **must** include ``webReader``, with the ``WebReader`` instance to be used.
-
-
-
-..  _`145`:
-..  rubric:: LoadAction subclass loads the document web (145) =
-..  parsed-literal::
-    :class: code
-
-    
-    class LoadAction( Action ):
-        """An action that loads the source web for a document."""
-        def \_\_init\_\_( self ):
-            super().\_\_init\_\_( "Load" )
-        def \_\_str\_\_( self ):
-            return "Load [{:s}, {:s}]".format( self.webReader, self.web )
-        |srarr|\ LoadAction call method loads the input files (`146`_)
-        |srarr|\ LoadAction summary provides lines read (`147`_)
-    
-
-..
-
-    ..  class:: small
-
-        |loz| *LoadAction subclass loads the document web (145)*. Used by: Action class hierarchy - used to describe basic actions of the application (`131`_); Base Class Definitions (`1`_); pyweb.py (`148`_)
-
-
-Trying to load the web involves two steps, either of which can raise 
-exceptions due to incorrect inputs.
-
-
--   The ``WebReader`` class ``load()`` method can raise exceptions for a number of 
-    syntax errors.
-
-    -     Missing closing brackets (``@}``, @] or ``@>``).
-
-    -     Missing opening bracket (``@{`` or ``@[``) after a chunk name (``@d`` or ``@o``).
-
-    -     Extra brackets (``@{``, ``@[``, ``@}``, ``@]``).
-
-    -     Extra ``@|``.
-
-    -     The input file does not exist or is not readable.
-
--   The ``Web`` class ``createUsedBy()`` method can raise an exception when a 
-    chunk reference cannot be resolved to a named chunk.
-
-
-..  _`146`:
-..  rubric:: LoadAction call method loads the input files (146) =
-..  parsed-literal::
-    :class: code
-
-    
-    def \_\_call\_\_( self ):
-        super().\_\_call\_\_()
-        self.webReader= self.options.webReader
-        try:
-            with open(self.options.webFileName, "r") as source:
-                self.webReader.load( self.web, self.options.webFileName, source )
-        except (Error,IOError) as e:
-            self.logger.error(
-                "Problems with source file {!r}, no output produced.".format(
-                self.web.webFileName) )
-            raise
-        self.web.createUsedBy()
-    
-
-..
-
-    ..  class:: small
-
-        |loz| *LoadAction call method loads the input files (146)*. Used by: LoadAction subclass loads the document web (`145`_); Action class hierarchy - used to describe basic actions of the application (`131`_); Base Class Definitions (`1`_); pyweb.py (`148`_)
-
-
-The ``summary()`` method returns some basic processing
-statistics for the load action.
-
-
-..  _`147`:
-..  rubric:: LoadAction summary provides lines read (147) =
-..  parsed-literal::
-    :class: code
-
-    
-    def summary( self ):
-        return "{:s} {:d} lines from {:d} files in {:0.2f} sec.".format( 
-            self.name, self.webReader.totalLines, 
-            self.webReader.totalFiles, self.duration() )
-    
-
-..
-
-    ..  class:: small
-
-        |loz| *LoadAction summary provides lines read (147)*. Used by: LoadAction subclass loads the document web (`145`_); Action class hierarchy - used to describe basic actions of the application (`131`_); Base Class Definitions (`1`_); pyweb.py (`148`_)
-
-
-
-**pyWeb** Module File
-------------------------
-
-The **pyWeb** application file is shown below:
-
-
-..  _`148`:
-..  rubric:: pyweb.py (148) =
-..  parsed-literal::
-    :class: code
-
-    |srarr|\ Overheads (`150`_), |srarr|\ (`151`_), |srarr|\ (`152`_)
-    |srarr|\ Imports (`11`_), |srarr|\ (`47`_), |srarr|\ (`115`_), |srarr|\ (`123`_), |srarr|\ (`149`_), |srarr|\ (`153`_), |srarr|\ (`159`_)
-    |srarr|\ Base Class Definitions (`1`_)
-    |srarr|\ Application Class (`154`_), |srarr|\ (`155`_)
-    |srarr|\ Logging Setup (`160`_), |srarr|\ (`161`_)
-    |srarr|\ Interface Functions (`162`_)
-
-..
-
-    ..  class:: small
-
-        |loz| *pyweb.py (148)*.
-
-
-The overhead elements are described in separate sub sections as follows:
-
--     shell escape
-
--     doc string
-
--     ``__version__`` setting
-
--     imports
-
-
-The more important elements are described in separate sections:
-
--     Base Class Definitions
-
--     Application Class and Main Functions
-
--     Interface Functions
-
-Python Library Imports
-~~~~~~~~~~~~~~~~~~~~~~~
-
-The following Python library modules are used by this application.
-
-
--   The ``sys`` module provides access to the command line arguments.
-
--   The ``os`` module provide os-specific file and path manipulations; it is used
-    to transform the input file name into the output file name as well as track down file modification
-    times.
-
--   The ``re`` module provides regular expressions; these are used to 
-    parse the input file.
-
--   The ``time`` module provides a handy current-time string; this is used
-    to by the HTML Weaver to write a closing timestamp on generated HTML files, 
-    as well as log messages.
-    
--   The ``datetime`` module is used to format times, phasing out use of ``time``.
-
--   The ``types`` module is used only to get at ``SimpleNamespace``.
-
-
-
-..  _`149`:
-..  rubric:: Imports (149) +=
-..  parsed-literal::
-    :class: code
-
-    import sys
-    import re
-    import os
-    import time
-    import datetime
-    import types
-    import platform
-    
-
-..
-
-    ..  class:: small
-
-        |loz| *Imports (149)*. Used by: pyweb.py (`148`_)
-
-
-Additionally, ``os.path``, ``time``, ``datetime`` and ``platform```
-are provided in the expression context.
-
-Overheads
-~~~~~~~~~~~~
-
-The shell escape is provided so that the user can define this
-file as executable, and launch it directly from their shell.
-The shell reads the first line of a file; when it finds the ``'#!'`` shell
-escape, the remainder of the line is taken as the path to the binary program
-that should be run.  The shell runs this binary, providing the 
-file as standard input.
-
-
-
-..  _`150`:
-..  rubric:: Overheads (150) =
-..  parsed-literal::
-    :class: code
-
-    #!/usr/bin/env python
-
-..
-
-    ..  class:: small
-
-        |loz| *Overheads (150)*. Used by: pyweb.py (`148`_)
-
-
-A Python ``__doc__`` string provides a standard vehicle for documenting
-the module or the application program.  The usual style is to provide
-a one-sentence summary on the first line.  This is followed by more 
-detailed usage information.
-
-
-
-..  _`151`:
-..  rubric:: Overheads (151) +=
-..  parsed-literal::
-    :class: code
-
-    """pyWeb Literate Programming - tangle and weave tool.
-    
-    Yet another simple literate programming tool derived from nuweb, 
-    implemented entirely in Python.  
-    This produces any markup for any programming language.
-    
-    Usage:
-        pyweb.py [-dvs] [-c x] [-w format] file.w
-    
-    Options:
-        -v           verbose output (the default)
-        -s           silent output
-        -d           debugging output
-        -c x         change the command character from '@' to x
-        -w format    Use the given weaver for the final document.
-                     The default is based on the input file, a leading '<'
-                     indicates HTML, otherwise LaTeX.
-                     choices are 'html', 'latex', 'rst'.
-                     Additionally, a \`module.class\` name can be used.
-        -xw          Exclude weaving
-        -xt          Exclude tangling
-        -pi          Permit include-command errors
-        
-        file.w       The input file, with @o, @d, @i, @[, @{, @\|, @<, @f, @m, @u commands.
-    """
-
-..
-
-    ..  class:: small
-
-        |loz| *Overheads (151)*. Used by: pyweb.py (`148`_)
-
-
-The keyword cruft is a standard way of placing version control information into
-a Python module so it is preserved.  See PEP (Python Enhancement Proposal) #8 for information
-on recommended styles.
-
-
-We also sneak in a "DO NOT EDIT" warning that belongs in all generated application 
-source files.
-
-
-..  _`152`:
-..  rubric:: Overheads (152) +=
-..  parsed-literal::
-    :class: code
-
-    \_\_version\_\_ = """2.3"""
-    
-    ### DO NOT EDIT THIS FILE!
-    ### It was created by pyweb.py, \_\_version\_\_='2.3'.
-    ### From source impl.w modified Tue Mar 11 10:21:21 2014.
-    ### In working directory '/Users/slott/Documents/Projects/pyWeb-2.3/pyweb'.
-
-..
-
-    ..  class:: small
-
-        |loz| *Overheads (152)*. Used by: pyweb.py (`148`_)
-
-
-
-The Application Class
------------------------
-
-The ``Application`` class is provided so that the ``Action`` instances
-have an overall application to update.  This allows the ``WeaveAction`` to 
-provide the selected ``Weaver`` instance to the application.  It also provides a
-central location for the various options and alternatives that might be accepted from
-the command line.
-
-
-The constructor creates a default ``argparse.Namespace`` with values
-suitable for weaving and tangling.
-
-The ``parseArgs()`` method uses the ``sys.argv`` sequence to 
-parse the command line arguments and update the options.  This allows a
-program to pre-process the arguments, passing other arguments to this module.
-
-
-The ``process()`` method processes a list of files.  This is either
-the list of files passed as an argument, or it is the list of files
-parsed by the ``parseArgs()`` method.
-
-
-The ``parseArgs()`` and process() functions are separated so that
-another application can ``import pyweb``, bypass command-line parsing, yet still perform
-the basic actionss simply and consistently.
-For example:
-
-..  parsed-literal::
-
-    import pyweb, argparse
-    
-    p= argparse.ArgumentParser()
-    *argument definition*
-    config = p.parse_args()
-    
-    a= pyweb.Application()
-    *Configure the Application based on options*
-    a.process( config )
-
-
-The ``main()`` function creates an ``Application`` instance and
-calls the ``parseArgs()`` and ``process()`` methods to provide the
-expected default behavior for this module when it is used as the main program.
-
-The configuration can be either a ``types.SimpleNamespace`` or an
-``argparse.Namespace`` instance.
-
-
-
-..  _`153`:
-..  rubric:: Imports (153) +=
-..  parsed-literal::
-    :class: code
-
-    import argparse
-    
-
-..
-
-    ..  class:: small
-
-        |loz| *Imports (153)*. Used by: pyweb.py (`148`_)
-
-
-
-..  _`154`:
-..  rubric:: Application Class (154) =
-..  parsed-literal::
-    :class: code
-
-    
-    class Application:
-        def \_\_init\_\_( self ):
-            self.logger= logging.getLogger( self.\_\_class\_\_.\_\_qualname\_\_ )
-            |srarr|\ Application default options (`156`_)
-        |srarr|\ Application parse command line (`157`_)
-        |srarr|\ Application class process all files (`158`_)
-    
-
-..
-
-    ..  class:: small
-
-        |loz| *Application Class (154)*. Used by: pyweb.py (`148`_)
-
-
-The first part of parsing the command line is 
-setting default values that apply when parameters are omitted.
-The default values are set as follows:
-
-:defaults:
-    A default configuration.
-
-:webReader:
-    is the ``WebReader`` instance created for the current
-    input file.
- 
-:doWeave:
-    instance of ``Action``
-    that does weaving only.
-
-:doTangle:
-    instance of ``Action``
-    that does tangling only.
-    
-:theAction:
-    is an instance of ``Action`` that describes
-    the default overall action: load, tangle and weave.  This is the default unless
-    overridden by an option.
-    
-Here are the configuration values. These are attributes
-of the ``argparse.namespace`` default as well as the updated
-namespace returned by ``parseArgs()``.
-
-:verbosity:
-    Either logging.INFO, logging.WARN or logging.DEBUG
-    
-:command:
-    is set to ``@`` as the  default command introducer.
-
-:permit:
-    The raw list of permitted command characters, perhaps 'i'.
-    
-:permitList:
-    provides a list of commands that are permitted
-    to fail.  Typically this is empty, or contains ``@i`` to allow the include
-    command to fail.
-
-:files:
-    is the final list of argument files from the command line; 
-    these will be processed unless overridden in the call to ``process()``.
-
-:skip:
-    a list of steps to skip: perhaps 'w' or 't' to skip weaving or tangling.
-    
-:weaver:
-    the short name of the weaver.
-    
-:theTangler:
-    is set to a ``TanglerMake`` instance 
-    to create the output files.
-
-:theWeaver:
-    is set to an instance of a subclass of ``Weaver`` based on ``weaver``
-
-Other instance variables.
-   
-Here's the global list of available weavers. Essentially this is the subclass
-list of ``Weaver``.  Essentially, the list is this:
-
-..  parsed-literal::
-
-    weavers = dict( 
-        (x.__class__.__name__.lower(), x) 
-        for x in Weaver.__subclasses__()
-    )
-
-Rather than automate this, and potentially expose elements of the class hierarchy
-that aren't really meant to be used, we provide a manually-developed list. 
-
-
-..  _`155`:
-..  rubric:: Application Class (155) +=
-..  parsed-literal::
-    :class: code
-
-    
-    # Global list of available weavers.
-    weavers = {
-        'html':  HTML(),
-        'htmlshort': HTMLShort(),
-        'latex': LaTeX(),
-        'rst': RST(), 
-    }
-
-..
-
-    ..  class:: small
-
-        |loz| *Application Class (155)*. Used by: pyweb.py (`148`_)
-
-
-The defaults used for application configuration. The ``expand()`` method expands
-on these simple text values to create more useful objects.
-
-
-..  _`156`:
-..  rubric:: Application default options (156) =
-..  parsed-literal::
-    :class: code
-
-    
-    self.defaults= argparse.Namespace(
-        verbosity= logging.INFO,
-        command= '@',
-        weaver= 'rst',
-        skip= '',
-        permit= ''
-        )
-    self.expand( self.defaults )
-    
-    # Primitive Actions
-    self.loadOp= LoadAction()
-    self.weaveOp= WeaveAction()
-    self.tangleOp= TangleAction()
-    
-    # Composite Actions
-    self.doWeave= ActionSequence( "load and weave", [self.loadOp, self.weaveOp] )
-    self.doTangle= ActionSequence( "load and tangle", [self.loadOp, self.tangleOp] )
-    self.theAction= ActionSequence( "load, tangle and weave", [self.loadOp, self.tangleOp, self.weaveOp] )
-
-..
-
-    ..  class:: small
-
-        |loz| *Application default options (156)*. Used by: Application Class (`154`_); pyweb.py (`148`_)
-
-
-The algorithm for parsing the command line parameters uses the built in
-``argparse`` module.  We have to build a parser, define the options,
-and the parse the command-line arguments, updating the default namespace.
-
-We further expand on the arguments. This transforms simple strings into object
-instances.
-
-
-
-..  _`157`:
-..  rubric:: Application parse command line (157) =
-..  parsed-literal::
-    :class: code
-
-    
-    def parseArgs( self ):
-        p = argparse.ArgumentParser()
-        p.add\_argument( "-v", "--verbose", dest="verbosity", action="store\_const", const=logging.INFO )
-        p.add\_argument( "-s", "--silent", dest="verbosity", action="store\_const", const=logging.WARN )
-        p.add\_argument( "-d", "--debug", dest="verbosity", action="store\_const", const=logging.DEBUG )
-        p.add\_argument( "-c", "--command", dest="command", action="store" )
-        p.add\_argument( "-w", "--weaver", dest="weaver", action="store" )
-        p.add\_argument( "-x", "--except", dest="skip", action="store", choices=('w','t') )
-        p.add\_argument( "-p", "--permit", dest="permit", action="store" )
-        p.add\_argument( "files", nargs='+' )
-        config= p.parse\_args( namespace=self.defaults )
-        self.expand( config )
-        return config
-        
-    def expand( self, config ):
-        """Expand some arguments from simple text to useful objects."""
-        try:
-            config.theWeaver= weavers[config.weaver.lower()]
-        except KeyError:
-            module\_name, \_, class\_name = config.weaver.partition('.')
-            weaver\_module = \_\_import\_\_(module\_name)
-            weaver\_class = weaver\_module.\_\_dict\_\_[class\_name]
-            if not issubclass(weaver\_class, Weaver):
-                raise TypeError( "{0!r} not a subclass of Weaver".format(weaver\_class) )
-            config.theWeaver= weaver\_class()
-        
-        config.theTangler= TanglerMake()
-        
-        if config.permit:
-            # save permitted errors, usual case is -pi to permit include errors
-            config.permitList= [ '{:s}{:s}'.format( config.command, c ) for c in config.permit ]
-        else:
-            config.permitList= []
-    
-        config.webReader= WebReader( command=config.command, permit=config.permitList )
-    
-        return config
-    
-    
-
-..
-
-    ..  class:: small
-
-        |loz| *Application parse command line (157)*. Used by: Application Class (`154`_); pyweb.py (`148`_)
-
-
-The ``process()`` function uses the current ``Application`` settings
-to process each file as follows:
-
-1.  Create a new ``WebReader`` for the ``Application``, providing
-    the parameters required to process the input file.
-
-2.  Create a ``Web`` instance, *w* 
-    and set the Web's *sourceFileName* from the WebReader's *fileName*.
-
-3.  Perform the given command, typically a ``ActionSequence``, 
-    which does some combination of load, tangle the output files and
-    weave the final document in the target language; if
-    necessary, examine the ``Web`` to determine the documentation language.
-
-4.  Print a performance summary line that shows lines processed per second.
-
-In the event of failure in any of the major processing steps, 
-a summary message is produced, to clarify the state of 
-the output files, and the exception is reraised.
-The re-raising is done so that all exceptions are handled by the 
-outermost main program.
-
-
-..  _`158`:
-..  rubric:: Application class process all files (158) =
-..  parsed-literal::
-    :class: code
-
-    
-    def process( self, config ):
-        if config.verbosity:
-            logging.getLogger().setLevel( config.verbosity )
-        
-        if config.command:
-            self.logger.info( "Setting command character to {!r}".format(config.command) )
-            
-        if config.skip:
-            if config.skip.lower().startswith('w'): # skip weaving
-                self.theAction= self.doTangle
-            elif config.skip.lower().startswith('t'): # skip tangling
-                self.theAction= self.doWeave
-            else:
-                raise Exception( "Unknown -x option {!r}".format(config.skip) )
-    
-        self.logger.info( "Weaving {:s}".format(config.theWeaver) )
-    
-        for f in config.files:
-            w= Web() # An empty web to load and process.
-            self.logger.info( "Reading {!r}".format(f) )
-            config.webFileName= f
-            self.theAction.web= w
-            self.theAction.options= config
-            self.theAction()
-            self.logger.info( self.theAction.summary() )
-    
-
-..
-
-    ..  class:: small
-
-        |loz| *Application class process all files (158)*. Used by: Application Class (`154`_); pyweb.py (`148`_)
-
-
-Logging Setup
---------------
-
-We'll create a logging context manager. This allows us to wrap the ``main()`` 
-function in an explicit ``with`` statement that assures that logging is
-configured and cleaned up politely.
-
-
-..  _`159`:
-..  rubric:: Imports (159) +=
-..  parsed-literal::
-    :class: code
-
-    
-    import logging
-    import logging.config
-
-..
-
-    ..  class:: small
-
-        |loz| *Imports (159)*. Used by: pyweb.py (`148`_)
-
-
-This has two configuration approaches. If a positional argument is given,
-that dictionary is used for ``logging.config.dictConfig``. Otherwise,
-keyword arguments are provided to ``logging.basicConfig``.
-
-A subclass might properly load a dictionary 
-encoded in YAML and use that with ``logging.config.dictConfig``.
-
-
-..  _`160`:
-..  rubric:: Logging Setup (160) =
-..  parsed-literal::
-    :class: code
-
-    
-    class Logger:
-        def \_\_init\_\_( self, dict\_config=None, \*\*kw\_config ):
-            self.dict\_config= dict\_config
-            self.kw\_config= kw\_config
-        def \_\_enter\_\_( self ):
-            if self.dict\_config:
-                logging.config.dictConfig( self.dict\_config )
-            else:
-                logging.basicConfig( \*\*self.kw\_config )
-            return self
-        def \_\_exit\_\_( self, \*args ):
-            logging.shutdown()
-            return False
-
-..
-
-    ..  class:: small
-
-        |loz| *Logging Setup (160)*. Used by: pyweb.py (`148`_)
-
-
-Here's a sample logging setup. This creates a simple console handler and 
-a formatter that matches the ``basicConfig`` formatter.
-
-It defines the root logger plus two overrides for class loggers that might be
-used to gather additional information.
-
-
-..  _`161`:
-..  rubric:: Logging Setup (161) +=
-..  parsed-literal::
-    :class: code
-
-    
-    log\_config= dict(
-        version= 1,
-        handlers= {
-            'console': {
-                'class': 'logging.StreamHandler',
-                'level': logging.INFO,
-                'stream': 'ext://sys.stderr',
-                'formatter': 'basic',
-            },
-        },
-        formatters = {
-            'basic': {
-                'format': "{levelname}:{name}:{message}",
-                'style': "{",
-            }
-        },
-        
-        root= { 'level': logging.INFO, 'handlers': ['console'] },
-        loggers= {
-            'pyweb.TanglerMake': { 'level': logging.WARN },
-            'pyweb.WebReader': { 'level': logging.WARN },
-        },
-    )
-
-..
-
-    ..  class:: small
-
-        |loz| *Logging Setup (161)*. Used by: pyweb.py (`148`_)
-
-
-This is a bit verbose; a separate configuration file might be better.
-
-Also, we might want a decorator to define loggers consistently for each class.
-
-
-The Main Function
-------------------
-
-The top-level interface is the ``main()`` function.
-This function creates an ``Application`` instance.
-
-The ``Application`` object parses the command-line arguments.
-Then the ``Application`` object does the requested processing.
-This two-step process allows for some dependency injection to customize argument processing.
-
-We might also want to parse a logging configuration file, as well
-as a weaver template configuration file.
-
-
-..  _`162`:
-..  rubric:: Interface Functions (162) =
-..  parsed-literal::
-    :class: code
-
-    
-    def main():
-        a= Application()
-        config= a.parseArgs()
-        a.process(config)
-    
-    if \_\_name\_\_ == "\_\_main\_\_":
-        with Logger( log\_config ) as logger:
-            logging.getLogger( "pyweb.TanglerMake" ).setLevel( logging.WARN )
-            logging.getLogger( "pyweb.WebReader" ).setLevel( logging.WARN )
-            main( )
-
-..
-
-    ..  class:: small
-
-        |loz| *Interface Functions (162)*. Used by: pyweb.py (`148`_)
-
-
-This can be extended by doing something like the following.
-
-1.  Subclass ``Weaver`` create a subclass with different templates.
-
-2.  Update the ``pyweb.weavers`` dictionary.
-
-3.  Call ``pyweb.main()`` to run the existing
-    main program with extra classes available to it.
-
-
-..  parsed-literal::
-
-    import pyweb
-    class MyWeaver( HTML ):
-       *Any template changes*
-     
-    pyweb.weavers['myweaver']= MyWeaver()
-    pyweb.main()
-
-
-This will create a variant on **pyWeb** that will handle a different
-weaver via the command-line option ``-w myweaver``.
-
-
-
-..  pyweb/test.w
-
-Unit Tests
-===========
-
-The ``test`` directory includes ``pyweb_test.w``, which will create a 
-complete test suite.
-
-This source will weaves a ``pyweb_test.html`` file. See file:test/pyweb_test.html
-
-This source will tangle several test modules:  ``test.py``, ``test_tangler.py``, ``test_weaver.py``,
-``test_loader.py`` and ``test_unit.py``.  Running the ``test.py`` module will include and
-execute all 71 tests.
-
-Here's a script that works out well for running this without disturbing the development
-environment. The ``PYTHONPATH`` setting is essential to support importing ``pyweb``.
-
-..	parsed-literal::
-
-	cd test
-	python ../pyweb.py pyweb_test.w
-	PYTHONPATH=.. python test.py
-
-
-
-..	pyweb/additional.w
-
-Additional Files
-================
-
-Two aditional scripts are provided as examples 
-which an be customized.
-
-The ``README`` and ``setup.py`` files are also an important part of the
-distribution.
-
-The ``.CSS`` file and ``.conf`` file for RST production are also provided here.
-
-``tangle.py`` Script
----------------------
-
-This script shows a simple version of Tangling.  This has a permitted 
-error for '@i' commands to allow an include file (for example test results)
-to be omitted from the tangle operation.
-
-
-..  _`163`:
-..  rubric:: tangle.py (163) =
-..  parsed-literal::
-    :class: code
-
-    #!/usr/bin/env python
-    """Sample tangle.py script."""
-    import pyweb
-    import logging, sys
-    
-    logging.basicConfig( stream=sys.stderr, level=logging.INFO )
-    logger= logging.getLogger(\_\_file\_\_)
-    
-    w= pyweb.Web( "pyweb.w" ) # The web we'll work on.
-    
-    permitList= ['@i']
-    commandChar= '@'
-    load= pyweb.LoadAction()
-    load.webReader= pyweb.WebReader( command=commandChar, permit=permitList )
-    load.webReader.web( w ).source( "pyweb.w" )
-    load.web= w
-    load()
-    logger.info( load.summary() )
-    
-    tangle= pyweb.TangleAction()
-    tangle.theTangler= pyweb.TanglerMake()
-    tangle.web= w
-    tangle()
-    logger.info( tangle.summary() )
-
-..
-
-    ..  class:: small
-
-        |loz| *tangle.py (163)*.
-
-
-``weave.py`` Script
----------------------
-
-This script shows a simple version of Weaving.  This shows how
-to define a customized set of templates for a different markup language.
-
-
-A customized weaver generally has three parts.
-
-
-..  _`164`:
-..  rubric:: weave.py (164) =
-..  parsed-literal::
-    :class: code
-
-    |srarr|\ weave.py overheads for correct operation of a script (`165`_)
-    |srarr|\ weave.py weaver definition to customize the Weaver being used (`166`_)
-    |srarr|\ weaver.py actions to load and weave the document (`167`_)
-
-..
-
-    ..  class:: small
-
-        |loz| *weave.py (164)*.
-
-
-
-..  _`165`:
-..  rubric:: weave.py overheads for correct operation of a script (165) =
-..  parsed-literal::
-    :class: code
-
-    #!/usr/bin/env python
-    """Sample weave.py script."""
-    import pyweb
-    import logging, sys, string
-    
-    logging.basicConfig( stream=sys.stderr, level=logging.INFO )
-    logger= logging.getLogger(\_\_file\_\_)
-
-..
-
-    ..  class:: small
-
-        |loz| *weave.py overheads for correct operation of a script (165)*. Used by: weave.py (`164`_)
-
-
-
-..  _`166`:
-..  rubric:: weave.py weaver definition to customize the Weaver being used (166) =
-..  parsed-literal::
-    :class: code
-
-    
-    class MyHTML( pyweb.HTML ):
-        """HTML formatting templates."""
-        extension= ".html"
-        
-        cb\_template= string.Template("""
-        
-        

${fullName} (${seq}) ${concat}

-
\\n""")
-    
-        ce\_template= string.Template("""
-        
-

${fullName} (${seq}). - ${references} -

\\n""") - - fb\_template= string.Template(""" - -

\`\`${fullName}\`\` (${seq}) ${concat}

-
\\n""") # Prevent indent
-            
-        fe\_template= string.Template( """
-

◊ \`\`${fullName}\`\` (${seq}). - ${references} -

\\n""") - - ref\_item\_template = string.Template( - '${fullName} (${seq})' - ) - - ref\_template = string.Template( ' Used by ${refList}.' ) - - refto\_name\_template = string.Template( - '${fullName} (${seq})' - ) - refto\_seq\_template = string.Template( '(${seq})' ) - - xref\_head\_template = string.Template( "
\\n" ) - xref\_foot\_template = string.Template( "
\\n" ) - xref\_item\_template = string.Template( "
${fullName}
${refList}
\\n" ) - - name\_def\_template = string.Template( '•${seq}' ) - name\_ref\_template = string.Template( '${seq}' ) - -.. - - .. class:: small - - |loz| *weave.py weaver definition to customize the Weaver being used (166)*. Used by: weave.py (`164`_) - - - -.. _`167`: -.. rubric:: weaver.py actions to load and weave the document (167) = -.. parsed-literal:: - :class: code - - - w= pyweb.Web( "pyweb.w" ) # The web we'll work on. - - permitList= [] - commandChar= '@' - load= pyweb.LoadAction() - load.webReader= pyweb.WebReader( command=commandChar, permit=permitList ) - load.webReader.web( w ).source( "pyweb.w" ) - load.web= w - load() - logger.info( load.summary() ) - - weave= pyweb.WeaveAction() - weave.theWeaver= MyHTML() - weave.web= w - weave() - logger.info( weave.summary() ) - -.. - - .. class:: small - - |loz| *weaver.py actions to load and weave the document (167)*. Used by: weave.py (`164`_) - - -The ``setup.py`` and ``manifest.in`` files --------------------------------------------- - -In order to support a pleasant installation, the ``setup.py`` file is helpful. - - -.. _`168`: -.. rubric:: setup.py (168) = -.. parsed-literal:: - :class: code - - #!/usr/bin/env python - """Setup for pyWeb.""" - - from distutils.core import setup - - setup(name='pyweb', - version='2.3', - description='pyWeb 2.3: In Python, Yet Another Literate Programming Tool', - author='S. Lott', - author\_email='s\_lott@yahoo.com', - url='http://slott-softwarearchitect.blogspot.com/', - py\_modules=['pyweb'], - classifiers=[ - 'Intended Audience :: Developers', - 'Topic :: Documentation', - 'Topic :: Software Development :: Documentation', - 'Topic :: Text Processing :: Markup', - ] - ) - -.. - - .. class:: small - - |loz| *setup.py (168)*. - - -In order build a source distribution kit the ``setup.py sdist`` requires a -``MANIFEST``. We can either list all files or provide a ``MANIFEST.in`` -that specifies additional rules. -We use a simple inclusion to augment the default manifest rules. - - -.. _`169`: -.. rubric:: MANIFEST.in (169) = -.. parsed-literal:: - :class: code - - include \*.w \*.css \*.html - include test/\*.w test/\*.css test/\*.html test/\*.py - include jedit/\* - -.. - - .. class:: small - - |loz| *MANIFEST.in (169)*. - - -The ``README`` file ---------------------- - -Generally, a ``README`` is also considered to be good form. - - -.. _`170`: -.. rubric:: README (170) = -.. parsed-literal:: - :class: code - - pyWeb 2.3: In Python, Yet Another Literate Programming Tool - - Literate programming is an attempt to reconcile the opposing needs - of clear presentation to people with the technical issues of - creating code that will work with our current set of tools. - - Presentation to people requires extensive and sophisticated typesetting - techniques. Further, the "narrative arc" of a presentation may not - follow the source code as layed out for the compiler. - - pyWeb is a literate programming tool that combines the actions - of weaving a document with tangling source files. - It is independent of any particular document markup or source language. - Is uses a simple set of markup tags to define chunks of code and - documentation. - - The pyweb.w file is the source for the various pyweb module and script files, plus - the pyweb.html file. The various source code files are created by applying a - tangle operation to the .w file. The final documentation is created by - applying a weave operation to the .w file. - - Installation - ------------- - - :: - - python setup.py install - - This will install the pyweb module. - - Document production - -------------------- - - The supplied documentation uses RST markup, and requires docutils. - - :: - - python pyweb.py pyweb.w - rst2html.py pyweb.rst pyweb.html - - Authoring - --------- - - The pyweb document describes the simple markup used to define code chunks - and assemble those code chunks into a coherent document as well as working code. - - If you're a JEdit user, the \`\`jedit\`\` directory can be used - to configure syntax highlighting that includes PyWeb and RST. - - Operation - --------- - - You can then run pyweb with - - :: - - python -m pyweb pyweb.w - - This will create the various output files from the source .w file. - - - pyweb.html is the final woven document. - - - pyweb.py, tangle.py, weave.py, readme, setup.py and MANIFEST.in are tangled output files. - - Testing - ------- - - The test directory includes pyweb\_test.w, which will create a - complete test suite. - - This weaves a pyweb\_test.html file. - - This tangles several test modules: test.py, test\_tangler.py, test\_weaver.py, - test\_loader.py and test\_unit.py. Running the test.py module will include and - execute all 71 tests. - - :: - - cd test - python ../pyweb.py pyweb\_test.w - PYTHONPATH=.. python test.py - rst2html.py pyweb\_test.rst pyweb\_test.html - - - -.. - - .. class:: small - - |loz| *README (170)*. - - -The CSS Files -------------- - -To get the RST to look good, there are two additional files. - -``docutils.conf`` defines two CSS files to use. - The default CSS file may need to be customized. - - -.. _`171`: -.. rubric:: docutils.conf (171) = -.. parsed-literal:: - :class: code - - # docutils.conf - - [html4css1 writer] - stylesheet-path: /Library/Frameworks/Python.framework/Versions/3.3/lib/python3.3/site-packages/docutils-0.11-py3.3.egg/docutils/writers/html4css1/html4css1.css, - page-layout.css - syntax-highlight: long - -.. - - .. class:: small - - |loz| *docutils.conf (171)*. - - -``page-layout.css`` This tweaks one CSS to be sure that -the resulting HTML pages are easier to read. These are minor -tweaks to the default CSS. - - -.. _`172`: -.. rubric:: page-layout.css (172) = -.. parsed-literal:: - :class: code - - /\* Page layout tweaks \*/ - div.document { width: 7in; } - .small { font-size: smaller; } - .code - { - color: #101080; - display: block; - border-color: black; - border-width: thin; - border-style: solid; - background-color: #E0FFFF; - /\*#99FFFF\*/ - padding: 0 0 0 1%; - margin: 0 6% 0 6%; - text-align: left; - font-size: smaller; - } - -.. - - .. class:: small - - |loz| *page-layout.css (172)*. - - -.. pyweb/jedit.w - -JEdit Configuration -==================== - -Here's the ``pyweb.xml`` file that you'll need to configure -JEdit so that it properly highlights your PyWeb commands. - -We'll define the overall properties plus two sets of rules. - -.. _`173`: -.. rubric:: jedit/pyweb.xml (173) = -.. parsed-literal:: - :class: code - - - - - - |srarr|\ props for JEdit mode (`174`_) - |srarr|\ rules for JEdit PyWeb and RST (`175`_) - |srarr|\ rules for JEdit PyWeb XML-Like Constructs (`176`_) - - -.. - - .. class:: small - - |loz| *jedit/pyweb.xml (173)*. - - -Here are some properties to define RST constructs to JEdit - -.. _`174`: -.. rubric:: props for JEdit mode (174) = -.. parsed-literal:: - :class: code - - - - - - - - - -.. - - .. class:: small - - |loz| *props for JEdit mode (174)*. Used by: jedit/pyweb.xml (`173`_) - - -Here are some rules to define PyWeb and RST constructs to JEdit. - - -.. _`175`: -.. rubric:: rules for JEdit PyWeb and RST (175) = -.. parsed-literal:: - :class: code - - - - - - \_\_ - .. \_ - - - ={3,} - -{3,} - ~{3,} - #{3,} - "{3,} - \\^{3,} - \\+{3,} - \\\*{3,} - - - \\.\\.\\s\\\|[^\|]+\\\| - - - \\\|[^\|]+\\\| - - - \\.\\.\\s[A-z][A-z0-9-\_]+:: - - - \\\*\\\*[^\*]+\\\*\\\* - - - \\\*[^\\s\*][^\*]\*\\\* - - - .. - - - \`[A-z0-9]+[^\`]+\`\_{1,2} - - - \\[[0-9]+\\]\_ - - - \\[#[A-z0-9\_]\*\\]\_ - - - [\*]\_ - - - \\[[A-z][A-z0-9\_-]\*\\]\_ - - - - - \`\` - \`\` - - - - - @d - @o - - - @{ - @} - - - - \` - \` - - - \`{3,} - - - :[A-z][A-z0-9 =\\s\\t\_]\*: - - - \\+-[+-]+ - \\+=[+=]+ - - - -.. - - .. class:: small - - |loz| *rules for JEdit PyWeb and RST (175)*. Used by: jedit/pyweb.xml (`173`_) - - -Here are some additional rules to define PyWeb constructs to JEdit -that look like XML. - - -.. _`176`: -.. rubric:: rules for JEdit PyWeb XML-Like Constructs (176) = -.. parsed-literal:: - :class: code - - - - - @< - @> - - - -.. - - .. class:: small - - |loz| *rules for JEdit PyWeb XML-Like Constructs (176)*. Used by: jedit/pyweb.xml (`173`_) - - -Additionally, you'll want to update the JEdit catalog. - -.. parsed-literal:: - - - - - - - - - - -.. End - - -Indices -======= - -Files ------- - - -:MANIFEST.in: - |srarr|\ (`169`_) -:README: - |srarr|\ (`170`_) -:docutils.conf: - |srarr|\ (`171`_) -:jedit/pyweb.xml: - |srarr|\ (`173`_) -:page-layout.css: - |srarr|\ (`172`_) -:pyweb.py: - |srarr|\ (`148`_) -:setup.py: - |srarr|\ (`168`_) -:tangle.py: - |srarr|\ (`163`_) -:weave.py: - |srarr|\ (`164`_) - - - -Macros ------- - - -:Action call method actually does the real work: - |srarr|\ (`133`_) -:Action class hierarchy - used to describe basic actions of the application: - |srarr|\ (`131`_) -:Action final summary of what was done: - |srarr|\ (`134`_) -:Action superclass has common features of all actions: - |srarr|\ (`132`_) -:ActionSequence append adds a new action to the sequence: - |srarr|\ (`137`_) -:ActionSequence call method delegates the sequence of ations: - |srarr|\ (`136`_) -:ActionSequence subclass that holds a sequence of other actions: - |srarr|\ (`135`_) -:ActionSequence summary summarizes each step: - |srarr|\ (`138`_) -:Application Class: - |srarr|\ (`154`_) |srarr|\ (`155`_) -:Application class process all files: - |srarr|\ (`158`_) -:Application default options: - |srarr|\ (`156`_) -:Application parse command line: - |srarr|\ (`157`_) -:Base Class Definitions: - |srarr|\ (`1`_) -:Chunk add to the web: - |srarr|\ (`55`_) -:Chunk append a command: - |srarr|\ (`53`_) -:Chunk append text: - |srarr|\ (`54`_) -:Chunk class: - |srarr|\ (`52`_) -:Chunk class hierarchy - used to describe input chunks: - |srarr|\ (`51`_) -:Chunk examination: starts with, matches pattern: - |srarr|\ (`57`_) -:Chunk generate references from this Chunk: - |srarr|\ (`58`_) -:Chunk references to this Chunk: - |srarr|\ (`59`_) -:Chunk superclass make Content definition: - |srarr|\ (`56`_) -:Chunk tangle this Chunk into a code file: - |srarr|\ (`61`_) -:Chunk weave this Chunk into the documentation: - |srarr|\ (`60`_) -:CodeCommand class to contain a program source code block: - |srarr|\ (`79`_) -:Command analysis features: starts-with and Regular Expression search: - |srarr|\ (`76`_) -:Command class hierarchy - used to describe individual commands: - |srarr|\ (`74`_) -:Command superclass: - |srarr|\ (`75`_) -:Command tangle and weave functions: - |srarr|\ (`77`_) -:Emitter class hierarchy - used to control output files: - |srarr|\ (`2`_) -:Emitter core open, close and write: - |srarr|\ (`4`_) -:Emitter doClose, to be overridden by subclasses: - |srarr|\ (`6`_) -:Emitter doOpen, to be overridden by subclasses: - |srarr|\ (`5`_) -:Emitter indent control: set, clear and reset: - |srarr|\ (`10`_) -:Emitter superclass: - |srarr|\ (`3`_) -:Emitter write a block of code: - |srarr|\ (`7`_) |srarr|\ (`8`_) |srarr|\ (`9`_) -:Error class - defines the errors raised: - |srarr|\ (`92`_) -:FileXrefCommand class for an output file cross-reference: - |srarr|\ (`81`_) -:HTML code chunk begin: - |srarr|\ (`33`_) -:HTML code chunk end: - |srarr|\ (`34`_) -:HTML output file begin: - |srarr|\ (`35`_) -:HTML output file end: - |srarr|\ (`36`_) -:HTML reference to a chunk: - |srarr|\ (`39`_) -:HTML references summary at the end of a chunk: - |srarr|\ (`37`_) -:HTML short references summary at the end of a chunk: - |srarr|\ (`42`_) -:HTML simple cross reference markup: - |srarr|\ (`40`_) -:HTML subclass of Weaver: - |srarr|\ (`31`_) |srarr|\ (`32`_) -:HTML write a line of code: - |srarr|\ (`38`_) -:HTML write user id cross reference line: - |srarr|\ (`41`_) -:Imports: - |srarr|\ (`11`_) |srarr|\ (`47`_) |srarr|\ (`115`_) |srarr|\ (`123`_) |srarr|\ (`149`_) |srarr|\ (`153`_) |srarr|\ (`159`_) -:Interface Functions: - |srarr|\ (`162`_) -:LaTeX code chunk begin: - |srarr|\ (`24`_) -:LaTeX code chunk end: - |srarr|\ (`25`_) -:LaTeX file output begin: - |srarr|\ (`26`_) -:LaTeX file output end: - |srarr|\ (`27`_) -:LaTeX reference to a chunk: - |srarr|\ (`30`_) -:LaTeX references summary at the end of a chunk: - |srarr|\ (`28`_) -:LaTeX subclass of Weaver: - |srarr|\ (`23`_) -:LaTeX write a line of code: - |srarr|\ (`29`_) -:LoadAction call method loads the input files: - |srarr|\ (`146`_) -:LoadAction subclass loads the document web: - |srarr|\ (`145`_) -:LoadAction summary provides lines read: - |srarr|\ (`147`_) -:Logging Setup: - |srarr|\ (`160`_) |srarr|\ (`161`_) -:MacroXrefCommand class for a named chunk cross-reference: - |srarr|\ (`82`_) -:NamedChunk add to the web: - |srarr|\ (`64`_) -:NamedChunk class: - |srarr|\ (`62`_) -:NamedChunk tangle into the source file: - |srarr|\ (`66`_) -:NamedChunk user identifiers set and get: - |srarr|\ (`63`_) -:NamedChunk weave into the documentation: - |srarr|\ (`65`_) -:NamedDocumentChunk class: - |srarr|\ (`71`_) -:NamedDocumentChunk tangle: - |srarr|\ (`73`_) -:NamedDocumentChunk weave: - |srarr|\ (`72`_) -:OutputChunk add to the web: - |srarr|\ (`68`_) -:OutputChunk class: - |srarr|\ (`67`_) -:OutputChunk tangle: - |srarr|\ (`70`_) -:OutputChunk weave: - |srarr|\ (`69`_) -:Overheads: - |srarr|\ (`150`_) |srarr|\ (`151`_) |srarr|\ (`152`_) -:RST subclass of Weaver: - |srarr|\ (`22`_) -:Reference class hierarchy - references to a chunk: - |srarr|\ (`89`_) |srarr|\ (`90`_) |srarr|\ (`91`_) -:ReferenceCommand class for chunk references: - |srarr|\ (`84`_) -:ReferenceCommand refers to a chunk: - |srarr|\ (`86`_) -:ReferenceCommand resolve a referenced chunk name: - |srarr|\ (`85`_) -:ReferenceCommand tangle a referenced chunk: - |srarr|\ (`88`_) -:ReferenceCommand weave a reference to a chunk: - |srarr|\ (`87`_) -:TangleAction call method does tangling of the output files: - |srarr|\ (`143`_) -:TangleAction subclass initiates the tangle action: - |srarr|\ (`142`_) -:TangleAction summary method provides total lines tangled: - |srarr|\ (`144`_) -:Tangler code chunk begin: - |srarr|\ (`45`_) -:Tangler code chunk end: - |srarr|\ (`46`_) -:Tangler doOpen, and doClose overrides: - |srarr|\ (`44`_) -:Tangler subclass of Emitter to create source files with no markup: - |srarr|\ (`43`_) -:TanglerMake doClose override, comparing temporary to original: - |srarr|\ (`50`_) -:TanglerMake doOpen override, using a temporary file: - |srarr|\ (`49`_) -:TanglerMake subclass which is make-sensitive: - |srarr|\ (`48`_) -:TextCommand class to contain a document text block: - |srarr|\ (`78`_) -:Tokenizer class - breaks input into tokens: - |srarr|\ (`111`_) -:UserIdXrefCommand class for a user identifier cross-reference: - |srarr|\ (`83`_) -:WeaveAction call method to pick the language: - |srarr|\ (`140`_) -:WeaveAction subclass initiates the weave action: - |srarr|\ (`139`_) -:WeaveAction summary of language choice: - |srarr|\ (`141`_) -:Weaver code chunk begin-end: - |srarr|\ (`17`_) -:Weaver cross reference output methods: - |srarr|\ (`20`_) |srarr|\ (`21`_) -:Weaver doOpen, doClose and setIndent overrides: - |srarr|\ (`13`_) -:Weaver document chunk begin-end: - |srarr|\ (`15`_) -:Weaver file chunk begin-end: - |srarr|\ (`18`_) -:Weaver quoted characters: - |srarr|\ (`14`_) -:Weaver reference command output: - |srarr|\ (`19`_) -:Weaver reference summary, used by code chunk and file chunk: - |srarr|\ (`16`_) -:Weaver subclass of Emitter to create documentation: - |srarr|\ (`12`_) -:Web Chunk check reference counts are all one: - |srarr|\ (`102`_) -:Web Chunk cross reference methods: - |srarr|\ (`101`_) |srarr|\ (`103`_) |srarr|\ (`104`_) |srarr|\ (`105`_) -:Web Chunk name resolution methods: - |srarr|\ (`99`_) |srarr|\ (`100`_) -:Web add a named macro chunk: - |srarr|\ (`97`_) -:Web add an anonymous chunk: - |srarr|\ (`96`_) -:Web add an output file definition chunk: - |srarr|\ (`98`_) -:Web add full chunk names, ignoring abbreviated names: - |srarr|\ (`95`_) -:Web class - describes the overall "web" of chunks: - |srarr|\ (`93`_) -:Web construction methods used by Chunks and WebReader: - |srarr|\ (`94`_) -:Web determination of the language from the first chunk: - |srarr|\ (`108`_) -:Web tangle the output files: - |srarr|\ (`109`_) -:Web weave the output document: - |srarr|\ (`110`_) -:WebReader class - parses the input file, building the Web structure: - |srarr|\ (`112`_) -:WebReader command literals: - |srarr|\ (`130`_) -:WebReader fluent setter methods: - |srarr|\ (`128`_) -:WebReader handle a command string: - |srarr|\ (`113`_) |srarr|\ (`126`_) -:WebReader load the web: - |srarr|\ (`129`_) -:WebReader location in the input stream: - |srarr|\ (`127`_) -:XrefCommand superclass for all cross-reference commands: - |srarr|\ (`80`_) -:add a reference command to the current chunk: - |srarr|\ (`122`_) -:add an expression command to the current chunk: - |srarr|\ (`124`_) -:assign user identifiers to the current chunk: - |srarr|\ (`121`_) -:collect all user identifiers from a given map into ux: - |srarr|\ (`106`_) -:double at-sign replacement, append this character to previous TextCommand: - |srarr|\ (`125`_) -:find user identifier usage and update ux from the given map: - |srarr|\ (`107`_) -:finish a chunk, start a new Chunk adding it to the web: - |srarr|\ (`119`_) -:import another file: - |srarr|\ (`118`_) -:major commands segment the input into separate Chunks: - |srarr|\ (`114`_) -:minor commands add Commands to the current Chunk: - |srarr|\ (`120`_) -:props for JEdit mode: - |srarr|\ (`174`_) -:rules for JEdit PyWeb XML-Like Constructs: - |srarr|\ (`176`_) -:rules for JEdit PyWeb and RST: - |srarr|\ (`175`_) -:start a NamedChunk or NamedDocumentChunk, adding it to the web: - |srarr|\ (`117`_) -:start an OutputChunk, adding it to the web: - |srarr|\ (`116`_) -:weave.py overheads for correct operation of a script: - |srarr|\ (`165`_) -:weave.py weaver definition to customize the Weaver being used: - |srarr|\ (`166`_) -:weaver.py actions to load and weave the document: - |srarr|\ (`167`_) - - - -User Identifiers ----------------- - - -:Action: - [`132`_] `135`_ `139`_ `142`_ `145`_ -:ActionSequence: - [`135`_] `156`_ -:Application: - [`154`_] `162`_ -:Chunk: - [`52`_] `58`_ `62`_ `88`_ `93`_ `101`_ `109`_ `118`_ `119`_ `122`_ `129`_ -:CodeCommand: - `62`_ [`79`_] -:Command: - `53`_ [`75`_] `78`_ `80`_ `84`_ `88`_ -:Emitter: - [`3`_] `12`_ `43`_ -:Error: - `58`_ `60`_ `61`_ `65`_ `66`_ `69`_ `72`_ `73`_ `80`_ `88`_ [`92`_] `97`_ `99`_ `100`_ `109`_ `110`_ `113`_ `118`_ `121`_ `124`_ `126`_ `140`_ `143`_ `146`_ -:FileXrefCommand: - [`81`_] `120`_ -:HTML: - `31`_ [`32`_] `108`_ `151`_ `155`_ `166`_ -:LaTeX: - [`23`_] `108`_ `151`_ `155`_ -:LoadAction: - [`145`_] `156`_ `163`_ `167`_ -:MacroXrefCommand: - [`82`_] `120`_ -:NamedChunk: - [`62`_] `67`_ `71`_ `117`_ -:NamedDocumentChunk: - [`71`_] `117`_ -:OutputChunk: - [`67`_] `116`_ -:ReferenceCommand: - [`84`_] `122`_ -:TangleAction: - [`142`_] `156`_ `163`_ -:Tangler: - `3`_ [`43`_] `48`_ -:TanglerMake: - [`48`_] `157`_ `161`_ `162`_ `163`_ -:TextCommand: - `54`_ `56`_ `66`_ `71`_ [`78`_] `79`_ -:Tokenizer: - [`111`_] `129`_ -:UserIdXrefCommand: - [`83`_] `120`_ -:WeaveAction: - [`139`_] `156`_ `167`_ -:Weaver: - [`12`_] `22`_ `23`_ `31`_ `108`_ `157`_ -:Web: - `55`_ `64`_ `68`_ [`93`_] `158`_ `163`_ `167`_ -:WebReader: - `88`_ [`112`_] `118`_ `157`_ `161`_ `162`_ `163`_ `167`_ -:XrefCommand: - [`80`_] `81`_ `82`_ `83`_ -:__version__: - `124`_ [`152`_] -:_gatherUserId: - [`105`_] -:_updateUserId: - [`105`_] -:add: - `55`_ [`96`_] -:addDefName: - [`95`_] `97`_ `122`_ -:addNamed: - `64`_ [`97`_] -:addOutput: - `68`_ [`98`_] -:append: - `10`_ `13`_ `53`_ `54`_ `91`_ `96`_ `97`_ `98`_ `101`_ `107`_ `120`_ `122`_ [`137`_] -:appendText: - [`54`_] `122`_ `124`_ `125`_ `129`_ -:argparse: - [`153`_] `156`_ `157`_ -:chunkXref: - `82`_ [`104`_] -:close: - [`4`_] `13`_ `44`_ `50`_ `109`_ `110`_ -:clrIndent: - [`10`_] `65`_ `88`_ -:codeBegin: - [`17`_] `45`_ `65`_ `66`_ -:codeBlock: - [`7`_] `65`_ `79`_ -:codeEnd: - `17`_ [`25`_] `46`_ `65`_ `66`_ -:codeFinish: - `4`_ [`9`_] `13`_ -:createUsedBy: - [`101`_] `146`_ -:datetime: - `124`_ [`149`_] -:doClose: - `4`_ [`6`_] `13`_ `44`_ `50`_ -:doOpen: - `4`_ [`5`_] `13`_ `44`_ `49`_ -:docBegin: - [`15`_] `60`_ -:docEnd: - [`15`_] `60`_ -:duration: - [`134`_] `141`_ `144`_ `147`_ -:expand: - `72`_ `122`_ `156`_ [`157`_] -:expect: - `116`_ `117`_ `122`_ `124`_ [`126`_] -:fileBegin: - `18`_ [`35`_] `69`_ -:fileEnd: - `18`_ [`36`_] `69`_ -:fileXref: - `81`_ [`104`_] -:filecmp: - [`47`_] `50`_ -:formatXref: - [`80`_] `81`_ `82`_ -:fullNameFor: - `65`_ `69`_ `85`_ `95`_ [`99`_] `100`_ `101`_ -:genReferences: - [`58`_] `101`_ -:getUserIDRefs: - [`57`_] `63`_ `106`_ -:getchunk: - `85`_ [`100`_] `101`_ `109`_ `110`_ -:handleCommand: - [`113`_] `129`_ -:language: - [`108`_] `140`_ `151`_ `170`_ -:lineNumber: - `17`_ `18`_ `33`_ `35`_ `45`_ `54`_ `56`_ [`57`_] `62`_ `66`_ `71`_ `75`_ `78`_ `80`_ `84`_ `111`_ `118`_ `120`_ `122`_ `124`_ `125`_ `127`_ `129`_ `166`_ -:load: - `118`_ [`129`_] `146`_ `156`_ `158`_ `163`_ `167`_ -:location: - `121`_ `124`_ `126`_ [`127`_] -:main: - [`162`_] -:makeContent: - `54`_ `56`_ [`62`_] `71`_ -:multi_reference: - `102`_ [`103`_] -:no_definition: - `102`_ [`103`_] -:no_reference: - `102`_ [`103`_] -:open: - [`4`_] `13`_ `44`_ `109`_ `110`_ `118`_ `124`_ `129`_ `146`_ -:os: - `13`_ `44`_ `49`_ `50`_ `124`_ [`149`_] -:parseArgs: - [`157`_] `162`_ -:perform: - [`143`_] -:platform: - `124`_ [`149`_] -:process: - `124`_ [`158`_] `162`_ -:quote: - [`8`_] `79`_ -:quoted_chars: - `8`_ `14`_ [`29`_] `38`_ -:re: - `107`_ `111`_ [`149`_] `170`_ -:ref: - `28`_ `58`_ [`77`_] `86`_ -:referenceTo: - [`19`_] `20`_ `65`_ -:references: - [`16`_] `17`_ `18`_ `25`_ `32`_ `34`_ `36`_ `52`_ `58`_ `102`_ `121`_ `166`_ -:resetIndent: - `3`_ [`10`_] `13`_ -:resolve: - `66`_ [`85`_] `86`_ `87`_ `88`_ `100`_ -:searchForRE: - [`57`_] `76`_ `78`_ `107`_ -:setIndent: - `10`_ [`13`_] `65`_ `88`_ -:setUserIDRefs: - [`63`_] `121`_ -:shlex: - [`115`_] `116`_ -:startswith: - [`57`_] `76`_ `78`_ `99`_ `108`_ `129`_ `158`_ -:string: - [`11`_] `16`_ `17`_ `18`_ `19`_ `20`_ `21`_ `24`_ `25`_ `28`_ `30`_ `33`_ `34`_ `35`_ `36`_ `37`_ `39`_ `40`_ `41`_ `42`_ `165`_ `166`_ -:summary: - [`134`_] `138`_ `141`_ `144`_ `147`_ `158`_ `163`_ `167`_ -:sys: - `124`_ [`149`_] `161`_ `163`_ `165`_ -:tangle: - `45`_ `61`_ `66`_ `67`_ `70`_ `71`_ `73`_ `77`_ [`78`_] `79`_ `80`_ `88`_ `109`_ `143`_ `151`_ `156`_ `163`_ `170`_ -:tangleChunk: - `88`_ [`109`_] -:tempfile: - [`47`_] `49`_ -:time: - `133`_ `134`_ [`149`_] -:types: - `12`_ `124`_ [`149`_] -:usedBy: - [`86`_] -:userNamesXref: - `83`_ [`105`_] -:weave: - `60`_ `65`_ `69`_ `72`_ `77`_ [`78`_] `79`_ `81`_ `82`_ `83`_ `87`_ `110`_ `140`_ `151`_ `156`_ `165`_ `167`_ `170`_ -:weaveChunk: - `87`_ [`110`_] -:weaveReferenceTo: - `60`_ `65`_ [`72`_] `110`_ -:weaveShortReferenceTo: - `60`_ `65`_ [`72`_] `110`_ -:webAdd: - [`55`_] `64`_ `68`_ `116`_ `117`_ `118`_ `119`_ `129`_ -:write: - [`4`_] `7`_ `9`_ `17`_ `18`_ `20`_ `21`_ `45`_ `78`_ `110`_ -:xrefDefLine: - [`21`_] `83`_ -:xrefFoot: - `20`_ [`40`_] `80`_ `83`_ -:xrefHead: - `20`_ [`40`_] `80`_ `83`_ -:xrefLine: - `20`_ [`40`_] `80`_ - - - - ---------- - -.. class:: small - - Created by pyweb.py at Tue Mar 11 10:21:24 2014. - - pyweb.__version__ '2.3'. - - Source jedit.w modified Wed Mar 5 16:46:36 2014. - - Working directory '/Users/slott/Documents/Projects/pyWeb-2.3/pyweb'. diff --git a/jedit.w b/jedit.w deleted file mode 100644 index 554864e..0000000 --- a/jedit.w +++ /dev/null @@ -1,197 +0,0 @@ -.. pyweb/jedit.w - -JEdit Configuration -==================== - -Here's the ``pyweb.xml`` file that you'll need to configure -JEdit so that it properly highlights your PyWeb commands. - -We'll define the overall properties plus two sets of rules. -@o jedit/pyweb.xml @{ - - - - @ - @ - @ - -@} - -Here are some properties to define RST constructs to JEdit -@d props for JEdit mode -@{ - - - - - - -@} - -Here are some rules to define PyWeb and RST constructs to JEdit. - -@d rules for JEdit PyWeb and RST -@{ - - - - __ - .. _ - - - ={3,} - -{3,} - ~{3,} - #{3,} - "{3,} - \^{3,} - \+{3,} - \*{3,} - - - \.\.\s\|[^|]+\| - - - \|[^|]+\| - - - \.\.\s[A-z][A-z0-9-_]+:: - - - \*\*[^*]+\*\* - - - \*[^\s*][^*]*\* - - - .. - - - `[A-z0-9]+[^`]+`_{1,2} - - - \[[0-9]+\]_ - - - \[#[A-z0-9_]*\]_ - - - [*]_ - - - \[[A-z][A-z0-9_-]*\]_ - - - - - `` - `` - - - - - @@d - @@o - - - @@{ - @@} - - - - ` - ` - - - `{3,} - - - :[A-z][A-z0-9 =\s\t_]*: - - - \+-[+-]+ - \+=[+=]+ - - -@} - -Here are some additional rules to define PyWeb constructs to JEdit -that look like XML. - -@d rules for JEdit PyWeb XML... -@{ - - - @@< - @@> - - -@} - -Additionally, you'll want to update the JEdit catalog. - -.. parsed-literal:: - - - - - - - - - - -.. End diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..caeae78 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,24 @@ +[build-system] +requires = ["setuptools >= 61.2.0", "wheel >= 0.37.1", "pytest == 7.1.2", "mypy == 0.910"] +build-backend = "setuptools.build_meta" + +[tool.tox] +legacy_tox_ini = """ +# After 3.1 release, with the -o option in the bootstrap directory. +# We can change this to *also* bootstrap the next release +# From the 3.1 release as part of the tox test suite. +[tox] +envlist = py310 + +[testenv] +deps = + pytest == 7.1.2 + mypy == 0.961 +setenv = + PYTHONPATH = {toxinidir}/src +commands_pre = + python3 src/pyweb.py -o tests tests/pyweb_test.w +commands = + pytest + mypy --strict --show-error-codes src +""" diff --git a/pyweb_t.html b/pyweb_t.html deleted file mode 100644 index 7e9913c..0000000 --- a/pyweb_t.html +++ /dev/null @@ -1,7755 +0,0 @@ - - - - - - -pyWeb Literate Programming 2.3 - - - - -
-

pyWeb Literate Programming 2.3

-

Yet Another Literate Programming Tool

- - - - - -
-

Contents

- -
- -
-

Introduction

-

Literate programming was pioneered by Knuth as a method for -developing readable, understandable presentations of programs. -These would present a program in a literate fashion for people -to read and understand; this would be in parallel with presentation as source text -for a compiler to process and both would be generated from a common source file.

-

One intent is to synchronize the program source with the -documentation about that source. If the program and the documentation -have a common origin, then the traditional gaps between intent -(expressed in the documentation) and action (expressed in the -working program) are significantly reduced.

-

pyWeb is a literate programming tool that combines the actions -of weaving a document with tangling source files. -It is independent of any source language. -It is designed to work with RST document markup. -Is uses a simple set of markup tags to define chunks of code and -documentation.

-
-

Background

-

The following is an almost verbatim quote from Briggs' nuweb documentation, -and provides an apt summary of Literate Programming.

-
-

In 1984, Knuth introduced the idea of literate programming and -described a pair of tools to support the practise (Donald E. Knuth, -"Literate Programming", The Computer Journal 27 (1984), no. 2, 97-111.) -His approach was to combine Pascal code with TeX documentation to -produce a new language, WEB, that offered programmers a superior -approach to programming. He wrote several programs in WEB, -including weave and tangle, the programs used to support -literate programming. -The idea was that a programmer wrote one document, the web file, that -combined documentation written in TeX (Donald E. Knuth, -TeX book, Computers and Typesetting, 1986) with code (written in Pascal).

-

Running tangle on the web file would produce a complete -Pascal program, ready for compilation by an ordinary Pascal compiler. -The primary function of tangle is to allow the programmer to -present elements of the program in any desired order, regardless of -the restrictions imposed by the programming language. Thus, the -programmer is free to present his program in a top-down fashion, -bottom-up fashion, or whatever seems best in terms of promoting -understanding and maintenance.

-

Running weave on the web file would produce a TeX file, ready -to be processed by TeX. The resulting document included a variety of -automatically generated indices and cross-references that made it much -easier to navigate the code. Additionally, all of the code sections -were automatically prettyprinted, resulting in a quite impressive -document.

-

Knuth also wrote the programs for TeX and METAFONT -entirely in WEB, eventually publishing them in book -form. These are probably the -largest programs ever published in a readable form.

-
-
-
-

Other Tools

-

Numerous tools have been developed based on Knuth's initial -work. A relatively complete survey is available at sites -like Literate Programming, -and the OASIS -XML Cover Pages: Literate Programming with SGML and XML.

-

The immediate predecessors to this pyWeb tool are -FunnelWeb, -noweb and -nuweb. The ideas lifted from these other -tools created the foundation for pyWeb.

-

There are several Python-oriented literate programming tools. -These include -LEO, -interscript, -lpy, -py2html, -PyLit.

-

The FunnelWeb tool is independent of any programming language -and only mildly dependent on TeX. -It has 19 commands, many of which duplicate features of HTML or -LaTeX.

-

The noweb tool was written by Norman Ramsey. -This tool uses a sophisticated multi-processing framework, via Unix -pipes, to permit flexible manipulation of the source file to tangle -and weave the programming language and documentation markup files.

-

The nuweb Simple Literate Programming Tool was developed by -Preston Briggs (preston@tera.com). His work was supported by ARPA, -through ONR grant N00014-91-J-1989. It is written -in C, and very focused on producing LaTeX documents. It can -produce HTML, but this is clearly added after the fact. It cannot be -easily extended, and is not object-oriented.

-

The LEO tool is a structured GUI editor for creating -source. It uses XML and noweb-style chunk management. It is more -than a simple weave and tangle tool.

-

The interscript tool is very large and sophisticated, but doesn't gracefully -tolerate HTML markup in the document. It can create a variety of -markup languages from the interscript source, making it suitable for -creating HTML as well as LaTeX.

-

The lpy tool can produce very complex HTML representations of -a Python program. It works by locating documentation markup embedded -in Python comments and docstrings. This is called "inverted literate -programming".

-

The py2html tool does very sophisticated syntax coloring.

-

The PyLit tool is perhaps the very best approach to simple Literate -programming, since it leverages an existing lightweight markup language -and it's output formatting. However, it's limited in the presentation order, -making it difficult to present a complex Python module out of the proper -Python required presentation.

-
-
-

pyWeb

-

pyWeb works with any -programming language. It can work with any markup language, but is currently -configured to work with RST only. This philosophy -comes from FunnelWeb -noweb, nuweb and interscript. The primary differences -between pyWeb and other tools are the following.

-
    -
  • pyWeb is object-oriented, permitting easy extension. -noweb extensions -are separate processes that communicate through a sophisticated protocol. -nuweb is not easily extended without rewriting and recompiling -the C programs.
  • -
  • pyWeb is built in the very portable Python programming -language. This allows it to run anywhere that Python 3.3 runs, with -only the addition of docutils. This makes it a useful -tool for programmers in any language.
  • -
  • pyWeb is much simpler than FunnelWeb, LEO or Interscript. It has -a very limited selection of commands, but can still produce -complex programs and HTML documents.
  • -
  • pyWeb does not invent a complex markup language like Interscript. -Because Iterscript has its own markup, it can generate LaTeX or HTML or other -output formats from a unique input format. While powerful, it seems simpler to -avoid inventing yet another sophisticated markup language. The language pyWeb -uses is very simple, and the author's use their preferred markup language almost -exclusively.
  • -
  • pyWeb supports the forward literate programming philosophy, -where a source document creates programming language and markup language. -The alternative, deriving the document from markup embedded in -program comments ("inverted literate programming"), seems less appealing. -The disadvantage of inverted literate programming is that the final document -can't reflect the original author's preferred order of exposition, -since that informtion generally isn't part of the source code.
  • -
  • pyWeb also specifically rejects some features of nuweb -and FunnelWeb. These include the macro capability with parameter -substitution, and multiple references to a chunk. These two capabilities -can be used to grow object-like applications from non-object programming -languages (e.g. C or Pascal). Since most modern languages (Python, -Java, C++) are object-oriented, this macro capability is more of a problem -than a help.
  • -
  • Since pyWeb is built in the Python interpreter, a source document -can include Python expressions that are evaluated during weave operation to -produce time stamps, source file descriptions or other information in the woven -or tangled output.
  • -
-

pyWeb works with any programming language; it can work with any markup language. -The initial release supports RST via simple templates.

-

The following is extensively quoted from Briggs' nuweb documentation, -and provides an excellent background in the advantages of the very -simple approach started by nuweb and adopted by pyWeb.

-
-

The need to support arbitrary -programming languages has many consequences:

- --- - - - - - - - -
No prettyprinting:
 Both WEB and CWEB are able to -prettyprint the code sections of their documents because they -understand the language well enough to parse it. Since we want to use -any language, we've got to abandon this feature. -However, we do allow particular individual formulas or fragments -of LaTeX -or HTML code to be formatted and still be part of the output files.
Limited index of identifiers:
 Because WEB knows about Pascal, -it is able to construct an index of all the identifiers occurring in -the code sections (filtering out keywords and the standard type -identifiers). Unfortunately, this isn't as easy in our case. We don't -know what an identifier looks like in each language and we certainly -don't know all the keywords. We provide a mechanism to mark -identifiers, and we use a pretty standard pattern for recognizing -identifiers almost most programming languages.
-

Of course, we've got to have some compensation for our losses or the -whole idea would be a waste. Here are the advantages I [Briggs] can see:

- --- - - - - - - - - - - - - - - - -
Simplicity:The majority of the commands in WEB are concerned with control of the -automatic prettyprinting. Since we don't prettyprint, many commands are -eliminated. A further set of commands is subsumed by LaTeX -and may also be eliminated. As a result, our set of commands is reduced to -only about seven members (explained in the next section). -This simplicity is also reflected in the size of this tool, -which is quite a bit smaller than the tools used with other approaches.
No prettyprinting:
 Everyone disagrees about how their code should look, so automatic -formatting annoys many people. One approach is to provide ways to -control the formatting. Our approach is simpler -- we perform no -automatic formatting and therefore allow the programmer complete -control of code layout.
Control:We also offer the programmer reasonably complete control of the -layout of his output files (the files generated during tangling). -Of course, this is essential for languages that are sensitive to layout; -but it is also important in many practical situations, e.g., debugging.
Speed:Since [pyWeb] doesn't do too much, it runs very quickly. -It combines the functions of tangle and weave into a single -program that performs both functions at once.
Chunk numbers:Inspired by the example of noweb, [pyWeb] refers to all program code -chunks by a simple, ascending sequence number through the file. -This becomes the HTML anchor name, also.
Multiple file output:
 The programmer may specify more than one output file in a single [pyWeb] -source file. This is required when constructing programs in a combination of -languages (say, Fortran and C). It's also an advantage when constructing -very large programs.
-
-
-
-

Use Cases

-

pyWeb supports two use cases, Tangle Source Files and Weave Documentation. -These are often combined into a single request of the application that will both -weave and tangle.

-
-

Tangle Source Files

-

A user initiates this process when they have a complete .w file that contains -a description of source files. These source files are described with @o commands -in the .w file.

-

The use case is successful when the source files are produced.

-

Outside this use case, the user will debug those source files, possibly updating the -.w file. This will lead to a need to restart this use case.

-

The use case is a failure when the source files cannot be produced, due to -errors in the .w file. These must be corrected based on information in log messages.

-

The sequence is simply ./pyweb.py *theFile*.w.

-
-
-

Weave Documentation

-

A user initiates this process when they have a .w file that contains -a description of a document to produce. The document is described by the entire -.w file.

-

The use case is successful when the documentation file is produced.

-

Outside this use case, the user will edit the documentation file, possibly updating the -.w file. This will lead to a need to restart this use case.

-

The use case is a failure when the documentation file cannot be produced, due to -errors in the .w file. These must be corrected based on information in log messages.

-

The sequence is simply ./pyweb.py *theFile*.w.

-
-
-

Tangle, Regression Test and Weave

-

A user initiates this process when they have a .w file that contains -a description of a document to produce. The document is described by the entire -.w file. Further, their final document should include regression test output -from the source files created by the tangle operation.

-

The use case is successful when the documentation file is produced, including -current regression test output.

-

Outside this use case, the user will edit the documentation file, possibly updating the -.w file. This will lead to a need to restart this use case.

-

The use case is a failure when the documentation file cannot be produced, due to -errors in the .w file. These must be corrected based on information in log messages.

-

The use case is a failure when the documentation file does not include current -regression test output.

-

The sequence is as follows:

-
-./pyweb.py -xw -pi theFile.w
-python theTest >aLog
-./pyweb.py -xt theFile.w
-
-

The first step excludes weaving and permits errors on the @i command. The -pi option -is necessary in the event that the log file does not yet exist. The second step -runs the regression test, creating a log file. The third step weaves the final document, -including the regression test output.

-
-
-
-

Writing pyWeb .w Files

-

The essence of literate programming is a markup language that distinguishes code -from documentation. For tangling, the code is relevant. For weaving, both code -and documentation are relevant.

-

The pyWeb markup defines a sequence of Chunks. -Each Chunk is either program source code to -be tangled or it is documentation to be woven. The bulk of -the file is typically documentation chunks that describe the program in -some human-oriented markup language like RST, HTML, or LaTeX.

-

The pyWeb tool parses the input, and performs the -tangle and weave operations. It tangles each individual output file -from the program source chunks. It weaves a final documentation file -file from the entire sequence of chunks provided, mixing the author's -original documentation with some markup around the embedded program source.

-

pyWeb markup surrounds the code with tags. Everything else is documentation. -When tangling, the tagged code is assembled into the final file. -When weaving, the tags are replaced with output markup. This means that pyWeb -is not totally independent of the output markup.

-

The code chunks will have their indentation adjusted to match the context in which -they were originally defined. This assures that Python (which relies on indentation) -parses correctly. For other languages, proper indentation is expected but not required.

-

The non-code chunks are not transformed up in any way. Everything that's not -explicitly a code chunk is simply output without modification.

-

All of the pyWeb tags begin with @. This can be changed.

-

The Structural tags (historically called "major commands") partition the input and define the -various chunks. The Inline tags are (called "minor commands") are used to control the -woven and tangled output from those chunks. There are Content tags which generate -summary cross-reference content in woven files.

-
-

Structural Tags

-

There are two definitional tags; these define the various chunks -in an input file.

-

@o file @{ text @}

-
-

The @o (output) command defines a named output file chunk. -The text is tangled to the named -file with no alteration. It is woven into the document -in an appropriate fixed-width font.

-

There are options available to specify comment conventions -for the tangled output; this allows inclusion of source -line numbers.

-
-

@d name @{ text @}

-
-

The @d (define) command defines a named chunk of program source. -This text is tangled -or woven when it is referenced by the reference inline tag.

-

There are options available to specify the indentation for this -particular chunk. In rare cases, it can be helpful to override -the indentation context.

-
-

Each @o and @d tag is followed by a chunk which is -delimited by @{ and @} tags. -At the end of that chunk, there is an optional "major" tag.

-

@|

-
-A chunk may define user identifiers. The list of defined identifiers is placed -in the chunk, separated by the @| separator.
-

Additionally, these tags provide for the inclusion of additional input files. -This is necessary for decomposing a long document into easy-to-edit sections.

-

@i file

-
-The @i (include) command includes another file. The previous chunk -is ended. The file is processed completely, then a new chunk -is started for the text after the @i command.
-

All material that is not explicitly in a @o or @d named chunk is -implicitly collected into a sequence of anonymous document source chunks. -These anonymous chunks form the backbone of the document that is woven. -The anonymous chunks are never tangled into output program source files. -They are woven into the document without any alteration.

-

Note that white space (line breaks ('\n'), tabs and spaces) have no effect on the input parsing. -They are completely preserved on output.

-

The following example has three chunks:

-
-Some RST-format documentation that describes the following piece of the
-program.
-
-@o myFile.py
-@{
-import math
-print( math.pi )
-@| math math.pi
-@}
-
-Some more RST documentation.
-
-

This starts with an anonymous chunk of -documentation. It includes a named output chunk which will write to myFile.py. -It ends with an anonymous chunk of documentation.

-
-
-

Inline Tags

-

There are several tags that are replaced by content in the woven output.

-

@@

-
-The @@ command creates a single @ in the output file. -This is replaced in tangled as well as woven output.
-

@<name@>

-
-The name references a named chunk. -When tangling, the referenced chunk replaces the reference command. -When weaving, a reference marker is used. For example, in RST, this can be -replaced with RST `reference`_ markup. -Note that the indentation prior to the @< tag is preserved -for the tangled chunk that replaces the tag.
-

@(Python expression@)

-
-The Python expression is evaluated and the result is tangled or -woven in place. A few global variables and modules are available. -These are described in Expression Context.
-
-
-

Content Tags

-

There are three index creation tags that are replaced by content in the woven output.

-

@f

-
-The @f command inserts a file cross reference. This -lists the name of each file created by an @o command, and all of the various -chunks that are concatenated to create this file.
-

@m

-
-The @m command inserts a named chunk ("macro") cross reference. This -lists the name of each chunk created by a @d command, and all of the various -chunks that are concatenated to create the complete chunk.
-

@u

-
-The @u command inserts a user identifier cross reference. -This index lists the name of each chunk created by an @d command or @|, -and all of the various chunks that are concatenated to create the complete chunk.
-
-
-

Additional Features

-

Sequence Numbers. The named chunks (from both @o and @d commands) are assigned -unique sequence numbers to simplify cross references.

-

Case Sensitive. Chunk names and file names are case sensitive.

-

Abbreviations. Chunk names can be abbreviated. A partial name can have a trailing ellipsis (...), -this will be resolved to the full name. The most typical use for this -is shown in the following example:

-
-Some RST-format documentation.
-
-@o myFile.py
-@{
-@<imports of the various packages used>
-print( math.pi,time.time() )
-@}
-
-Some notes on the packages used.
-
-@d imports...
-@{
-import math,time
-@| math time
-@}
-
-Some more RST-format documentation.
-
-

This example shows five chunks.

-
    -
  1. An anonymous chunk of documentation.
  2. -
  3. A named chunk that tangles the myFile.py output. It has -a reference to the imports of the various packages used chunk. -Note that the full name of the chunk is essentially a line of -documentation, traditionally done as a comment line in a non-literate -programming environment.
  4. -
  5. An anonymous chunk of documentation.
  6. -
  7. A named chunk with an abbreviated name. The imports... -matches the name imports of the various packages used. -Set off after the @| separator is -the list of user-specified identifiers defined in this chunk.
  8. -
  9. An anonymous chunk of documentation.
  10. -
-

Note that the first time a name appears (in a reference or definition), -it must be the full name. All subsequent uses can be elisions. -Also not that ambiguous elision is an annoying problem when you -first start creating a document.

-

Concatenation. Named chunks are concatenated from their various pieces. -This allows a named chunk to be broken into several pieces, simplifying -the description. This is most often used when producing -fairly complex output files.

-
-An anonymous chunk with some RST documentation.
-
-@o myFile.py
-@{
-import math,time
-@}
-
-Some notes on the packages used.
-
-@o myFile.py
-@{
-print math.pi,time.time()
-@}
-
-Some more HTML documentation.
-
-

This example shows five chunks.

-
    -
  1. An anonymous chunk of documentation.
  2. -
  3. A named chunk that tangles the myFile.py output. It has -the first part of the file. In the woven document -this is marked with "=".
  4. -
  5. An anonymous chunk of documentation.
  6. -
  7. A named chunk that also tangles the myFile.py output. This -chunk's content is appended to the first chunk. In the woven document -this is marked with "+=".
  8. -
  9. An anonymous chunk of documentation.
  10. -
-

Newline Preservation. Newline characters are preserved on input. -Because of this the output may appear to have excessive newlines. -In all of the above examples, each -named chunk was defined with the following.

-
-@{
-import math,time
-@}
-
-

This puts a newline character before and after the import line.

-
-
-

Controlling Indentation

-

We have two choices in indentation:

-
    -
  • Context-Sensitive.
  • -
  • Consistent.
  • -
-

If we have context-sensitive indentation, then the indentation of a chunk reference -is applied to the entire chunk when expanded in place of the reference. This makes it -simpler to prepare source for languages (like Python) where indentation -is important.

-

There are cases, however, when this is not desirable. There are some places in Python -where we want to create long, triple-quoted strings with indentation that does -not follow the prevailing indentations of the surrounding code.

-

Here's how the context-sensitive indentation works.

-
-@o myFile.py
-@{
-def aFunction( a, b ):
-    @<body of aFunction@>
-@| aFunction @}
-
-@d body...
-@{
-"""doc string"""
-return a + b
-@}
-
-

The tangled output from this will look like the following. -All of the newline characters are preserved, and the reference to -body of the aFunction is indented to match the prevailing -indent where it was referenced. In the following example, -explicit line markers of ~ are provided to make the blank lines -more obvious.

-
-~
-~def aFunction( a, b ):
-~
-~    """doc string"""
-~    return a + b
-~
-
-

[The @| command shows that this chunk defines the identifier aFunction.]

-

This leads to a difficult design choice.

-
    -
  • Do we use context-sensitive indentation without any exceptions? -This is the current implementation.

    -
  • -
  • Do we use consistent indentation and require the author to get it right? -This seems to make Python awkward, since we might indent our outdent a -@< name @> command, expecting the chunk to indent properly.

    -
  • -
  • Do we use context-sensitive indentation with an exception indicator? -This seems to go against the utter simplicity we're cribbing from noweb. -However, it makes a great deal of sense to add an option for @d chunks to -supersede context-sensitive indentation. The author must then get it right.

    -

    The syntax to define a section looks like this:

    -
  • -
-
-@d -noindent some chunk name
-@{First partial line
-More that uses """
-@}
-
-

We might reference such a section like this.

-
-@d some bigger chunk...
-@{code
-    @<some chunk name@>
-@}
-
-

This will include the -noindent section by resetting the contextual indentation -to zero. The First partial line line will be output after the four spaces -provided by the some bigger chunk context.

-

After the first newline (More that uses """) will be at the left margin.

-
-
-

Tracking Source Line Numbers

-

Since the tangled output files are -- well -- tangled, it can be difficult to -trace back from a Python error stack to the original line in the .w file that -needs to be fixed.

-

To facilitate this, there is a two-step operation to get more detailed information -on how tangling worked.

-
    -
  1. Use the -n command-line option to get line numbers.
  2. -
  3. Include comment indicators on the @o commands that define output files.
  4. -
-

The expanded syntax for @o looks like this.

-
-@o -start /* -end / page-layout.css
-@{
-*Some CSS code
-@}
-
-

We've added two options: -start /* and -end */ which define comment -start and end syntax. This will lead to comments embedded in the tangled output -which contain source line numbers for every (every!) chunk.

-
-
-

Expression Context

-

There are two possible implementations for evaluation of a Python -expression in the input.

-
    -
  1. Create an ExpressionCommand, and append this to the current Chunk. -This will allow evaluation during weave processing and during tangle processing. This -makes the entire weave (or tangle) context available to the expression, including -completed cross reference information.
  2. -
  3. Evaluate the expression during input parsing, and append the resulting text -as a TextCommand to the current Chunk. This provides a common result -available to both weave and parse, but the only context available is the WebReader and -the incomplete Web, built up to that point.
  4. -
-

In this implementation, we adopt the latter approach, and evaluate expressions immediately. -A simple global context is created with the following variables defined.

- --- - - - - - - - - - - - - - - - - - - - - -
os.path:This is the standard os.path module. The complete os module is not -available. Just this one item.
datetime:This is the standard datetime module.
platform:This is the standard platform module.
__builtins__:Most of the built-ins are available, too. Not all. -exec(), eval(), open() and __import__() aren't available.
theLocation:A tuple with the file name, first line number and last line number -for the original expression's location.
theWebReader:The WebReader instance doing the parsing.
theFile:The .w file being processed.
thisApplication:
 The name of the running pyWeb application. It may not be pyweb.py, -if some other script is being used.
__version__:The version string in the pyWeb application.
-
-
-
-

Running pyWeb to Tangle and Weave

-

Assuming that you have marked pyweb.py as executable, -you do the following.

-
-./pyweb.py file...
-
-

This will tangle the @o commands in each file. -It will also weave the output, and create file.txt.

-
-

Command Line Options

-

Currently, the following command line options are accepted.

- --- - - - - - - - - - - - - - - - -
-v:Verbose logging.
-s:Silent operation.
-cx:Change the command character from @ to *x*.
-wweaver:Choose a particular documentation weaver template. Currently the choices -are RST and HTML.
-xw:Exclude weaving. This does tangling of source program files only.
-xt:Exclude tangling. This does weaving of the document file only.
-pcommand:Permit errors in the given list of commands. The most common -version is -pi to permit errors in locating an include file. -This is done in the following scenario: pass 1 uses -xw -pi to exclude -weaving and permit include-file errors; -the tangled program is run to create test results; pass 2 uses --xt to exclude tangling and include the test results.
-
-
-
-

Bootstrapping

-

pyWeb is written using pyWeb. The distribution includes the original .w -files as well as a .py module.

-

The bootstrap procedure is this.

-
-python pyweb.py pyweb.w
-rst2html.py pyweb.rst pyweb.html
-
-

The resulting pyweb.html file is the final documentation.

-

Similarly, the tests are bootstrapped from .w files.

-
-cd test
-python ../pyweb.py pyweb_test.w
-PYTHONPATH=.. python test.py
-rst2html.py pyweb_test.rst pyweb_test.html
-
-
-
-

Dependencies

-

pyWeb requires Python 3.3 or newer.

-

If you create RST output, you'll want to use docutils to translate -the RST to HTML or LaTeX or any of the other formats supported by docutils.

-
-
-

Acknowledgements

-
-
This application is very directly based on (derived from?) work that
-
preceded this, particularly the following:
-
- -

Also, after using John Skaller's interscript http://interscript.sourceforge.net/ -for two large development efforts, I finally understood the feature set I really needed.

-

Jason Fruit contributed to the previous version.

- -
-
-
-

Architecture and Design Overview

-

This application breaks the overall problem into the following sub-problems.

-
    -
  1. Representation of the Web as Chunks and Commands
  2. -
  3. Reading and parsing the input.
  4. -
  5. Weaving a document file.
  6. -
  7. Tangling the desired program source files.
  8. -
-
-

Representation

-

The basic parse tree has three layers. The source document is transformed into a web, -which is the overall container. The source is -decomposed into a simple sequence of Chunks. Each Chunk is a simple sequence -of Commands.

-

Chunks and Commands cannot be nested, leading to delightful simplification.

-

The overall Web -includes the sequence of Chunks as well as an index for the named chunks.

-

Note that a named chunk may be created through a number of @d commands. -This means that -each named chunk may be a sequence of Chunks with a common name.

-

Because a Chunk is composed of a sequence Commands, the weave and tangle actions can be -delegated to each Chunk, and in turn, delegated to each Command that -composes a Chunk.

-

There is a small interaction between Tanglers and Chunks to work out the indentation. -Otherwise, the output and input work is largely independent of the Web itself.

-
-
-

Reading and Parsing

-

A solution to the reading and parsing problem depends on a convenient -tool for breaking up the input stream and a representation for the chunks of input. -Input decomposition is done with the Python Splitter pattern.

-

The Splitter pattern is widely used in text processing, and has a long legacy -in a variety of languages and libraries. A Splitter decomposes a string into -a sequence of strings using the split pattern. There are many variant implementations. -One variant locates only a single occurence (usually the left-most); this is -commonly implemented as a Find or Search string function. Another variant locates all -occurrences of a specific string or character, and discards the matching string or -character.

-

The variation on Splitter that we use in this application -creates each element in the resulting sequence as either (1) an instance of the -split regular expression or (2) the text between split patterns.

-

We define our splitting pattern with the regular -expression '@.|\n'. This will split on either of these patterns:

-
    -
  • @ followed by a single character,
  • -
  • or, a newline.
  • -
-

For the most part, \n is just text. The exception is the -@i filename command, which ends at the end of the line, making the \n -significant syntax.

-

We could be a tad more specific and use the following as a split pattern: -'@[doOifmu\|<>(){}\[\]]|\n'. This would silently ignore unknown commands, -merging them in with the surrounding text. This would leave the '@@' sequences -completely alone, allowing us to replace '@@' with '@' in -every text chunk.

-

Within the @d and @o commands, we also parse options. These follow -the syntax rules for Tcl or the shell. Optional fields are prefaced with -. -All options come before all positional arguments.

-
-
-

Weaving

-

The weaving operation depends on the target document markup language. -There are several approaches to this problem.

-
    -
  • We can use a markup language unique to pyWeb, -and weave using markup in the desired target language.
  • -
  • We can use a standard markup language and use converters to transform -the standard markup to the desired target markup. We could adopt -XML or RST or some other generic markup that can be converted.
  • -
-

The problem with the second method is the mixture of background document -in some standard markup and the code elements, which need to be bracketed -with common templates. We hate to repeat these templates; that's the -job of a literate programming tool. Also, certain code characters must -be properly escaped.

-

Since pyWeb must transform the code into a specific markup language, -we opt using a Strategy pattern to encapsulate markup language details. -Each alternative markup strategy is then a subclass of Weaver. This -simplifies adding additional markup languages without inventing a -markup language unique to pyWeb. -The author uses their preferred markup, and their preferred -toolset to convert to other output languages.

-

The templates used to wrap code sections can be tweaked relatively easily.

-
-
-

Tangling

-

The tangling operation produces output files. In other tools, -some care was taken to understand the source code context for tangling, and -provide a correct indentation. This required a command-line parameter -to turn off indentation for languages like Fortran, where identation -is not used.

-

In pyWeb, there are two options. The default behavior is that the -indent of a @< command is used to set the indent of the -material is expanded in place of this reference. If all @< commands are presented at the -left margin, no indentation will be done. This is helpful simplification, -particularly for users of Python, where indentation is significant.

-

In rare cases, we might need both, and a @d chunk can override the indentation -rule to force the material to be placed at the left margin.

-
-
-

Application

-

The overall application has two layers to it. There are actions (Load, Tangle, Weave) -as well as a top-level application that parses the command line, creates -and configures the actions, and then closes up shop when all done.

-

The idea is that the Weaver Action should fit with SCons Builder. -We can see Weave( "someFile.w" ) as sensible. Tangling is tougher -because the @o commands define the file dependencies there.

- -
-
-
-

Implementation

-

The implementation is contained in a file that both defines -the base classes and provides an overall main() function. The main() -function uses these base classes to weave and tangle the output files.

-

The broad outline of the presentation is as follows:

- -
-

Base Class Definitions

-

There are the core classes that define the enduring application objects. These form -fairly complex hierarchies.

-
    -
  • Commands. A Command contains user input and creates output. -This can be a block of text from the input file, -one of the various kinds of cross reference commands (@f, @m, or @u) -or a reference to a chunk (via the @<name@> sequence.)
  • -
  • Chunks. A Chunk is a collection of Command instances. This can be -either an anonymous chunk that will be sent directly to the output, -or one the classes of named chunks delimited by the -structural @d or @o commands.
  • -
-

There are classes for reading the input. These don't form a complex hierarchy.

-

The Web as a whole is a collection of Chunk instances. It's built by -a WebReader which uses a Tokenizer.

-

There is a hierarchy for the various kinds of output.

-
    -
  • Emitters. An Emitter creates an output file, either tangled code or some kind of markup from -the chunks that make up the source file. Two major subclasses are the Weaver, which -has a focus on markup output, and Tangler which has a focus on pure source output.

    -

    We have further specialization of the weavers for RST, HTML or LaTeX. The issue is -generating proper markup to surround the code and include cross-references among code -blocks. A number of simple templates are used for this.

    -
  • -
  • Reference Strategy. We can have references resolved transitively or simply. A transitive -reference becomes a list of parent @d NamedChunk instances.

    -
  • -
-

Hovering at the edge of the base class definitions is the Action Class Hierarchy. -It's not an essential part of the base class definitions. But it doesn't seem to -fit elsewhere

-

Base Class Definitions (1) =

-
-→Error class - defines the errors raised (94)
-→Command class hierarchy - used to describe individual commands (76)
-→Chunk class hierarchy - used to describe input chunks (51)
-→Web class - describes the overall "web" of chunks (95)
-→Tokenizer class - breaks input into tokens (132)
-→Option Parser class - locates optional values on commands (134), →(135)
-→WebReader class - parses the input file, building the Web structure (114)
-→Emitter class hierarchy - used to control output files (2)
-→Reference class hierarchy - strategies for references to a chunk (91), →(92), →(93)
-
-→Action class hierarchy - used to describe basic actions of the application (136)
-
- -
-

Base Class Definitions (1). Used by: pyweb.py (153)

-
-
-
-

Emitters

-

An Emitter instance is resposible for control of an output file format. -This includes the necessary file naming, opening, writing and closing operations. -It also includes providing the correct markup for the file type.

-

There are several subclasses of the Emitter superclass, specialized for various file -formats.

-

Emitter class hierarchy - used to control output files (2) =

-
-→Emitter superclass (3)
-→Weaver subclass of Emitter to create documentation (12)
-→RST subclass of Weaver (22)
-→LaTeX subclass of Weaver (23)
-→HTML subclass of Weaver (31), →(32)
-→Tangler subclass of Emitter to create source files with no markup (43)
-→TanglerMake subclass which is make-sensitive (48)
-
- -
-

Emitter class hierarchy - used to control output files (2). Used by: Base Class Definitions (1)

-
-

An Emitter instance is created to contain the various details of -writing an output file. Emitters are created as follows:

-
    -
  • A Web object will create a Weaver to weave the final document.
  • -
  • A Web object will create a Tangler to tangle each file.
  • -
-

Since each Emitter instance is responsible for the details of one file -type, different subclasses of Emitter are used when tangling source code files -(Tangler) and -weaving files that include source code plus markup (Weaver).

-

Further specialization is required when weaving HTML or LaTeX. Generally, this is -a matter of providing three things:

-
    -
  • Boilerplate text to replace various pyWeb constructs,
  • -
  • Escape rules to make source code amenable to the markup language,
  • -
  • A header to provide overall includes or other setup.
  • -
-

An additional part of the escape rules can include using a syntax coloring -toolset instead of simply applying escapes.

-

In the case of tangle, the following algorithm is used:

-
-

Visit each each output Chunk (@o), doing the following:

-
    -
  1. Open the Tangler instance using the target file name.
  2. -
  3. Visit each Chunk directed to the file, calling the chunk's tangle() method.
      -
    1. Call the Tangler's docBegin() method. This sets the Tangler's indents.
    2. -
    3. Visit each Command, call the command's tangle() method. -For the text of the chunk, the -text is written to the tangler using the codeBlock() method. For -references to other chunks, the referenced chunk is tangled using the -referenced chunk's tangler() method.
    4. -
    5. Call the Tangler's docEnd() method. This clears the Tangler's indents.
    6. -
    -
  4. -
-
-

In the case of weave, the following algorithm is used:

-
-
    -
  1. Open the Weaver instance using the source file name. This name is transformed -by the weaver to an output file name appropriate to the language.
  2. -
  3. Visit each each sequential Chunk (anonymous, @d or @o), doing the following:
      -
    1. Visit each Chunk, calling the Chunk's weave() method.
        -
      1. Call the Weaver's docBegin(), fileBegin() or codeBegin() method, -depending on the subclass of Chunk. For -fileBegin() and codeBegin(), this writes the header for -a code chunk in the weaver's markup language.
      2. -
      3. Visit each Command, call the Command's weave() method. -For ordinary text, the -text is written to the Weaver using the codeBlock() method. For -references to other chunks, the referenced chunk is woven using -the Weaver's referenceTo() method.
      4. -
      5. Call the Weaver's docEnd(), fileEnd() or codeEnd() method. -For fileEnd() or codeEnd(), this writes a trailer for -a code chunk in the Weaver's markup language.
      6. -
      -
    2. -
    -
  4. -
-
-
-

Emitter Superclass

-

The Emitter class is not a concrete class; it is never instantiated. It -contains common features factored out of the Weaver and Tangler subclasses.

-

Inheriting from the Emitter class generally requires overriding one or more -of the core methods: doOpen(), and doClose(). -A subclass of Tangler, might override the code writing methods: -quote(), codeBlock() or codeFinish().

-

The Emitter class defines the basic -framework used to create and write to an output file. -This class follows the Template design pattern. This design pattern -directs us to factor the basic open(), close() and write() methods into two step algorithms.

-
-def open( self ):
-    common preparation
-    self.doOpen() #overridden by subclasses
-    return self
-
-

The common preparation section is generally internal -housekeeping. The doOpen() method would be overridden by subclasses to change the -basic behavior.

-

The class has the following attributes:

- --- - - - - - - - - - - - -
fileName:the name of the current open file created by the -open method
theFile:the current open file created by the -open method
linesWritten:the total number of '\n' characters written to the file
totalFiles:count of total number of files
totalLines:count of total number of lines
-

Additionally, an emitter tracks an indentation context used by -The codeBlock() method to indent each line written.

- --- - - - - - - - - - -
context:the indentation context stack, updated by addIndent(), -clrIndent() and readdIndent() methods.
lastIndent:the last indent used after writing a line of source code
fragment:the last line written was a fragment and needs a '\n'.
code_indent:Any initial code indent. RST weavers needs additional code indentation. -Other weavers don't care. Tanglers must have this set to zero.
-

Emitter superclass (3) =

-
-class Emitter:
-    """Emit an output file; handling indentation context."""
-    code_indent= 0 # Used by a Tangler
-    def __init__( self ):
-        self.fileName= ""
-        self.theFile= None
-        self.linesWritten= 0
-        self.totalFiles= 0
-        self.totalLines= 0
-        self.fragment= False
-        self.logger= logging.getLogger( self.__class__.__qualname__ )
-        self.log_indent= logging.getLogger( "indent." + self.__class__.__qualname__ )
-        self.readdIndent( self.code_indent ) # Create context and initial lastIndent values
-    def __str__( self ):
-        return self.__class__.__name__
-    →Emitter core open, close and write (4)
-    →Emitter write a block of code (7), →(8), →(9)
-    →Emitter indent control: set, clear and reset (10)
-
- -
-

Emitter superclass (3). Used by: Emitter class hierarchy... (2)

-
-

The core open() method tracks the open files. -A subclass overrides a doOpen() method to name the output file, and -then actually open the file. The Weaver will create an output file with -a name that's based on the overall project. The Tangler will open the given file -name.

-

The close() method closes the file. As with open(), a -doClose() method actually closes the file. This allows subclasses -to do overrides on the actual file processing.

-

The write() method is the lowest-level, unadorned write. -This does some additional counting as well as writing the -characters to the file.

-

Emitter core open, close and write (4) =

-
-def open( self, aFile ):
-    """Open a file."""
-    self.fileName= aFile
-    self.linesWritten= 0
-    self.doOpen( aFile )
-    return self
-→Emitter doOpen, to be overridden by subclasses (5)
-def close( self ):
-    self.codeFinish() # Trailing newline for tangler only.
-    self.doClose()
-    self.totalFiles += 1
-    self.totalLines += self.linesWritten
-→Emitter doClose, to be overridden by subclasses (6)
-def write( self, text ):
-    if text is None: return
-    self.linesWritten += text.count('\n')
-    self.theFile.write( text )
-
-# Context Manager
-def __enter__( self ):
-    return self
-def __exit__( self, *exc ):
-    self.close()
-    return False
-
- -
-

Emitter core open, close and write (4). Used by: Emitter superclass (3)

-
-

The doOpen(), and doClose() -methods are overridden by the various subclasses to -perform the unique operation for the subclass.

-

Emitter doOpen, to be overridden by subclasses (5) =

-
-def doOpen( self, aFile ):
-    self.logger.debug( "creating {!r}".format(self.fileName) )
-
- -
-

Emitter doOpen, to be overridden by subclasses (5). Used by: Emitter core... (4)

-
-

Emitter doClose, to be overridden by subclasses (6) =

-
-def doClose( self ):
-    self.logger.debug( "wrote {:d} lines to {!s}".format(
-        self.linesWritten, self.fileName) )
-
- -
-

Emitter doClose, to be overridden by subclasses (6). Used by: Emitter core... (4)

-
-

The codeBlock() method writes several lines of code. It calls -the quote() method to quote each line of code after doing the correct indentation. -Often, the last line of code is incomplete, so it is left unterminated. -This last line of code also sets the indentation for any -additional code to be tangled into this section.

-
-

Important

-

Tab characters confuse the indent algorithm. Tabs are -not expanded to spaces in this application. They should be expanded -prior to creating a .w file.

-
-

The algorithm is as follows:

-
    -
  1. Save the topmost value of the context stack as the current indent.

    -
  2. -
  3. Split the block of text on '\n' boundaries. -There are two cases.

    -
      -
    • One line only, no newline.

      -

      Write this with the saved lastIndent. -The lastIndent is reset to zero since we've only written a fragmentary line.

      -
    • -
    • Multiple lines.

      -
        -
      1. Write the first line with saved lastIndent.

        -
      2. -
      3. For each remaining line (except the last), write with the indented text, -ending with a newline.

        -
      4. -
      5. The string split() method will put a trailing -zero-length element in the list if the original block ended with a -newline. We drop this zero length piece to prevent writing a useless fragment -of indent-only after the final '\n'.

        -
      6. -
      7. If the last line has content: Write with the indented text, -but do not write a trailing '\n'. Set lastIndent to zero because -the next codeBlock() will continue this fragmentary line.

        -

        If the last line has no content: Write nothing. -Save the length of the last line as the most recent indent for any @<name@> -reference to.

        -
      8. -
      -
    • -
    -
  4. -
-

This feels a bit too complex. Indentation is a feature of a tangling a reference to -a NamedChunk. It's not really a general feature of emitters or even tanglers.

-

Emitter write a block of code (7) =

-
-def codeBlock( self, text ):
-    """Indented write of a block of code. We buffer
-    The spaces from the last line to act as the indent for the next line.
-    """
-    indent= self.context[-1]
-    lines= text.split( '\n' )
-    if len(lines) == 1: # Fragment with no newline.
-        self.write('{!s}{!s}'.format(self.lastIndent*' ', lines[0]) )
-        self.lastIndent= 0
-        self.fragment= True
-    else:
-        first, rest= lines[:1], lines[1:]
-        self.write('{!s}{!s}\n'.format(self.lastIndent*' ', first[0]) )
-        for l in rest[:-1]:
-            self.write( '{!s}{!s}\n'.format(indent*' ', l) )
-        if rest[-1]:
-            self.write( '{!s}{!s}'.format(indent*' ', rest[-1]) )
-            self.lastIndent= 0
-            self.fragment= True
-        else:
-            # Buffer a next indent
-            self.lastIndent= len(rest[-1]) + indent
-            self.fragment= False
-
- -
-

Emitter write a block of code (7). Used by: Emitter superclass (3)

-
-

The quote() method quotes a single line of source code. -This is used by Weaver subclasses to transform source into -a form acceptable by the final weave file format.

-

In the case of an HTML weaver, the HTML reserved characters -- -<, >, &, and " -- must be replaced in the output -of code with &lt;, &gt;, &amp;, and &quot;. -However, since the author's original document sections contain -HTML these will not be altered.

-

Emitter write a block of code (8) +=

-
-quoted_chars = [
-    # Must be empty for tangling.
-]
-
-def quote( self, aLine ):
-    """Each individual line of code; often overridden by weavers to quote the code."""
-    clean= aLine
-    for from_, to_ in self.quoted_chars:
-        clean= clean.replace( from_, to_ )
-    return clean
-
- -
-

Emitter write a block of code (8). Used by: Emitter superclass (3)

-
-

The codeFinish() method handles a trailing fragmentary line when tangling.

-

Emitter write a block of code (9) +=

-
-def codeFinish( self ):
-    if self.fragment:
-        self.write('\n')
-
- -
-

Emitter write a block of code (9). Used by: Emitter superclass (3)

-
-

These three methods are used when to be sure that the included text is indented correctly with respect to the -surrounding text.

-

The addIndent() method pushes the next indent on the context stack -using an increment to the previous indent.

-

When tangling, a "previous" value is set from the indent left over from the -previous command. This allows @<name@> references to be indented -properly. A tangle must track all nested @d contexts to create a proper -global indent.

-

Weaving, however, is entirely localized to the block of code. There's no -real context tracking. Just "lastIndent" from the previous command's codeBlock().

-

The setIndent() pushes a fixed indent instead adding an increment.

-

The clrIndent() method discards the most recent indent from the context stack. -This is used when finished -tangling a source chunk. This restores the indent to the prevailing indent.

-

The readdIndent() method removes all indent context information and resets the indent -to a default.

-

Weaving may use an initial offset. -It's an additional indent for woven code; not used for tangled code. In particular, RST -requires this. readdIndent() uses this initial offset for weaving.

-

Emitter indent control: set, clear and reset (10) =

-
-def addIndent( self, increment ):
-    self.lastIndent= self.context[-1]+increment
-    self.context.append( self.lastIndent )
-    self.log_indent.debug( "addIndent {!s}: {!r}".format(increment, self.context) )
-def setIndent( self, indent ):
-    self.lastIndent= self.context[-1]
-    self.context.append( indent )
-    self.log_indent.debug( "setIndent {!s}: {!r}".format(indent, self.context) )
-def clrIndent( self ):
-    if len(self.context) > 1:
-        self.context.pop()
-    self.lastIndent= self.context[-1]
-    self.log_indent.debug( "clrIndent {!r}".format(self.context) )
-def readdIndent( self, indent=0 ):
-    self.lastIndent= indent
-    self.context= [self.lastIndent]
-    self.log_indent.debug( "readdIndent {!s}: {!r}".format(indent, self.context) )
-
- -
-

Emitter indent control: set, clear and reset (10). Used by: Emitter superclass (3)

-
-
-
-

Weaver subclass of Emitter

-

A Weaver is an Emitter that produces the final user-focused document. -This will include the source document with the code blocks surrounded by -markup to present that code properly. In effect, the pyWeb @ commands -are replaced by markup.

-

The Weaver class uses a simple set of templates to product RST markup as the default -Subclasses can introduce other templates to produce HTML or LaTeX output.

-

Most Weaver languages don't rely on special indentation rules. -The woven code samples usually start right on the left margin of -the source document. However, the RST markup language does rely -on extra indentation of code blocks. For that reason, the weavers -have an additional indent for code blocks. This is generally -set to zero, except when generating RST where 4 spaces is good.

-

The Weaver subclass extends an Emitter to weave the final -documentation. This involves decorating source code to make it -displayable. It also involves creating references and cross -references among the various chunks.

-

The Weaver class adds several methods to the basic Emitter methods. These -additional methods are also included that are used exclusively when weaving, never when tangling.

-

This class hierarch depends heavily on the string module.

-

Class-level variables include the following

- --- - - - - - - - -
extension:The filename extension used by this weaver.
code_indent:The number of spaces to indent code to separate code blocks from -surrounding text. Mostly this is used by RST where a non-zero value -is required.
header:Any additional header material this weaver requires.
-

Instance-level configuration values:

- --- - - - - -
reference_style:
 Either an instance of TransitiveReference() or SimpleReference()
-

Imports (11) =

-
-import string
-
- -
-

Imports (11). Used by: pyweb.py (153)

-
-

Weaver subclass of Emitter to create documentation (12) =

-
-class Weaver( Emitter ):
-    """Format various types of XRef's and code blocks when weaving.
-    RST format.
-    Requires ``..  include:: <isoamsa.txt>``
-    and      ``..  include:: <isopub.txt>``
-    """
-    extension= ".rst"
-    code_indent= 4
-    header= """\n..  include:: <isoamsa.txt>\n..  include:: <isopub.txt>\n"""
-
-    def __init__( self ):
-        super().__init__()
-        self.reference_style= None # Must be configured.
-
-    →Weaver doOpen, doClose and addIndent overrides (13)
-
-    # Template Expansions.
-
-    →Weaver quoted characters (14)
-    →Weaver document chunk begin-end (15)
-    →Weaver reference summary, used by code chunk and file chunk (16)
-    →Weaver code chunk begin-end (17)
-    →Weaver file chunk begin-end (18)
-    →Weaver reference command output (19)
-    →Weaver cross reference output methods (20), →(21)
-
- -
-

Weaver subclass of Emitter to create documentation (12). Used by: Emitter class hierarchy... (2)

-
-

The doOpen() method opens the file for writing. For weavers, the file extension -is specified part of the target markup language being created.

-

The doClose() method extends the Emitter class close() method by closing the -actual file created by the open() method.

-

The addIndent() reflects the fact that we're not tracking global indents, merely -the local indentation required to weave a code chunk. The "indent" can vary because -we're not always starting a fresh line with weaveReferenceTo().

-

Weaver doOpen, doClose and addIndent overrides (13) =

-
-def doOpen( self, basename ):
-    self.fileName= basename + self.extension
-    self.logger.info( "Weaving {!r}".format(self.fileName) )
-    self.theFile= open( self.fileName, "w" )
-    self.readdIndent( self.code_indent )
-def doClose( self ):
-    self.theFile.close()
-    self.logger.info( "Wrote {:d} lines to {!r}".format(
-        self.linesWritten, self.fileName) )
-def addIndent( self, increment=0 ):
-    """increment not used when weaving"""
-    self.context.append( self.context[-1] )
-    self.log_indent.debug( "addIndent {!s}: {!r}".format(self.lastIndent, self.context) )
-def codeFinish( self ):
-    pass # Not needed when weaving
-
- -
-

Weaver doOpen, doClose and addIndent overrides (13). Used by: Weaver subclass of Emitter... (12)

-
-

This is an overly simplistic list. We use the parsed-literal -directive because we're including links and what-not in the code. -We have to quote certain inline markup -- but only when the -characters are paired in a way that might confuse RST.

-

We really should use patterns like `.*?`, _.*?_, \*.*?\*, and \|.*?\| -to look for paired RST inline markup and quote just these special character occurrences.

-

Weaver quoted characters (14) =

-
-quoted_chars = [
-    # prevent some RST markup from being recognized
-    ('\\',r'\\'), # Must be first.
-    ('`',r'\`'),
-    ('_',r'\_'),
-    ('*',r'\*'),
-    ('|',r'\|'),
-]
-
- -
-

Weaver quoted characters (14). Used by: Weaver subclass of Emitter... (12)

-
-

The remaining methods apply a chunk to a template.

-

The docBegin() and docEnd() -methods are used when weaving a document text chunk. -Typically, nothing is done before emitting these kinds of chunks. -However, putting a .. line line number RST comment is an example -of possible additional processing.

-

Weaver document chunk begin-end (15) =

-
-def docBegin( self, aChunk ):
-    pass
-def docEnd( self, aChunk ):
-    pass
-
- -
-

Weaver document chunk begin-end (15). Used by: Weaver subclass of Emitter... (12)

-
-

Each code chunk includes the places where the chunk is referenced.

-
-

Note

-

This may be one of the rare places where for... else: could be the correct statement.

-

Currently, something more complex is used.

-
-

Weaver reference summary, used by code chunk and file chunk (16) =

-
-ref_template = string.Template( "${refList}" )
-ref_separator = "; "
-ref_item_template = string.Template( "$fullName (`${seq}`_)" )
-def references( self, aChunk ):
-    references= aChunk.references_list( self )
-    if len(references) != 0:
-        refList= [
-            self.ref_item_template.substitute( seq=s, fullName=n )
-            for n,s in references ]
-        return self.ref_template.substitute( refList=self.ref_separator.join( refList ) )
-    else:
-        return ""
-
- -
-

Weaver reference summary, used by code chunk and file chunk (16). Used by: Weaver subclass of Emitter... (12)

-
-

The codeBegin() method emits the necessary material prior to -a chunk of source code, defined with the @d command.

-

The codeEnd() method emits the necessary material subsequent to -a chunk of source code, defined with the @d command. -Links or cross references to chunks that -refer to this chunk can be emitted.

-

Weaver code chunk begin-end (17) =

-
-cb_template = string.Template( "\n..  _`${seq}`:\n..  rubric:: ${fullName} (${seq}) ${concat}\n..  parsed-literal::\n    :class: code\n\n" )
-
-def codeBegin( self, aChunk ):
-    txt = self.cb_template.substitute(
-        seq= aChunk.seq,
-        lineNumber= aChunk.lineNumber,
-        fullName= aChunk.fullName,
-        concat= "=" if aChunk.initial else "+=", # RST Separator
-    )
-    self.write( txt )
-
-ce_template = string.Template( "\n..\n\n    ..  class:: small\n\n        |loz| *${fullName} (${seq})*. Used by: ${references}\n" )
-
-def codeEnd( self, aChunk ):
-    txt = self.ce_template.substitute(
-        seq= aChunk.seq,
-        lineNumber= aChunk.lineNumber,
-        fullName= aChunk.fullName,
-        references= self.references( aChunk ),
-    )
-    self.write(txt)
-
- -
-

Weaver code chunk begin-end (17). Used by: Weaver subclass of Emitter... (12)

-
-

The fileBegin() method emits the necessary material prior to -a chunk of source code, defined with the @o command. -A subclass would override this to provide specific text -for the intended file type.

-

The fileEnd() method emits the necessary material subsequent to -a chunk of source code, defined with the @o command.

-

There shouldn't be a list of references to a file. We assert that this -list is always empty.

-

Weaver file chunk begin-end (18) =

-
-fb_template = string.Template( "\n..  _`${seq}`:\n..  rubric:: ${fullName} (${seq}) ${concat}\n..  parsed-literal::\n    :class: code\n\n" )
-
-def fileBegin( self, aChunk ):
-    txt= self.fb_template.substitute(
-        seq= aChunk.seq,
-        lineNumber= aChunk.lineNumber,
-        fullName= aChunk.fullName,
-        concat= "=" if aChunk.initial else "+=", # RST Separator
-    )
-    self.write( txt )
-
-fe_template= string.Template( "\n..\n\n    ..  class:: small\n\n        |loz| *${fullName} (${seq})*.\n" )
-
-def fileEnd( self, aChunk ):
-    assert len(self.references( aChunk )) == 0
-    txt= self.fe_template.substitute(
-        seq= aChunk.seq,
-        lineNumber= aChunk.lineNumber,
-        fullName= aChunk.fullName,
-        references= [] )
-    self.write( txt )
-
- -
-

Weaver file chunk begin-end (18). Used by: Weaver subclass of Emitter... (12)

-
-

The referenceTo() method emits a reference to -a chunk of source code. There reference is made with a -@<name@> reference within a @d or @o chunk. -The references are defined with the @d or @o commands. -A subclass would override this to provide specific text -for the intended file type.

-

The referenceSep() method emits a separator to be used -in a sequence of references. It's usually a ", ", but that might be changed to -a simple " " because it looks better.

-

Weaver reference command output (19) =

-
-refto_name_template= string.Template(r"|srarr|\ ${fullName} (`${seq}`_)")
-refto_seq_template= string.Template("|srarr|\ (`${seq}`_)")
-refto_seq_separator= ", "
-
-def referenceTo( self, aName, seq ):
-    """Weave a reference to a chunk.
-    Provide name to get a full reference.
-    name=None to get a short reference."""
-    if aName:
-        return self.refto_name_template.substitute( fullName= aName, seq= seq )
-    else:
-        return self.refto_seq_template.substitute( seq= seq )
-
-def referenceSep( self ):
-    """Separator between references."""
-    return self.refto_seq_separator
-
- -
-

Weaver reference command output (19). Used by: Weaver subclass of Emitter... (12)

-
-

The xrefHead() method puts decoration in front of cross-reference -output. A subclass may override this to change the look of the final -woven document.

-

The xrefFoot() method puts decoration after cross-reference -output. A subclass may override this to change the look of the final -woven document.

-

The xrefLine() method is used for both -file and chunk ("macro") cross-references to show a name (either file name -or chunk name) and a list of chunks that reference the file or chunk.

-

The xrefDefLine() method is used for the user identifier cross-reference. -This shows a name and a list of chunks that -reference or define the name. One of the chunks is identified as the -defining chunk, all others are referencing chunks.

-

An xrefEmpty() is used in the rare case of no user identifiers present.

-

The default behavior simply writes the Python data structure used -to represent cross reference information. A subclass may override this -to change the look of the final woven document.

-

Weaver cross reference output methods (20) =

-
-xref_head_template = string.Template( "\n" )
-xref_foot_template = string.Template( "\n" )
-xref_item_template = string.Template( ":${fullName}:\n    ${refList}\n" )
-xref_empty_template = string.Template( "(None)\n" )
-
-def xrefHead( self ):
-    txt = self.xref_head_template.substitute()
-    self.write( txt )
-
-def xrefFoot( self ):
-    txt = self.xref_foot_template.substitute()
-    self.write( txt )
-
-def xrefLine( self, name, refList ):
-    refList= [ self.referenceTo( None, r ) for r in refList ]
-    txt= self.xref_item_template.substitute( fullName= name, refList = " ".join(refList) ) # RST Separator
-    self.write( txt )
-
-def xrefEmpty( self ):
-    self.write( self.xref_empty_template.substitute() )
-
- -
-

Weaver cross reference output methods (20). Used by: Weaver subclass of Emitter... (12)

-
-

Cross-reference definition line

-

Weaver cross reference output methods (21) +=

-
-name_def_template = string.Template( '[`${seq}`_]' )
-name_ref_template = string.Template( '`${seq}`_' )
-
-def xrefDefLine( self, name, defn, refList ):
-    templates = { defn: self.name_def_template }
-    refTxt= [ templates.get(r,self.name_ref_template).substitute( seq= r )
-        for r in sorted( refList + [defn] )
-        ]
-    # Generic space separator
-    txt= self.xref_item_template.substitute( fullName= name, refList = " ".join(refTxt) )
-    self.write( txt )
-
- -
-

Weaver cross reference output methods (21). Used by: Weaver subclass of Emitter... (12)

-
-
-
-

RST subclass of Weaver

-

A degenerate case. This slightly simplifies the configuration and makes the output -look a little nicer.

-

RST subclass of Weaver (22) =

-
-class RST(Weaver):
-    pass
-
- -
-

RST subclass of Weaver (22). Used by: Emitter class hierarchy... (2)

-
-
-
-

LaTeX subclass of Weaver

-

Experimental, at best.

-

An instance of LaTeX can be used by the Web object to -weave an output document. The instance is created outside the Web, and -given to the weave() method of the Web.

-
-w= Web()
-WebReader().load(w,"somefile.w")
-weave_latex= LaTeX()
-w.weave( weave_latex )
-
-

Note that the template language and LaTeX both use $. -This means that all $ that are intended to be output to LaTeX -must appear as $$ in the template.

-

The LaTeX subclass defines a Weaver that is customized to -produce LaTeX output of code sections and cross reference information. -Its markup is pretty rudimentary, but it's also distinctive enough to -function pretty well in most L!sub:AT!sub:EX documents.

-

LaTeX subclass of Weaver (23) =

-
-class LaTeX( Weaver ):
-    """LaTeX formatting for XRef's and code blocks when weaving.
-    Requires \\usepackage{fancyvrb}
-    """
-    extension= ".tex"
-    code_indent= 0
-    header= """\n\\usepackage{fancyvrb}\n"""
-
-    →LaTeX code chunk begin (24)
-    →LaTeX code chunk end (25)
-    →LaTeX file output begin (26)
-    →LaTeX file output end (27)
-    →LaTeX references summary at the end of a chunk (28)
-    →LaTeX write a line of code (29)
-    →LaTeX reference to a chunk (30)
-
- -
-

LaTeX subclass of Weaver (23). Used by: Emitter class hierarchy... (2)

-
-

The LaTeX open() method opens the woven file by replacing the -source file's suffix with ".tex" and creating the resulting file.

-

The LaTeX codeBegin() template writes the header prior to a -chunk of source code. It aligns the block to the left, prints an -italicised header, and opens a preformatted block.

-

LaTeX code chunk begin (24) =

-
-cb_template = string.Template( """\\label{pyweb${seq}}
-\\begin{flushleft}
-\\textit{Code example ${fullName} (${seq})}
-\\begin{Verbatim}[commandchars=\\\\\\{\\},codes={\\catcode`$$=3\\catcode`^=7},frame=single]\n""") # Prevent indent
-
- -
-

LaTeX code chunk begin (24). Used by: LaTeX subclass... (23)

-
-

The LaTeX codeEnd() template writes the trailer subsequent to -a chunk of source code. This first closes the preformatted block and -then calls the references() method to write a reference -to the chunk that invokes this chunk; finally, it restores paragraph -indentation.

-

LaTeX code chunk end (25) =

-
-ce_template= string.Template("""
-\\end{Verbatim}
-${references}
-\\end{flushleft}\n""") # Prevent indentation
-
- -
-

LaTeX code chunk end (25). Used by: LaTeX subclass... (23)

-
-

The LaTeX fileBegin() template writes the header prior to a -the creation of a tangled file. Its formatting is identical to the -start of a code chunk.

-

LaTeX file output begin (26) =

-
-fb_template= cb_template
-
- -
-

LaTeX file output begin (26). Used by: LaTeX subclass... (23)

-
-

The LaTeX fileEnd() template writes the trailer subsequent to -a tangled file. This closes the preformatted block, calls the LaTeX -references() method to write a reference to the chunk that -invokes this chunk, and restores normal indentation.

-

LaTeX file output end (27) =

-
-fe_template= ce_template
-
- -
-

LaTeX file output end (27). Used by: LaTeX subclass... (23)

-
-

The references() template writes a list of references after a -chunk of code. Each reference includes the example number, the title, -and a reference to the LaTeX section and page numbers on which the -referring block appears.

-

LaTeX references summary at the end of a chunk (28) =

-
-ref_item_template = string.Template( """
-\\item Code example ${fullName} (${seq}) (Sect. \\ref{pyweb${seq}}, p. \\pageref{pyweb${seq}})\n""")
-ref_template = string.Template( """
-\\footnotesize
-Used by:
-\\begin{list}{}{}
-${refList}
-\\end{list}
-\\normalsize\n""")
-
- -
-

LaTeX references summary at the end of a chunk (28). Used by: LaTeX subclass... (23)

-
-

The quote() method quotes a single line of code to the -weaver; since these lines are always in preformatted blocks, no -special formatting is needed, except to avoid ending the preformatted -block. Our one compromise is a thin space if the phrase -\\end{Verbatim} is used in a code block.

-

LaTeX write a line of code (29) =

-
-quoted_chars = [
-    ("\\end{Verbatim}", "\\end\,{Verbatim}"), # Allow \end{Verbatim}
-    ("\\{","\\\,{"), # Prevent unexpected commands in Verbatim
-    ("$","\\$"), # Prevent unexpected math in Verbatim
-]
-
- -
-

LaTeX write a line of code (29). Used by: LaTeX subclass... (23)

-
-

The referenceTo() template writes a reference to another chunk of -code. It uses write directly as to follow the current indentation on -the current line of code.

-

LaTeX reference to a chunk (30) =

-
-refto_name_template= string.Template("""$$\\triangleright$$ Code Example ${fullName} (${seq})""")
-refto_seq_template= string.Template("""(${seq})""")
-
- -
-

LaTeX reference to a chunk (30). Used by: LaTeX subclass... (23)

-
-
-
-

HTML subclasses of Weaver

-

This works, but, it's not clear that it should be kept.

-

An instance of HTML can be used by the Web object to -weave an output document. The instance is created outside the Web, and -given to the weave() method of the Web.

-
-w= Web()
-WebReader().load(w,"somefile.w")
-weave_html= HTML()
-w.weave( weave_html )
-
-

Variations in the output formatting are accomplished by having -variant subclasses of HTML. In this implementation, we have two -variations: full path references, and short references. The base class -produces complete reference paths; a subclass produces abbreviated references.

-

The HTML subclass defines a Weaver that is customized to -produce HTML output of code sections and cross reference information.

-

All HTML chunks are identified by anchor names of the form pyweb*n*. Each -n is the unique chunk number, in sequential order.

-

An HTMLShort subclass defines a Weaver that produces HTML output -with abbreviated (no name) cross references at the end of the chunk.

-

HTML subclass of Weaver (31) =

-
-class HTML( Weaver ):
-    """HTML formatting for XRef's and code blocks when weaving."""
-    extension= ".html"
-    code_indent= 0
-    header= ""
-    →HTML code chunk begin (33)
-    →HTML code chunk end (34)
-    →HTML output file begin (35)
-    →HTML output file end (36)
-    →HTML references summary at the end of a chunk (37)
-    →HTML write a line of code (38)
-    →HTML reference to a chunk (39)
-    →HTML simple cross reference markup (40)
-
- -
-

HTML subclass of Weaver (31). Used by: Emitter class hierarchy... (2)

-
-

HTML subclass of Weaver (32) +=

-
-class HTMLShort( HTML ):
-    """HTML formatting for XRef's and code blocks when weaving with short references."""
-    →HTML short references summary at the end of a chunk (42)
-
- -
-

HTML subclass of Weaver (32). Used by: Emitter class hierarchy... (2)

-
-

The codeBegin() template starts a chunk of code, defined with @d, providing a label -and HTML tags necessary to set the code off visually.

-

HTML code chunk begin (33) =

-
-cb_template= string.Template("""
-<a name="pyweb${seq}"></a>
-<!--line number ${lineNumber}-->
-<p><em>${fullName}</em> (${seq})&nbsp;${concat}</p>
-<code><pre>\n""")
-
- -
-

HTML code chunk begin (33). Used by: HTML subclass... (31)

-
-

The codeEnd() template ends a chunk of code, providing a HTML tags necessary -to finish the code block visually. This calls the references method to -write the list of chunks that reference this chunk.

-

HTML code chunk end (34) =

-
-ce_template= string.Template("""
-</pre></code>
-<p>&loz; <em>${fullName}</em> (${seq}).
-${references}
-</p>\n""")
-
- -
-

HTML code chunk end (34). Used by: HTML subclass... (31)

-
-

The fileBegin() template starts a chunk of code, defined with @o, providing a label -and HTML tags necessary to set the code off visually.

-

HTML output file begin (35) =

-
-fb_template= string.Template("""<a name="pyweb${seq}"></a>
-<!--line number ${lineNumber}-->
-<p>``${fullName}`` (${seq})&nbsp;${concat}</p>
-<code><pre>\n""") # Prevent indent
-
- -
-

HTML output file begin (35). Used by: HTML subclass... (31)

-
-

The fileEnd() template ends a chunk of code, providing a HTML tags necessary -to finish the code block visually. This calls the references method to -write the list of chunks that reference this chunk.

-

HTML output file end (36) =

-
-fe_template= string.Template( """</pre></code>
-<p>&loz; ``${fullName}`` (${seq}).
-${references}
-</p>\n""")
-
- -
-

HTML output file end (36). Used by: HTML subclass... (31)

-
-

The references() template writes the list of chunks that refer to this chunk. -Note that this list could be rather long because of the possibility of -transitive references.

-

HTML references summary at the end of a chunk (37) =

-
-ref_item_template = string.Template(
-'<a href="#pyweb${seq}"><em>${fullName}</em>&nbsp;(${seq})</a>'
-)
-ref_template = string.Template( '  Used by ${refList}.'  )
-
- -
-

HTML references summary at the end of a chunk (37). Used by: HTML subclass... (31)

-
-

The quote() method quotes an individual line of code for HTML purposes. -This encodes the four basic HTML entities (<, >, &, ") to prevent code from being interpreted -as HTML.

-

HTML write a line of code (38) =

-
-quoted_chars = [
-    ("&", "&amp;"), # Must be first
-    ("<", "&lt;"),
-    (">", "&gt;"),
-    ('"', "&quot;"),
-]
-
- -
-

HTML write a line of code (38). Used by: HTML subclass... (31)

-
-

The referenceTo() template writes a reference to another chunk. It uses the -direct write() method so that the reference is indented properly with the -surrounding source code.

-

HTML reference to a chunk (39) =

-
-refto_name_template = string.Template(
-'<a href="#pyweb${seq}">&rarr;<em>${fullName}</em> (${seq})</a>'
-)
-refto_seq_template = string.Template(
-'<a href="#pyweb${seq}">(${seq})</a>'
-)
-
- -
-

HTML reference to a chunk (39). Used by: HTML subclass... (31)

-
-

The xrefHead() method writes the heading for any of the cross reference blocks created by -@f, @m, or @u. In this implementation, the cross references are simply unordered lists.

-

The xrefFoot() method writes the footing for any of the cross reference blocks created by -@f, @m, or @u. In this implementation, the cross references are simply unordered lists.

-

The xrefLine() method writes a line for the file or macro cross reference blocks created by -@f or @m. In this implementation, the cross references are simply unordered lists.

-

HTML simple cross reference markup (40) =

-
-xref_head_template = string.Template( "<dl>\n" )
-xref_foot_template = string.Template( "</dl>\n" )
-xref_item_template = string.Template( "<dt>${fullName}</dt><dd>${refList}</dd>\n" )
-→HTML write user id cross reference line (41)
-
- -
-

HTML simple cross reference markup (40). Used by: HTML subclass... (31)

-
-

The xrefDefLine() method writes a line for the user identifier cross reference blocks created by -@u. In this implementation, the cross references are simply unordered lists. The defining instance -is included in the correct order with the other instances, but is bold and marked with a bullet (&bull;).

-

HTML write user id cross reference line (41) =

-
-name_def_template = string.Template( '<a href="#pyweb${seq}"><b>&bull;${seq}</b></a>' )
-name_ref_template = string.Template( '<a href="#pyweb${seq}">${seq}</a>' )
-
- -
-

HTML write user id cross reference line (41). Used by: HTML simple cross reference markup (40)

-
-

The HTMLShort subclass enhances the HTML class to provide short -cross references. -The references() method writes the list of chunks that refer to this chunk. -Note that this list could be rather long because of the possibility of -transitive references.

-

HTML short references summary at the end of a chunk (42) =

-
-ref_item_template = string.Template( '<a href="#pyweb${seq}">(${seq})</a>' )
-
- -
-

HTML short references summary at the end of a chunk (42). Used by: HTML subclass... (32)

-
-
-
-

Tangler subclass of Emitter

-

The Tangler class is concrete, and can tangle source files. An -instance of Tangler is given to the Web class tangle() method.

-
-w= Web()
-WebReader().load(w,"somefile.w")
-t= Tangler()
-w.tangle( t )
-
-

The Tangler subclass extends an Emitter to tangle the various -program source files. The superclass is used to simply emit correctly indented -source code and do very little else that could corrupt or alter the output.

-

Language-specific subclasses could be used to provide additional decoration. -For example, inserting #line directives showing the line number -in the original source file.

-

For Python, where indentation matters, the indent rules are relatively -simple. The whitespace berfore a @< command is preserved as -the prevailing indent for the block tangled as a replacement for the @<name@>.

-

There are three configurable values:

- --- - - - - - - - - -
comment_start:If not None, this is the leading character for a line-number comment
comment_end:This is the trailing character for a line-number comment
include_line_numbers:
 Show the source line numbers in the output via additional comments.
-

Tangler subclass of Emitter to create source files with no markup (43) =

-
-class Tangler( Emitter ):
-    """Tangle output files."""
-    def __init__( self ):
-        super().__init__()
-        self.comment_start= None
-        self.comment_end= ""
-        self.include_line_numbers= False
-    →Tangler doOpen, and doClose overrides (44)
-    →Tangler code chunk begin (45)
-    →Tangler code chunk end (46)
-
- -
-

Tangler subclass of Emitter to create source files with no markup (43). Used by: Emitter class hierarchy... (2)

-
-

The default for all tanglers is to create the named file. -In order to handle paths, we will examine the file name for any "/" -characters and perform the required os.makedirs functions to -allow creation of files with a path. We don't use Windows "\" -characters, but rely on Python to handle this automatically.

-

This doClose() method overrides the Emitter class doClose() method by closing the -actual file created by open.

-

Tangler doOpen, and doClose overrides (44) =

-
-def checkPath( self ):
-    if "/" in self.fileName:
-        dirname, _, _ = self.fileName.rpartition("/")
-        try:
-            os.makedirs( dirname )
-            self.logger.info( "Creating {!r}".format(dirname) )
-        except OSError as e:
-            # Already exists.  Could check for errno.EEXIST.
-            self.logger.debug( "Exception {!r} creating {!r}".format(e, dirname) )
-def doOpen( self, aFile ):
-    self.fileName= aFile
-    self.checkPath()
-    self.theFile= open( aFile, "w" )
-    self.logger.info( "Tangling {!r}".format(aFile) )
-def doClose( self ):
-    self.theFile.close()
-    self.logger.info( "Wrote {:d} lines to {!r}".format(
-        self.linesWritten, self.fileName) )
-
- -
-

Tangler doOpen, and doClose overrides (44). Used by: Tangler subclass of Emitter... (43)

-
-

The codeBegin() method starts emitting a new chunk of code. -It does this by setting the Tangler's indent to the -prevailing indent at the start of the @< reference command.

-

Tangler code chunk begin (45) =

-
-def codeBegin( self, aChunk ):
-    self.log_indent.debug( "<tangle {!s}:".format(aChunk.fullName) )
-    if self.include_line_numbers and self.comment_start is not None:
-        self.write( "\n{start!s} Web: {filename!s}:{line:d} {fullname!s}({seq:d}) {end!s}\n".format(
-            start=  self.comment_start,
-            webfilename= aChunk.web().webFileName,
-            filename= aChunk.fileName,
-            fullname= aChunk.fullName,
-            name= aChunk.name,
-            seq= aChunk.seq,
-            line= aChunk.lineNumber,
-            end= self.comment_end) )
-
- -
-

Tangler code chunk begin (45). Used by: Tangler subclass of Emitter... (43)

-
-

The codeEnd() method ends emitting a new chunk of code. -It does this by resetting the Tangler's indent to the previous -setting.

-

Tangler code chunk end (46) =

-
-def codeEnd( self, aChunk ):
-    self.log_indent.debug( ">{!s}".format(aChunk.fullName) )
-
- -
-

Tangler code chunk end (46). Used by: Tangler subclass of Emitter... (43)

-
-
-
-

TanglerMake subclass of Tangler

-

The TanglerMake class is can tangle source files. An -instance of TanglerMake is given to the Web class tangle() method.

-
-w= Web()
-WebReader().load(w,"somefile.w")
-t= TanglerMake()
-w.tangle( t )
-
-

The TanglerMake subclass extends Tangler to make the source files -more make-friendly. This subclass of Tangler -does not touch an output file -where there is no change. This is helpful when pyWeb's output is -sent to make. Using TanglerMake assures that only files with real changes -are rewritten, minimizing recompilation of an application for changes to -the associated documentation.

-

This subclass of Tangler changes how files -are opened and closed.

-

Imports (47) +=

-
-import tempfile
-import filecmp
-
- -
-

Imports (47). Used by: pyweb.py (153)

-
-

TanglerMake subclass which is make-sensitive (48) =

-
-class TanglerMake( Tangler ):
-    """Tangle output files, leaving files untouched if there are no changes."""
-    def __init__( self, *args ):
-        super().__init__( *args )
-        self.tempname= None
-    →TanglerMake doOpen override, using a temporary file (49)
-    →TanglerMake doClose override, comparing temporary to original (50)
-
- -
-

TanglerMake subclass which is make-sensitive (48). Used by: Emitter class hierarchy... (2)

-
-

A TanglerMake creates a temporary file to collect the -tangled output. When this file is completed, we can compare -it with the original file in this directory, avoiding -a "touch" if the new file is the same as the original.

-

TanglerMake doOpen override, using a temporary file (49) =

-
-def doOpen( self, aFile ):
-    fd, self.tempname= tempfile.mkstemp( dir=os.curdir )
-    self.theFile= os.fdopen( fd, "w" )
-    self.logger.info( "Tangling {!r}".format(aFile) )
-
- -
-

TanglerMake doOpen override, using a temporary file (49). Used by: TanglerMake subclass... (48)

-
-

If there is a previous file: compare the temporary file and the previous file.

-

If there was no previous file or the files are different: rename temporary to replace previous; -else there was a previous file and the files were the same: unlink temporary and discard it.

-

This preserves the original (with the original date -and time) if nothing has changed.

-

TanglerMake doClose override, comparing temporary to original (50) =

-
-def doClose( self ):
-    self.theFile.close()
-    try:
-        same= filecmp.cmp( self.tempname, self.fileName )
-    except OSError as e:
-        same= False # Doesn't exist.  Could check for errno.ENOENT
-    if same:
-        self.logger.info( "No change to {!r}".format(self.fileName) )
-        os.remove( self.tempname )
-    else:
-        # Windows requires the original file name be removed first.
-        self.checkPath()
-        try:
-            os.remove( self.fileName )
-        except OSError as e:
-            pass # Doesn't exist.  Could check for errno.ENOENT
-        os.rename( self.tempname, self.fileName )
-        self.logger.info( "Wrote {:d} lines to {!r}".format(
-            self.linesWritten, self.fileName) )
-
- -
-

TanglerMake doClose override, comparing temporary to original (50). Used by: TanglerMake subclass... (48)

-
-
-
-
-

Chunks

-

A Chunk is a piece of the input file. It is a collection of Command instances. -A chunk can be woven or tangled to create output.

-

The two most important methods are the weave() and tangle() methods. These -visit the commands of this chunk, producing the required output file.

-

Additional methods (startswith(), searchForRE() and usedBy()) -are used to examine the text of the Command instances within -the chunk.

-

A Chunk instance is created by the WebReader as the input file is parsed. -Each Chunk instance has one or more pieces of the original input text. -This text can be program source, a reference command, or the documentation source.

-

Chunk class hierarchy - used to describe input chunks (51) =

-
-→Chunk class (52)
-→NamedChunk class (63), →(68)
-→OutputChunk class (69)
-→NamedDocumentChunk class (73)
-
- -
-

Chunk class hierarchy - used to describe input chunks (51). Used by: Base Class Definitions (1)

-
-

The Chunk class is both the superclass for this hierarchy and the implementation -for anonymous chunks. An anonymous chunk is always documentation in the -target markup language. No transformation is ever done on anonymous chunks.

-

A NamedChunk is a chunk created with a @d command. -This is a chunk of source programming language, bracketed with @{ and @}.

-

An OutputChunk is a named chunk created with a @o command. -This must be a chunk of source programming language, bracketed with @{ and @}.

-

A NamedDocumentChunk is a named chunk created with a @d command. -This is a chunk of documentation in the target markup language, -bracketed with @[ and @].

-
-

Chunk Superclass

-

An instance of the Chunk class has a life that includes four important events:

-
    -
  • creation,
  • -
  • cross-reference,
  • -
  • weave,
  • -
  • and tangle.
  • -
-

A Chunk is created by a WebReader, and associated with a Web. -There are several web append methods, depending on the exact subclass of Chunk. -The WebReader calls the chunk's webAdd() method select the correct method -for appending and indexing the chunk. -Individual instances of Command are appended to the chunk. -The basic outline for creating a Chunk instance is as follows:

-
-w= Web( )
-c= Chunk()
-c.webAdd( w )
-c.append( ...some Command... )
-c.append( ...some Command... )
-
-

Before weaving or tangling, a cross reference is created for all -user identifiers in all of the Chunk instances. -This is done by: (1) visit each Chunk and call the -getUserIDRefs() method to gather all identifiers; (2) for each identifier, -visit each Chunk and call the searchForRE() method to find uses of -the identifier.

-
-ident= []
-for c in the Web's named chunk list:
-    ident.extend( c.getUserIDRefs() )
-for i in ident:
-    pattern= re.compile('W{!s}W'.format(i) )
-    for c in the Web's named chunk list:
-        c.searchForRE( pattern )
-
-

A Chunk is woven or tangled by the Web. The basic outline for weaving is -as follows. The tangling action is essentially the same.

-
-for c in the Web's chunk list:
-    c.weave( aWeaver )
-
-

The Chunk class contains the overall definitions for all of the -various specialized subclasses. In particular, it contains the append(), -and appendText() methods used by all of the various Chunk subclasses.

-

When a @@ construct is located in the input stream, the stream contains -three text tokens: material before the @@, the @@, -and the material after the @@. -These three tokens are reassembled into a single block of text. This reassembly -is accomplished by changing the chunk's state so that the next TextCommand is -appended onto the previous TextCommand.

-

The appendText() method either:

-
    -
  • appends to a previous TextCommand instance,
  • -
  • or finds that there are no commands at all, and creates an initial -TextCommand instance,
  • -
  • or finds that the last Command isn't a subclass of TextCommand -and creates a TextCommand instance.
  • -
-

Each subclass of Chunk has a particular type of text that it will process. Anonymous chunks -only handle document text. The NamedChunk subclass that handles program source -will override this method to create a different command type. The makeContent() method -creates the appropriate Command instance for this Chunk subclass.

-

The weave() method of an anonymous Chunk uses the weaver's -docBegin() and docEnd() -methods to insert text that is source markup. Other subclasses will override this to -use different Weaver methods for different kinds of text.

-

A Chunk has a Strategy object which is a subclass of Reference. This is -either an instance of SimpleReference or TransitiveReference. -A SimpleRerence does no additional processing, and locates the proximate reference to -this chunk. The TransitiveReference walks "up" the web toward top-level file -definitions that reference this Chunk.

-

The Chunk constructor initializes the following instance variables:

- --- - - - - - - - - - - - - - -
commands:is a sequence of the various Command instances the comprise this -chunk.
user_id_list:is used the list of user identifiers associated with -this chunk. This attribute is always None for this class. -The NamedChunk subclass, however, can have user identifiers.
initial:is True if this is the first -definition (display with '=') or a subsequent definition (display with '+=').
web:A weakref to the web which contains this Chunk. We want to inherit information -from the Web overall.
fileName:The file which contained this chunk's initial @o or @d.
name:has the name of the chunk. This is '' for anonymous chunks.
-
-
!seq:
-
has the sequence number associated with this chunk. This is None -for anonymous chunks.
-
- --- - - - - - -
referencedBy:is the list of Chunks which reference this chunk.
references:is the list of Chunks this chunk references.
-

Chunk class (52) =

-
-class Chunk:
-    """Anonymous piece of input file: will be output through the weaver only."""
-    # construction and insertion into the web
-    def __init__( self ):
-        self.commands= [ ] # The list of children of this chunk
-        self.user_id_list= None
-        self.initial= None
-        self.name= ''
-        self.fullName= None
-        self.seq= None
-        self.fileName= ''
-        self.referencedBy= [] # Chunks which reference this chunk.  Ideally just one.
-        self.references= [] # Names that this chunk references
-
-    def __str__( self ):
-        return "\n".join( map( str, self.commands ) )
-    def __repr__( self ):
-        return "{!s}('{!s}')".format( self.__class__.__name__, self.name )
-    →Chunk append a command (53)
-    →Chunk append text (54)
-    →Chunk add to the web (55)
-
-    →Chunk generate references from this Chunk (58)
-    →Chunk superclass make Content definition (56)
-    →Chunk examination: starts with, matches pattern (57)
-    →Chunk references to this Chunk (59)
-
-    →Chunk weave this Chunk into the documentation (60)
-    →Chunk tangle this Chunk into a code file (61)
-    →Chunk indent adjustments (62)
-
- -
-

Chunk class (52). Used by: Chunk class hierarchy... (51)

-
-

The append() method simply appends a Command instance to this chunk.

-

Chunk append a command (53) =

-
-def append( self, command ):
-    """Add another Command to this chunk."""
-    self.commands.append( command )
-    command.chunk= self
-
- -
-

Chunk append a command (53). Used by: Chunk class (52)

-
-

The appendText() method appends a TextCommand to this chunk, -or it concatenates it to the most recent TextCommand.

-

When an @@ construct is located, the appendText() method is -used to accumulate this character. This means that it will be appended to -any previous TextCommand, or new TextCommand will be built.

-

The reason for appending is that a TextCommand has an implicit indentation. The "@" cannot -be a separate TextCommand because it will wind up indented.

-

Chunk append text (54) =

-
-def appendText( self, text, lineNumber=0 ):
-    """Append a single character to the most recent TextCommand."""
-    try:
-        # Works for TextCommand, otherwise breaks
-        self.commands[-1].text += text
-    except IndexError as e:
-        # First command?  Then the list will have been empty.
-        self.commands.append( self.makeContent(text,lineNumber) )
-    except AttributeError as e:
-        # Not a TextCommand?  Then there won't be a text attribute.
-        self.commands.append( self.makeContent(text,lineNumber) )
-
- -
-

Chunk append text (54). Used by: Chunk class (52)

-
-

The webAdd() method adds this chunk to the given document web. -Each subclass of the Chunk class must override this to be sure that the various -Chunk subclasses are indexed properly. The -Chunk class uses the add() method -of the Web class to append an anonymous, unindexed chunk.

-

Chunk add to the web (55) =

-
-def webAdd( self, web ):
-    """Add self to a Web as anonymous chunk."""
-    web.add( self )
-
- -
-

Chunk add to the web (55). Used by: Chunk class (52)

-
-

This superclass creates a specific Command for a given piece of content. -A subclass can override this to change the underlying assumptions of that Chunk. -The generic chunk doesn't contain code, it contains text and can only be woven, -never tangled. A Named Chunk using @{ and @} creates code. -A Named Chunk using @[ and @] creates text.

-

Chunk superclass make Content definition (56) =

-
-def makeContent( self, text, lineNumber=0 ):
-    return TextCommand( text, lineNumber )
-
- -
-

Chunk superclass make Content definition (56). Used by: Chunk class (52)

-
-

The startsWith() method examines a the first Command instance this -Chunk instance to see if it starts -with the given prefix string.

-

The lineNumber() method returns the line number of the first -Command in this chunk. This provides some context for where the chunk -occurs in the original input file.

-

A NamedChunk instance may define one or more identifiers. This parent class -provides a dummy version of the getUserIDRefs method. The NamedChunk -subclass overrides this to provide actual results. By providing this -at the superclass-level, the Web can easily gather identifiers without -knowing the actual subclass of Chunk.

-

The searchForRE() method examines each Command instance to see if it matches -with the given regular expression. If so, this can be reported to the Web instance -and accumulated as part of a cross reference for this Chunk.

-

Chunk examination: starts with, matches pattern (57) =

-
-def startswith( self, prefix ):
-    """Examine the first command's starting text."""
-    return len(self.commands) >= 1 and self.commands[0].startswith( prefix )
-
-def searchForRE( self, rePat ):
-    """Visit each command, applying the pattern."""
-    for c in self.commands:
-        if c.searchForRE( rePat ):
-            return self
-    return None
-
-@property
-def lineNumber( self ):
-    """Return the first command's line number or None."""
-    return self.commands[0].lineNumber if len(self.commands) >= 1 else None
-
-def getUserIDRefs( self ):
-    return []
-
- -
-

Chunk examination: starts with, matches pattern (57). Used by: Chunk class (52)

-
-

The chunk search in the searchForRE() method parallels weaving and tangling a Chunk. -The operation is delegated to each Command instance within the Chunk instance.

-

The genReferences() method visits each Command instance inside this chunk; -a Command will yield the references.

-

Note that an exception may be raised by this operation if a referenced -Chunk does not actually exist. If a reference Command does raise an error, -we append this Chunk information and reraise the error with the additional -context information.

-

Chunk generate references from this Chunk (58) =

-
-def genReferences( self, aWeb ):
-    """Generate references from this Chunk."""
-    try:
-        for t in self.commands:
-            ref= t.ref( aWeb )
-            if ref is not None:
-                yield ref
-    except Error as e:
-        raise
-
- -
-

Chunk generate references from this Chunk (58). Used by: Chunk class (52)

-
-

The list of references to a Chunk uses a Strategy plug-in -to either generate a simple parent or a transitive closure of all parents.

-

Note that we need to get the Weaver.reference_style which is a -configuration item. This is a Strategy showing how to compute the list of references. -The Weaver pushed it into the Web so that it is available for each Chunk.

-

Chunk references to this Chunk (59) =

-
-def references_list( self, theWeaver ):
-    """Extract name, sequence from Chunks into a list."""
-    return [ (c.name, c.seq)
-        for c in theWeaver.reference_style.chunkReferencedBy( self ) ]
-
- -
-

Chunk references to this Chunk (59). Used by: Chunk class (52)

-
-

The weave() method weaves this chunk into the final document as follows:

-
    -
  1. Call the Weaver class docBegin() method. This method does nothing for document content.
  2. -
  3. Visit each Command instance: call the Command instance weave() method to -emit the content of the Command instance
  4. -
  5. Call the Weaver class docEnd() method. This method does nothing for document content.
  6. -
-

Note that an exception may be raised by this action if a referenced -Chunk does not actually exist. If a reference Command does raise an error, -we append this Chunk information and reraise the error with the additional -context information.

-

Chunk weave this Chunk into the documentation (60) =

-
-def weave( self, aWeb, aWeaver ):
-    """Create the nicely formatted document from an anonymous chunk."""
-    aWeaver.docBegin( self )
-    for cmd in self.commands:
-        cmd.weave( aWeb, aWeaver )
-    aWeaver.docEnd( self )
-def weaveReferenceTo( self, aWeb, aWeaver ):
-    """Create a reference to this chunk -- except for anonymous chunks."""
-    raise Exception( "Cannot reference an anonymous chunk.""")
-def weaveShortReferenceTo( self, aWeb, aWeaver ):
-    """Create a short reference to this chunk -- except for anonymous chunks."""
-    raise Exception( "Cannot reference an anonymous chunk.""")
-
- -
-

Chunk weave this Chunk into the documentation (60). Used by: Chunk class (52)

-
-

Anonymous chunks cannot be tangled. Any attempt indicates a serious -problem with this program or the input file.

-

Chunk tangle this Chunk into a code file (61) =

-
-def tangle( self, aWeb, aTangler ):
-    """Create source code -- except anonymous chunks should not be tangled"""
-    raise Error( 'Cannot tangle an anonymous chunk', self )
-
- -
-

Chunk tangle this Chunk into a code file (61). Used by: Chunk class (52)

-
-

Generally, a Chunk with a reference will adjust the indentation for -that referenced material. However, this is not universally true, -a subclass may not indent when tangling and may -- instead -- put stuff flush at the -left margin by forcing the local indent to zero.

-

Chunk indent adjustments (62) =

-
-def reference_indent( self, aWeb, aTangler, amount ):
-    aTangler.addIndent( amount )  # Or possibly set indent to local zero.
-
-def reference_dedent( self, aWeb, aTangler ):
-    aTangler.clrIndent()
-
- -
-

Chunk indent adjustments (62). Used by: Chunk class (52)

-
-
-
-

NamedChunk class

-

A NamedChunk is created and used almost identically to an anonymous Chunk. -The most significant difference is that a name is provided when the NamedChunk is created. -This name is used by the Web to organize the chunks.

-

A NamedChunk is created with a @d or @o command. -A NamedChunk contains programming language source -when the brackets are @{ and @}. A -separate subclass of NamedDocumentChunk is used when -the brackets are @[ and @].

-

A NamedChunk can be both tangled into the output program files, and -woven into the output document file.

-

The weave() method of a NamedChunk uses the Weaver's -codeBegin() and codeEnd() -methods to insert text that is program source and requires additional -markup to make it stand out from documentation. Other subclasses can override this to -use different Weaver methods for different kinds of text.

-

By inheritance from the superclass, this class indents. A separate subclass provides a no-indent -implementation of a NamedChunk.

-

This class introduces some additional attributes.

- --- - - - - - - - - - -
fullName:is the full name of the chunk. It's possible for a -chunk to be an abbreviated forward reference; full names cannot be resolved -until all chunks have been seen.
user_id_list:is the list of user identifiers associated with this chunk.
refCount:is the count of references to this chunk. If this is -zero, the chunk is unused; if this is more than one, this chunk is -multiply used. Either of these conditions is a possible error in the input. -This is set by the usedBy() method.
name:has the name of the chunk. Names can be abbreviated.
-
-
!seq:
-
has the sequence number associated with this chunk. This -is set by the Web by the webAdd() method.
-
-

NamedChunk class (63) =

-
-class NamedChunk( Chunk ):
-    """Named piece of input file: will be output as both tangler and weaver."""
-    def __init__( self, name ):
-        super().__init__()
-        self.name= name
-        self.user_id_list= []
-        self.refCount= 0
-    def __str__( self ):
-        return "{!r}: {!s}".format( self.name, Chunk.__str__(self) )
-    def makeContent( self, text, lineNumber=0 ):
-        return CodeCommand( text, lineNumber )
-    →NamedChunk user identifiers set and get (64)
-    →NamedChunk add to the web (65)
-    →NamedChunk weave into the documentation (66)
-    →NamedChunk tangle into the source file (67)
-
- -
-

NamedChunk class (63). Used by: Chunk class hierarchy... (51)

-
-

The setUserIDRefs() method accepts a list of user identifiers that are -associated with this chunk. These are provided after the @| separator -in a @d named chunk. These are used by the @u cross reference generator.

-

NamedChunk user identifiers set and get (64) =

-
-def setUserIDRefs( self, text ):
-    """Save user ID's associated with this chunk."""
-    self.user_id_list= text.split()
-def getUserIDRefs( self ):
-    return self.user_id_list
-
- -
-

NamedChunk user identifiers set and get (64). Used by: NamedChunk class (63)

-
-

The webAdd() method adds this chunk to the given document Web instance. -Each class of Chunk must override this to be sure that the various -Chunk classes are indexed properly. This class uses the Web.addNamed() method -of the Web class to append a named chunk.

-

NamedChunk add to the web (65) =

-
-def webAdd( self, web ):
-    """Add self to a Web as named chunk, update xrefs."""
-    web.addNamed( self )
-
- -
-

NamedChunk add to the web (65). Used by: NamedChunk class (63)

-
-

The weave() method weaves this chunk into the final document as follows:

-
    -
  1. call the Weaver class codeBegin() method. This method emits the necessary markup -for code appearing in the woven output.
  2. -
  3. visit each Command, calling the command's weave() method to emit the command's content.
  4. -
  5. call the Weaver class CodeEnd() method. This method emits the necessary markup -for code appearing in the woven output.
  6. -
-

For an RST weaver this becomes a parsed-literal, which requires a extra indent. -For an HTML weaver this becomes a <pre> in a different-colored box.

-

References generate links in a woven document. In a tangled document, they create the actual -code. The weaveRefenceTo() method weaves a reference to a chunk using both name and sequence number. -The weaveShortReferenceTo() method weaves a reference to a chunk using only the sequence number. -These references are created by ReferenceCommand instances within a chunk being woven.

-

The woven references simply follow whatever preceded them on the line; the indent -(if any) doesn't change from the default.

-

NamedChunk weave into the documentation (66) =

-
-def weave( self, aWeb, aWeaver ):
-    """Create the nicely formatted document from a chunk of code."""
-    self.fullName= aWeb.fullNameFor( self.name )
-    aWeaver.addIndent()
-    aWeaver.codeBegin( self )
-    for cmd in self.commands:
-        cmd.weave( aWeb, aWeaver )
-    aWeaver.clrIndent( )
-    aWeaver.codeEnd( self )
-def weaveReferenceTo( self, aWeb, aWeaver ):
-    """Create a reference to this chunk."""
-    self.fullName= aWeb.fullNameFor( self.name )
-    txt= aWeaver.referenceTo( self.fullName, self.seq )
-    aWeaver.codeBlock( txt )
-def weaveShortReferenceTo( self, aWeb, aWeaver ):
-    """Create a shortened reference to this chunk."""
-    txt= aWeaver.referenceTo( None, self.seq )
-    aWeaver.codeBlock( txt )
-
- -
-

NamedChunk weave into the documentation (66). Used by: NamedChunk class (63)

-
-

The tangle() method tangles this chunk into the final document as follows:

-
    -
  1. call the Tangler class codeBegin() method to set indents properly.
  2. -
  3. visit each Command, calling the Command's tangle() method to emit the Command's content.
  4. -
  5. call the Tangler class codeEnd() method to restore indents.
  6. -
-

If a ReferenceCommand does raise an error during tangling, -we append this Chunk information and reraise the error with the additional -context information.

-

NamedChunk tangle into the source file (67) =

-
-def tangle( self, aWeb, aTangler ):
-    """Create source code.
-    Use aWeb to resolve @<namedChunk@>.
-    Format as correctly indented source text
-    """
-    self.previous_command= TextCommand( "", self.commands[0].lineNumber )
-    aTangler.codeBegin( self )
-    for t in self.commands:
-        try:
-            t.tangle( aWeb, aTangler )
-        except Error as e:
-            raise
-        self.previous_command= t
-    aTangler.codeEnd( self )
-
- -
-

NamedChunk tangle into the source file (67). Used by: NamedChunk class (63)

-
-

There's a second variation on NamedChunk, one that doesn't indent based on -context. It simply sets an indent at the left margin.

-

NamedChunk class (68) +=

-
-class NamedChunk_Noindent( NamedChunk ):
-    """Named piece of input file: will be output as both tangler and weaver."""
-    def reference_indent( self, aWeb, aTangler, amount ):
-        aTangler.setIndent( 0 )
-
-    def reference_dedent( self, aWeb, aTangler ):
-        aTangler.clrIndent()
-
- -
-

NamedChunk class (68). Used by: Chunk class hierarchy... (51)

-
-
-
-

OutputChunk class

-

A OutputChunk is created and used identically to a NamedChunk. -The difference between this class and the parent class is the decoration of -the markup when weaving.

-

The OutputChunk class is a subclass of NamedChunk that handles -file output chunks defined with @o.

-

The weave() method of a OutputChunk uses the Weaver's -fileBegin() and fileEnd() -methods to insert text that is program source and requires additional -markup to make it stand out from documentation. Other subclasses could override this to -use different Weaver methods for different kinds of text.

-

All other methods, including the tangle method are identical to NamedChunk.

-

OutputChunk class (69) =

-
-class OutputChunk( NamedChunk ):
-    """Named piece of input file, defines an output tangle."""
-    def __init__( self, name, comment_start=None, comment_end="" ):
-        super().__init__( name )
-        self.comment_start= comment_start
-        self.comment_end= comment_end
-    →OutputChunk add to the web (70)
-    →OutputChunk weave (71)
-    →OutputChunk tangle (72)
-
- -
-

OutputChunk class (69). Used by: Chunk class hierarchy... (51)

-
-

The webAdd() method adds this chunk to the given document Web. -Each class of Chunk must override this to be sure that the various -Chunk classes are indexed properly. This class uses the addOutput() method -of the Web class to append a file output chunk.

-

OutputChunk add to the web (70) =

-
-def webAdd( self, web ):
-    """Add self to a Web as output chunk, update xrefs."""
-    web.addOutput( self )
-
- -
-

OutputChunk add to the web (70). Used by: OutputChunk class (69)

-
-

The weave() method weaves this chunk into the final document as follows:

-
    -
  1. call the Weaver class codeBegin() method to emit proper markup for an output file chunk.
  2. -
  3. visit each Command, call the Command's weave() method to emit the Command's content.
  4. -
  5. call the Weaver class codeEnd() method to emit proper markup for an output file chunk.
  6. -
-

These chunks of documentation are never tangled. Any attempt is an -error.

-

If a ReferenceCommand does raise an error during weaving, -we append this Chunk information and reraise the error with the additional -context information.

-

OutputChunk weave (71) =

-
-def weave( self, aWeb, aWeaver ):
-    """Create the nicely formatted document from a chunk of code."""
-    self.fullName= aWeb.fullNameFor( self.name )
-    aWeaver.fileBegin( self )
-    for cmd in self.commands:
-        cmd.weave( aWeb, aWeaver )
-    aWeaver.fileEnd( self )
-
- -
-

OutputChunk weave (71). Used by: OutputChunk class (69)

-
-

When we tangle, we provide the output Chunk's comment information to the Tangler -to be sure that -- if line numbers were requested -- they can be included properly.

-

OutputChunk tangle (72) =

-
-def tangle( self, aWeb, aTangler ):
-    aTangler.comment_start= self.comment_start
-    aTangler.comment_end= self.comment_end
-    super().tangle( aWeb, aTangler )
-
- -
-

OutputChunk tangle (72). Used by: OutputChunk class (69)

-
-
-
-

NamedDocumentChunk class

-

A NamedDocumentChunk is created and used identically to a NamedChunk. -The difference between this class and the parent class is that this chunk -is only woven when referenced. The original definition is silently skipped.

-

The NamedDocumentChunk class is a subclass of NamedChunk that handles -named chunks defined with @d and the @[...@] delimiters. -These are woven slightly -differently, since they are document source, not programming language source.

-

We're not as interested in the cross reference of named document chunks. -They can be used multiple times or never. They are expected to be referenced -by anonymous chunks. While this chunk subclass participates in this data -gathering, it is ignored for reporting purposes.

-

All other methods, including the tangle method are identical to NamedChunk.

-

NamedDocumentChunk class (73) =

-
-class NamedDocumentChunk( NamedChunk ):
-    """Named piece of input file with document source, defines an output tangle."""
-    def makeContent( self, text, lineNumber=0 ):
-        return TextCommand( text, lineNumber )
-    →NamedDocumentChunk weave (74)
-    →NamedDocumentChunk tangle (75)
-
- -
-

NamedDocumentChunk class (73). Used by: Chunk class hierarchy... (51)

-
-

The weave() method quietly ignores this chunk in the document. -A named document chunk is only included when it is referenced -during weaving of another chunk (usually an anonymous document -chunk).

-

The weaveReferenceTo() method inserts the content of this -chunk into the output document. This is done in response to a -ReferenceCommand in another chunk. -The weaveShortReferenceTo() method calls the weaveReferenceTo() -to insert the entire chunk.

-

NamedDocumentChunk weave (74) =

-
-def weave( self, aWeb, aWeaver ):
-    """Ignore this when producing the document."""
-    pass
-def weaveReferenceTo( self, aWeb, aWeaver ):
-    """On a reference to this chunk, expand the body in place."""
-    for cmd in self.commands:
-        cmd.weave( aWeb, aWeaver )
-def weaveShortReferenceTo( self, aWeb, aWeaver ):
-    """On a reference to this chunk, expand the body in place."""
-    self.weaveReferenceTo( aWeb, aWeaver )
-
- -
-

NamedDocumentChunk weave (74). Used by: NamedDocumentChunk class (73)

-
-

NamedDocumentChunk tangle (75) =

-
-def tangle( self, aWeb, aTangler ):
-    """Raise an exception on an attempt to tangle."""
-    raise Error( "Cannot tangle a chunk defined with @[.""" )
-
- -
-

NamedDocumentChunk tangle (75). Used by: NamedDocumentChunk class (73)

-
-
-
-
-

Commands

-

The input stream is broken into individual commands, based on the -various @*x* strings in the file. There are several subclasses of Command, -each used to describe a different command or block of text in the input.

-

All instances of the Command class are created by a WebReader instance. -In this case, a WebReader can be thought of as a factory for Command instances. -Each Command instance is appended to the sequence of commands that -belong to a Chunk. A chunk may be as small as a single command, or a long sequence -of commands.

-

Each Command instance responds to methods to examine the content, gather -cross reference information and tangle a file or weave the final document.

-

Command class hierarchy - used to describe individual commands (76) =

-
-→Command superclass (77)
-→TextCommand class to contain a document text block (80)
-→CodeCommand class to contain a program source code block (81)
-→XrefCommand superclass for all cross-reference commands (82)
-→FileXrefCommand class for an output file cross-reference (83)
-→MacroXrefCommand class for a named chunk cross-reference (84)
-→UserIdXrefCommand class for a user identifier cross-reference (85)
-→ReferenceCommand class for chunk references (86)
-
- -
-

Command class hierarchy - used to describe individual commands (76). Used by: Base Class Definitions (1)

-
-
-

Command Superclass

-

A Command is created by the WebReader, and attached to a Chunk. -The Command participates in cross reference creation, weaving and tangling.

-

The Command superclass is abstract, and has default methods factored out -of the various subclasses. When a subclass is created, it will override some -of the methods provided in this superclass.

-
-class MyNewCommand( Command ):
-    ... overrides for various methods ...
-
-

Additionally, a subclass of WebReader must be defined to parse the new command -syntax. The main process() function must also be updated to use this new subclass -of WebReader.

-

The Command superclass provides the parent class definition -for all of the various command types. The most common command -is a block of text. -For tangling it is presented with no changes. For weaving, -quoting is used to make it work nicely with the given markup language.

-

The next most -common command is a reference to a chunk. This is woven as a -mark-up reference: a link. It is tangled as an expansion of the source -code.

-

Additional methods include the following:

-
    -
  • The startswith() method examines any source text to see if -it begins with the given prefix text.
  • -
  • The searchForRE() method examines any source text to see if -it matches the given regular expression, usually a match for a user identifier.
  • -
  • The ref() method is ignored by all but the Reference subclass, -which returns reference made by the command to the parent chunk.
  • -
  • The weave() method weaves this into the output. If a document text -command, it is emitted directly; if a program source code command, -markup is applied. In the case of cross-reference commands, -the actual cross-reference content is emitted. In the case of -reference commands, they are woven as a reference to a named -chunk.
  • -
  • The tangle() method tangles this into the output. If a -this is a document text command, it is ignored; if a this is a -program source code -command, it is indented and emitted. In the case of cross-reference -commands, no output is produced. In the case of reference -commands, the named chunk is indented and emitted.
  • -
-

The attributes of a Command instance includes the line number on which -the command began, in lineNumber.

-

Command superclass (77) =

-
-class Command:
-    """A Command is the lowest level of granularity in the input stream."""
-    def __init__( self, fromLine=0 ):
-        self.lineNumber= fromLine+1 # tokenizer is zero-based
-        self.chunk= None
-        self.logger= logging.getLogger( self.__class__.__qualname__ )
-    def __str__( self ):
-        return "at {!r}".format(self.lineNumber)
-    →Command analysis features: starts-with and Regular Expression search (78)
-    →Command tangle and weave functions (79)
-
- -
-

Command superclass (77). Used by: Command class hierarchy... (76)

-
-

Command analysis features: starts-with and Regular Expression search (78) =

-
-def startswith( self, prefix ):
-    return None
-def searchForRE( self, rePat ):
-    return None
-def indent( self ):
-    return None
-
- -
-

Command analysis features: starts-with and Regular Expression search (78). Used by: Command superclass (77)

-
-

Command tangle and weave functions (79) =

-
-def ref( self, aWeb ):
-    return None
-def weave( self, aWeb, aWeaver ):
-    pass
-def tangle( self, aWeb, aTangler ):
-    pass
-
- -
-

Command tangle and weave functions (79). Used by: Command superclass (77)

-
-
-
-

TextCommand class

-

A TextCommand is created by a Chunk or a NamedDocumentChunk when a -WebReader calls the chunk's appendText() method.

-

This Command participates in cross reference creation, weaving and tangling. When it is -created, the source line number is provided so that this text can be tied back -to the source document.

-

An instance of the TextCommand class is a block of document text. It can originate -in an anonymous block or a named chunk delimited with @[ and @].

-

This subclass provides a concrete implementation for all of the methods. Since -text is the author's original markup language, it is emitted directly to the weaver -or tangler.

-

TextCommand class to contain a document text block (80) =

-
-class TextCommand( Command ):
-    """A piece of document source text."""
-    def __init__( self, text, fromLine=0 ):
-        super().__init__( fromLine )
-        self.text= text
-    def __str__( self ):
-        return "at {!r}: {!r}...".format(self.lineNumber,self.text[:32])
-    def startswith( self, prefix ):
-        return self.text.startswith( prefix )
-    def searchForRE( self, rePat ):
-        return rePat.search( self.text )
-    def indent( self ):
-        if self.text.endswith('\n'):
-            return 0
-        try:
-            last_line = self.text.splitlines()[-1]
-            return len(last_line)
-        except IndexError:
-            return 0
-    def weave( self, aWeb, aWeaver ):
-        aWeaver.write( self.text )
-    def tangle( self, aWeb, aTangler ):
-        aTangler.write( self.text )
-
- -
-

TextCommand class to contain a document text block (80). Used by: Command class hierarchy... (76)

-
-
-
-

CodeCommand class

-

A CodeCommand is created by a NamedChunk when a -WebReader calls the appendText() method. -The Command participates in cross reference creation, weaving and tangling. When it is -created, the source line number is provided so that this text can be tied back -to the source document.

-

An instance of the CodeCommand class is a block of program source code text. -It can originate in a named chunk (@d) with a @{ and @} delimiter. -Or it can be a file output chunk (@o).

-

It uses the codeBlock() methods of a Weaver or Tangler. The weaver will -insert appropriate markup for this code. The tangler will assure that the prevailing -indentation is maintained.

-

CodeCommand class to contain a program source code block (81) =

-
-class CodeCommand( TextCommand ):
-    """A piece of program source code."""
-    def weave( self, aWeb, aWeaver ):
-        aWeaver.codeBlock( aWeaver.quote( self.text ) )
-    def tangle( self, aWeb, aTangler ):
-        aTangler.codeBlock( self.text )
-
- -
-

CodeCommand class to contain a program source code block (81). Used by: Command class hierarchy... (76)

-
-
-
-

XrefCommand superclass

-

An XrefCommand is created by the WebReader when any of the -@f, @m, @u commands are found in the input stream. -The Command is then appended to the current Chunk being built by the WebReader.

-

The XrefCommand superclass defines any common features of the -various cross-reference commands (@f, @m, @u).

-

The formatXref() method creates the body of a cross-reference -by the following algorithm:

-
    -
  1. Use the Weaver class xrefHead() method to emit the cross-reference header.
  2. -
  3. Sort the keys in the cross-reference mapping.
  4. -
  5. Use the Weaver class xrefLine() method to emit each line of the cross-reference mapping.
  6. -
  7. Use the Weaver class xrefFoot() method to emit the cross-reference footer.
  8. -
-

If this command winds up in a tangle action, that use -is illegal. An exception is raised and processing stops.

-

XrefCommand superclass for all cross-reference commands (82) =

-
-class XrefCommand( Command ):
-    """Any of the Xref-goes-here commands in the input."""
-    def __str__( self ):
-        return "at {!r}: cross reference".format(self.lineNumber)
-    def formatXref( self, xref, aWeaver ):
-        aWeaver.xrefHead()
-        for n in sorted(xref):
-            aWeaver.xrefLine( n, xref[n] )
-        aWeaver.xrefFoot()
-    def tangle( self, aWeb, aTangler ):
-        raise Error('Illegal tangling of a cross reference command.')
-
- -
-

XrefCommand superclass for all cross-reference commands (82). Used by: Command class hierarchy... (76)

-
-
-
-

FileXrefCommand class

-

A FileXrefCommand is created by the WebReader when the -@f command is found in the input stream. -The Command is then appended to the current Chunk being built by the WebReader.

-

The FileXrefCommand class weave method gets the -file cross reference from the overall web instance, and uses -the formatXref() method of the XrefCommand superclass for format this result.

-

FileXrefCommand class for an output file cross-reference (83) =

-
-class FileXrefCommand( XrefCommand ):
-    """A FileXref command."""
-    def weave( self, aWeb, aWeaver ):
-        """Weave a File Xref from @o commands."""
-        self.formatXref( aWeb.fileXref(), aWeaver )
-
- -
-

FileXrefCommand class for an output file cross-reference (83). Used by: Command class hierarchy... (76)

-
-
-
-

MacroXrefCommand class

-

A MacroXrefCommand is created by the WebReader when the -@m command is found in the input stream. -The Command is then appended to the current Chunk being built by the WebReader.

-

The MacroXrefCommand class weave method gets the -named chunk (macro) cross reference from the overall web instance, and uses -the formatXref() method of the XrefCommand superclass method for format this result.

-

MacroXrefCommand class for a named chunk cross-reference (84) =

-
-class MacroXrefCommand( XrefCommand ):
-    """A MacroXref command."""
-    def weave( self, aWeb, aWeaver ):
-        """Weave the Macro Xref from @d commands."""
-        self.formatXref( aWeb.chunkXref(), aWeaver )
-
- -
-

MacroXrefCommand class for a named chunk cross-reference (84). Used by: Command class hierarchy... (76)

-
-
-
-

UserIdXrefCommand class

-

A MacroXrefCommand is created by the WebReader when the -@u command is found in the input stream. -The Command is then appended to the current Chunk being built by the WebReader.

-

The UserIdXrefCommand class weave method gets the -user identifier cross reference information from the -overall web instance. It then formats this line using the following -algorithm, which is similar to the algorithm in the XrefCommand superclass.

-
    -
  1. Use the Weaver class xrefHead() method to emit the cross-reference header.
  2. -
  3. Sort the keys in the cross-reference mapping.
  4. -
  5. Use the Weaver class xrefDefLine() method to emit each line of the cross-reference definition mapping.
  6. -
  7. Use the Weaver class xrefFoor() method to emit the cross-reference footer.
  8. -
-

UserIdXrefCommand class for a user identifier cross-reference (85) =

-
-class UserIdXrefCommand( XrefCommand ):
-    """A UserIdXref command."""
-    def weave( self, aWeb, aWeaver ):
-        """Weave a user identifier Xref from @d commands."""
-        ux= aWeb.userNamesXref()
-        if len(ux) != 0:
-            aWeaver.xrefHead()
-            for u in sorted(ux):
-                defn, refList= ux[u]
-                aWeaver.xrefDefLine( u, defn, refList )
-            aWeaver.xrefFoot()
-        else:
-            aWeaver.xrefEmpty()
-
- -
-

UserIdXrefCommand class for a user identifier cross-reference (85). Used by: Command class hierarchy... (76)

-
-
-
-

ReferenceCommand class

-

A ReferenceCommand instance is created by a WebReader when -a @<name@> construct in is found in the input stream. This is attached -to the current Chunk being built by the WebReader.

-

During a weave, this creates a markup reference to -another NamedChunk. During tangle, this actually includes the NamedChunk -at this point in the tangled output file.

-

The constructor creates several attributes of an instance -of a ReferenceCommand.

- --- - - - - - - - -
refTo:the name of the chunk to which this refers, possibly -elided with a trailing '...'.
fullName:the full name of the chunk to which this refers.
chunkList:the list of the chunks to which the name refers.
-

ReferenceCommand class for chunk references (86) =

-
-class ReferenceCommand( Command ):
-    """A reference to a named chunk, via @<name@>."""
-    def __init__( self, refTo, fromLine=0 ):
-        super().__init__( fromLine )
-        self.refTo= refTo
-        self.fullname= None
-        self.sequenceList= None
-        self.chunkList= []
-    def __str__( self ):
-        return "at {!r}: reference to chunk {!r}".format(self.lineNumber,self.refTo)
-    →ReferenceCommand resolve a referenced chunk name (87)
-    →ReferenceCommand refers to a chunk (88)
-    →ReferenceCommand weave a reference to a chunk (89)
-    →ReferenceCommand tangle a referenced chunk (90)
-
- -
-

ReferenceCommand class for chunk references (86). Used by: Command class hierarchy... (76)

-
-

The resolve() method queries the overall Web instance for the full -name and sequence number for this chunk reference. This is used -by the Weaver class referenceTo() method to write the markup reference -to the chunk.

-

ReferenceCommand resolve a referenced chunk name (87) =

-
-def resolve( self, aWeb ):
-    """Expand our chunk name and list of parts"""
-    self.fullName= aWeb.fullNameFor( self.refTo )
-    self.chunkList= aWeb.getchunk( self.refTo )
-
- -
-

ReferenceCommand resolve a referenced chunk name (87). Used by: ReferenceCommand class... (86)

-
-

The ref() method is a request that is delegated by a Chunk; -it resolves the reference this Command makes within the containing Chunk. -When the Chunk iterates through the Commands, it can accumulate a list of -Chinks to which it refers.

-

ReferenceCommand refers to a chunk (88) =

-
-def ref( self, aWeb ):
-    """Find and return the full name for this reference."""
-    self.resolve( aWeb )
-    return self.fullName
-
- -
-

ReferenceCommand refers to a chunk (88). Used by: ReferenceCommand class... (86)

-
-

The weave() method inserts a markup reference to a named -chunk. It uses the Weaver class referenceTo() method to format -this appropriately for the document type being woven.

-

ReferenceCommand weave a reference to a chunk (89) =

-
-def weave( self, aWeb, aWeaver ):
-    """Create the nicely formatted reference to a chunk of code."""
-    self.resolve( aWeb )
-    aWeb.weaveChunk( self.fullName, aWeaver )
-
- -
-

ReferenceCommand weave a reference to a chunk (89). Used by: ReferenceCommand class... (86)

-
-

The tangle() method inserts the resolved chunk in this -place. When a chunk is tangled, it sets the indent, -inserts the chunk and resets the indent.

-

This is where the Tangler indentation is updated by a reference. -Or where indentation is set to a local zero because the included -Chunk is a no-indent Chunk.

-

ReferenceCommand tangle a referenced chunk (90) =

-
-def tangle( self, aWeb, aTangler ):
-    """Create source code."""
-    self.resolve( aWeb )
-
-    self.logger.debug( "Indent {!r} + {!r}".format(aTangler.context, self.chunk.previous_command.indent()) )
-    self.chunk.reference_indent( aWeb, aTangler, self.chunk.previous_command.indent() )
-
-    self.logger.debug( "Tangling chunk {!r}".format(self.fullName) )
-    if len(self.chunkList) != 0:
-        for p in self.chunkList:
-            p.tangle( aWeb, aTangler )
-    else:
-        raise Error( "Attempt to tangle an undefined Chunk, {!s}.".format( self.fullName, ) )
-
-    self.chunk.reference_dedent( aWeb, aTangler )
-
- -
-

ReferenceCommand tangle a referenced chunk (90). Used by: ReferenceCommand class... (86)

-
-
-
-
-

Reference Strategy

-

The Reference Strategy has two implementations. An instance -of this is injected into each Chunk by the Web. By injecting this -algorithm, we assure that:

-
    -
  1. each Chunk can produce all relevant reference information and
  2. -
  3. a simple configuration change can be applied to the document.
  4. -
-
-

Reference Superclass

-

The superclass is an abstract class that defines the interface for -this object.

-

Reference class hierarchy - strategies for references to a chunk (91) =

-
-class Reference:
-    def __init__( self ):
-        self.logger= logging.getLogger( self.__class__.__qualname__ )
-    def chunkReferencedBy( self, aChunk ):
-        """Return a list of Chunks."""
-        pass
-
- -
-

Reference class hierarchy - strategies for references to a chunk (91). Used by: Base Class Definitions (1)

-
-
-
-

SimpleReference Class

-

The SimpleReference subclass does the simplest version of resolution. It returns -the Chunks referenced.

-

Reference class hierarchy - strategies for references to a chunk (92) +=

-
-class SimpleReference( Reference ):
-    def chunkReferencedBy( self, aChunk ):
-        refBy= aChunk.referencedBy
-        return refBy
-
- -
-

Reference class hierarchy - strategies for references to a chunk (92). Used by: Base Class Definitions (1)

-
-
-
-

TransitiveReference Class

-

The TransitiveReference subclass does a transitive closure of all -references to this Chunk.

-

This requires walking through the Web to locate "parents" of each referenced -Chunk.

-

Reference class hierarchy - strategies for references to a chunk (93) +=

-
-class TransitiveReference( Reference ):
-    def chunkReferencedBy( self, aChunk ):
-        refBy= aChunk.referencedBy
-        self.logger.debug( "References: {!s}({:d}) {!r}".format(aChunk.name, aChunk.seq, refBy) )
-        return self.allParentsOf( refBy )
-    def allParentsOf( self, chunkList, depth=0 ):
-        """Transitive closure of parents via recursive ascent.
-        """
-        final = []
-        for c in chunkList:
-            final.append( c )
-            final.extend( self.allParentsOf( c.referencedBy, depth+1 ) )
-        self.logger.debug( "References: {0:>{indent}s} {1!s}".format('--', final, indent=2*depth) )
-        return final
-
- -
-

Reference class hierarchy - strategies for references to a chunk (93). Used by: Base Class Definitions (1)

-
-
-
-
-

Error class

-

An Error is raised whenever processing cannot continue. Since it -is a subclass of Exception, it takes an arbitrary number of arguments. The -first should be the basic message text. Subsequent arguments provide -additional details. We will try to be sure that -all of our internal exceptions reference a specific chunk, if possible. -This means either including the chunk as an argument, or catching the -exception and appending the current chunk to the exception's arguments.

-

The Python raise statement takes an instance of Error and passes it -to the enclosing try/except statement for processing.

-

The typical creation is as follows:

-
-raise Error("No full name for {!r}".format(chunk.name), chunk)
-
-

A typical exception-handling suite might look like this:

-
-try:
-    ...something that may raise an Error or Exception...
-except Error as e:
-    print( e.args ) # this is a pyWeb internal Error
-except Exception as w:
-    print( w.args ) # this is some other Python Exception
-
-

The Error class is a subclass of Exception used to differentiate -application-specific -exceptions from other Python exceptions. It does no additional processing, -but merely creates a distinct class to facilitate writing except statements.

-

Error class - defines the errors raised (94) =

-
-class Error( Exception ): pass
-
- -
-

Error class - defines the errors raised (94). Used by: Base Class Definitions (1)

-
-
-
-

The Web and WebReader Classes

-

The overall web of chunks is carried in a -single instance of the Web class that is the principle parameter for the weaving and tangling actions. -Broadly, the functionality of a Web can be separated into several areas.

-

It supports construction methods used by Chunks and WebReader.

-

It also supports "enrichment" of the web, once all the Chunks are known. -This is a stateful update to the web. Each Chunk is updated with Chunk -references it makes as well as Chunks which reference it.

-

It supports Chunk cross-reference methods that traverse this enriched data. -This includes a kind of validity check to be sure that everything is used once -and once only.

-

More importantly, it supports tangle and weave operations.

-

Fundamentally, a Web is a hybrid list-dictionary.

-
    -
  • It's a mapping of chunks that also offers a -moderately sophisticated -lookup, including exact match for a chunk name and an approximate match for a chunk name. -There are several methods to resolve references among chunks.
  • -
  • It's a sequence that retains all chunks in order, also. -It may be a good candidate to be an OrderedDict, but there are multiple keys. Both Chunk -names and chunk numbers are used.
  • -
-

A web instance has a number of attributes.

- --- - - - - - - - - - - - -
webFileName:the name of the original .w file.
chunkSeq:the sequence of Chunk instances as seen in the input file. -To support anonymous chunks, and to assure that the original input document order -is preserved, we keep all chunks in a master sequential list.
output:the @o named OutputChunk chunks. -Each element of this dictionary is a sequence of chunks that have the same name. -The first is the initial definition (marked with "="), all others a second definitions -(marked with "+=").
named:the @d named NamedChunk chunks. Each element of this -dictionary is a sequence of chunks that have the same name. The first is the -initial definition (marked with "="), all others a second definitions -(marked with "+=").
usedBy:the cross reference of chunks referenced by commands in other -chunks.
-
-
!sequence:
-
is used to assign a unique sequence number to each -named chunk.
-
-

Web class - describes the overall "web" of chunks (95) =

-
-class Web:
-    """The overall Web of chunks."""
-    def __init__( self ):
-        self.webFileName= None
-        self.chunkSeq= []
-        self.output= {} # Map filename to Chunk
-        self.named= {} # Map chunkname to Chunk
-        self.sequence= 0
-        self.logger= logging.getLogger( self.__class__.__qualname__ )
-    def __str__( self ):
-        return "Web {!r}".format( self.webFileName, )
-
-    →Web construction methods used by Chunks and WebReader (97)
-    →Web Chunk name resolution methods (102), →(103)
-    →Web Chunk cross reference methods (104), →(106), →(107), →(108)
-    →Web determination of the language from the first chunk (111)
-    →Web tangle the output files (112)
-    →Web weave the output document (113)
-
- -
-

Web class - describes the overall "web" of chunks (95). Used by: Base Class Definitions (1)

-
-
-

Web Construction

-

During web construction, it is convenient to capture -information about the individual Chunk instances being appended to -the web. This done using a Callback design pattern. -Each subclass of Chunk provides an override for the Chunk class -webAdd() method. This override calls one of the appropriate -web construction methods.

-

Also note that the full name for a chunk can be given -either as part of the definition, or as part a reference. -Typically, the first reference has the full name and the definition -has the elided name. This allows a reference to a chunk -to contain a more complete description of the chunk.

-

We include a weakref to the Web to each Chunk.

-

Imports (96) +=

-
-import weakref
-
- -
-

Imports (96). Used by: pyweb.py (153)

-
-

Web construction methods used by Chunks and WebReader (97) =

-
-→Web add full chunk names, ignoring abbreviated names (98)
-→Web add an anonymous chunk (99)
-→Web add a named macro chunk (100)
-→Web add an output file definition chunk (101)
-
- -
-

Web construction methods used by Chunks and WebReader (97). Used by: Web class... (95)

-
-

A name is only added to the known names when it is -a full name, not an abbreviation ending with "...". -Abbreviated names are quietly skipped until the full name -is seen.

-

The algorithm for the addDefName() method, then is as follows:

-
    -
  1. Use the fullNameFor() method to locate the full name.
  2. -
  3. If no full name was found (the result of fullNameFor() ends with '...'), -ignore this name as an abbreviation with no definition.
  4. -
  5. If this is a full name and the name was not in the named mapping, add this full name to the mapping.
  6. -
-

This name resolution approach presents a problem when a chunk's first definition -uses an abbreviated name.

-
-

Note

-

Improved use case needed.

-

We can be more flexible about assembling the web if we -tolerate "..." elipsis as the initial use of a name.

-

We have to fold the abbreviated name(s) -into the Web. Prior to tangling, we have to -resolve the various abbreviated names to their proper -full names. This would then merge the chunks into a single -sequence.

-

We preserve source document ordering between the variations -on the name using the chunk sequence numbers.

-

Here "prior to tangling" means either eagerly -- as each full name arrives -- -or lazily -- after the entire Web is built.

-

We would no longer need to return a value from this function, either.

-
-

Web add full chunk names, ignoring abbreviated names (98) =

-
-def addDefName( self, name ):
-    """Reference to or definition of a chunk name."""
-    nm= self.fullNameFor( name )
-    if nm is None: return None
-    if nm[-3:] == '...':
-        self.logger.debug( "Abbreviated reference {!r}".format(name) )
-        return None # first occurance is a forward reference using an abbreviation
-    if nm not in self.named:
-        self.named[nm]= []
-        self.logger.debug( "Adding empty chunk {!r}".format(name) )
-    return nm
-
- -
-

Web add full chunk names, ignoring abbreviated names (98). Used by: Web construction... (97)

-
-

An anonymous Chunk is kept in a sequence of Chunks, used for -tangling.

-

Web add an anonymous chunk (99) =

-
-def add( self, chunk ):
-    """Add an anonymous chunk."""
-    self.chunkSeq.append( chunk )
-    chunk.web= weakref.ref(self)
-
- -
-

Web add an anonymous chunk (99). Used by: Web construction... (97)

-
-

A named Chunk is defined with a @d command. -It is collected into a mapping of NamedChunk instances. -An entry in the mapping is a sequence of chunks that have the -same name. This sequence of chunks is used to produce the -weave or tangle output.

-

All chunks are also placed in the overall sequence of chunks. -This overall sequence is used for weaving the document.

-

The addDefName() method is used to resolve this name if -it is an abbreviation, or add it to the mapping if this -is the first occurance of the name. If the name cannot be -added, an instance of our Error class is raised. If the name exists or -was added, the chunk is appended to the chunk list associated -with this name.

-

The Web's sequence counter is incremented, and this -unique sequence number sets the seq attribute of the Chunk. -If the chunk list was empty, this is the first chunk, the -initial flag is set to True when there's only one element -in the list. Otherwise, it's False.

-
-

Note

-

Improved use case.

-

If we improve name resolution, then the if and exception can go away. -The addDefName() no longer needs to return a value.

-
-

Web add a named macro chunk (100) =

-
-def addNamed( self, chunk ):
-    """Add a named chunk to a sequence with a given name."""
-    self.chunkSeq.append( chunk )
-    chunk.web= weakref.ref(self)
-    nm= self.addDefName( chunk.name )
-    if nm:
-        # We found the full name for this chunk
-        self.sequence += 1
-        chunk.seq= self.sequence
-        chunk.fullName= nm
-        self.named[nm].append( chunk )
-        chunk.initial= len(self.named[nm]) == 1
-        self.logger.debug( "Extending chunk {!r} from {!r}".format(nm, chunk.name) )
-    else:
-        raise Error("No full name for {!r}".format(chunk.name), chunk)
-
- -
-

Web add a named macro chunk (100). Used by: Web construction... (97)

-
-

An output file definition Chunk is defined with an @o -command. It is collected into a mapping of OutputChunk instances. -An entry in the mapping is a sequence of chunks that have the -same name. This sequence of chunks is used to produce the -weave or tangle output.

-

Note that file names cannot be abbreviated.

-

All chunks are also placed in overall sequence of chunks. -This overall sequence is used for weaving the document.

-

If the name does not exist in the output mapping, -the name is added with an empty sequence of chunks. -In all cases, the chunk is -appended to the chunk list associated -with this name.

-

The web's sequence counter is incremented, and this -unique sequence number sets the Chunk's seq attribute. -If the chunk list was empty, this is the first chunk, the -initial flag is True if this is the first chunk.

-

Web add an output file definition chunk (101) =

-
-def addOutput( self, chunk ):
-    """Add an output chunk to a sequence with a given name."""
-    self.chunkSeq.append( chunk )
-    chunk.web= weakref.ref(self)
-    if chunk.name not in self.output:
-        self.output[chunk.name] = []
-        self.logger.debug( "Adding chunk {!r}".format(chunk.name) )
-    self.sequence += 1
-    chunk.seq= self.sequence
-    chunk.fullName= chunk.name
-    self.output[chunk.name].append( chunk )
-    chunk.initial = len(self.output[chunk.name]) == 1
-
- -
-

Web add an output file definition chunk (101). Used by: Web construction... (97)

-
-
-
-

Web Chunk Name Resolution

-

Web Chunk name resolution has three aspects. The first -is resolving elided names (those ending with ...) to their -full names. The second is finding the named chunk -in the web structure. The third is returning a reference -to a specific chunk including the name and sequence number.

-

Note that a Chunk name actually refers to a sequence -of Chunk instances. Multiple definitions for a Chunk are allowed, and -all of the definitions are concatenated to create the complete -Chunk. This complexity makes it unwise to return the sequence -of same-named Chunk; therefore, we put the burden on the Web to -process all Chunk with a given name, in sequence.

-

The fullNameFor() method resolves full name for a chunk as follows:

-
    -
  1. If the string is already in the named mapping, this is the full name
  2. -
  3. If the string ends in '...', visit each key in the dictionary -to see if the key starts with the string up to the trailing '...'. -If a match is found, the dictionary key is the full name.
  4. -
  5. Otherwise, treat this as a full name.
  6. -
-

Web Chunk name resolution methods (102) =

-
-def fullNameFor( self, name ):
-    """Resolve "..." names into the full name."""
-    if name in self.named: return name
-    if name[-3:] == '...':
-        best= [ n for n in self.named.keys()
-            if n.startswith( name[:-3] ) ]
-        if len(best) > 1:
-            raise Error("Ambiguous abbreviation {!r}, matches {!r}".format( name, list(sorted(best)) ) )
-        elif len(best) == 1:
-            return best[0]
-    return name
-
- -
-

Web Chunk name resolution methods (102). Used by: Web class... (95)

-
-

The getchunk() method locates a named sequence of chunks by first determining the full name -for the identifying string. If full name is in the named mapping, the sequence -of chunks is returned. Otherwise, an instance of our Error class is raised because the name -is unresolvable.

-

It might be more helpful for debugging to emit this as an error in the -weave and tangle results and keep processing. This would allow an author to -catch multiple errors in a single run of pyWeb.

-

Web Chunk name resolution methods (103) +=

-
-def getchunk( self, name ):
-    """Locate a named sequence of chunks."""
-    nm= self.fullNameFor( name )
-    if nm in self.named:
-        return self.named[nm]
-    raise Error( "Cannot resolve {!r} in {!r}".format(name,self.named.keys()) )
-
- -
-

Web Chunk name resolution methods (103). Used by: Web class... (95)

-
-
-
-

Web Cross-Reference Support

-

Cross-reference support includes creating and reporting -on the various cross-references available in a web. This includes -creating the list of chunks that reference a given chunk; -and returning the file, macro and user identifier cross references.

-

Each Chunk has a list Reference commands that shows the chunks -to which a chunk refers. These relationships must be reversed to show -the chunks that refer to a given chunk. This is done by traversing -the entire web of named chunks and recording each chunk-to-chunk reference. -This mapping has the referred-to chunk as -the key, and a sequence of referring chunks as the value.

-

The accumulation is initiated by the web's createUsedBy() method. This -method visits a Chunk, calling the genReferences() method, -passing in the Web instance -as an argument. Each Chunk class genReferences() method, in turn, -invokes the usedBy() method -of each Command instance in the chunk. Most commands do nothing, -but a ReferenceCommand -will resolve the name to which it refers.

-

When the createUsedBy() method has accumulated the entire cross -reference, it also assures that all chunks are used exactly once.

-

Web Chunk cross reference methods (104) =

-
-def createUsedBy( self ):
-    """Update every piece of a Chunk to show how the chunk is referenced.
-    Each piece can then report where it's used in the web.
-    """
-    for aChunk in self.chunkSeq:
-        #usage = (self.fullNameFor(aChunk.name), aChunk.seq)
-        for aRefName in aChunk.genReferences( self ):
-            for c in self.getchunk( aRefName ):
-                c.referencedBy.append( aChunk )
-                c.refCount += 1
-    →Web Chunk check reference counts are all one (105)
-
- -
-

Web Chunk cross reference methods (104). Used by: Web class... (95)

-
-

We verify that the reference count for a -Chunk is exactly one. We don't gracefully tolerate multiple references to -a Chunk or unreferenced chunks.

-

Web Chunk check reference counts are all one (105) =

-
-for nm in self.no_reference():
-    self.logger.warn( "No reference to {!r}".format(nm) )
-for nm in self.multi_reference():
-    self.logger.warn( "Multiple references to {!r}".format(nm) )
-for nm in self.no_definition():
-    self.logger.error( "No definition for {!r}".format(nm) )
-    self.errors += 1
-
- -
-

Web Chunk check reference counts are all one (105). Used by: Web Chunk cross reference methods... (104)

-
-

The one-pass version

-
-for nm,cl in self.named.items():
-    if len(cl) > 0:
-        if cl[0].refCount == 0:
-           self.logger.warn( "No reference to {!r}".format(nm) )
-        elif cl[0].refCount > 1:
-           self.logger.warn( "Multiple references to {!r}".format(nm) )
-    else:
-        self.logger.error( "No definition for {!r}".format(nm) )
-
-

We use three methods to filter chunk names into -the various warning categories. The no_reference list -is a list of chunks defined by never referenced. -The multi_reference list -is a list of chunks defined by never referenced. -The no_definition list -is a list of chunks referenced but not defined.

-

Web Chunk cross reference methods (106) +=

-
-def no_reference( self ):
-    return [ nm for nm,cl in self.named.items() if len(cl)>0 and cl[0].refCount == 0 ]
-def multi_reference( self ):
-    return [ nm for nm,cl in self.named.items() if len(cl)>0 and cl[0].refCount > 1 ]
-def no_definition( self ):
-    return [ nm for nm,cl in self.named.items() if len(cl) == 0 ]
-
- -
-

Web Chunk cross reference methods (106). Used by: Web class... (95)

-
-

The fileXref() method visits all named file output chunks in output and -collects the sequence numbers of each section in the sequence of chunks.

-

The chunkXref() method uses the same algorithm as a the fileXref() method, -but applies it to the named mapping.

-

Web Chunk cross reference methods (107) +=

-
-def fileXref( self ):
-    fx= {}
-    for f,cList in self.output.items():
-        fx[f]= [ c.seq for c in cList ]
-    return fx
-def chunkXref( self ):
-    mx= {}
-    for n,cList in self.named.items():
-        mx[n]= [ c.seq for c in cList ]
-    return mx
-
- -
-

Web Chunk cross reference methods (107). Used by: Web class... (95)

-
-

The userNamesXref() method creates a mapping for each -user identifier. The value for this mapping is a tuple -with the chunk that defined the identifer (via a @| command), -and a sequence of chunks that reference the identifier.

-

For example: -{ 'Web': ( 87, (88,93,96,101,102,104) ), 'Chunk': ( 53, (54,55,56,60,57,58,59) ) }, -shows that the identifier -'Web' is defined in chunk with a sequence number of 87, and referenced -in the sequence of chunks that follow.

-

This works in two passes:

-
    -
  1. _gatherUserId() gathers all user identifiers
  2. -
  3. _updateUserId() searches all text commands for the identifiers -and updates the Web class cross reference information.
  4. -
-

Web Chunk cross reference methods (108) +=

-
-def userNamesXref( self ):
-    ux= {}
-    self._gatherUserId( self.named, ux )
-    self._gatherUserId( self.output, ux )
-    self._updateUserId( self.named, ux )
-    self._updateUserId( self.output, ux )
-    return ux
-def _gatherUserId( self, chunkMap, ux ):
-    →collect all user identifiers from a given map into ux (109)
-def _updateUserId( self, chunkMap, ux ):
-    →find user identifier usage and update ux from the given map (110)
-
- -
-

Web Chunk cross reference methods (108). Used by: Web class... (95)

-
-

User identifiers are collected by visiting each of the sequence of -Chunks that share the -same name; within each component chunk, if chunk has identifiers assigned -by the @| command, these are seeded into the dictionary. -If the chunk does not permit identifiers, it simply returns an empty -list as a default action.

-

collect all user identifiers from a given map into ux (109) =

-
-for n,cList in chunkMap.items():
-    for c in cList:
-        for id in c.getUserIDRefs():
-            ux[id]= ( c.seq, [] )
-
- -
-

collect all user identifiers from a given map into ux (109). Used by: Web Chunk cross reference methods... (108)

-
-

User identifiers are cross-referenced by visiting -each of the sequence of Chunks that share the -same name; within each component chunk, visit each user identifier; -if the Chunk class searchForRE() method matches an identifier, -this is appended to the sequence of chunks that reference the original user identifier.

-

find user identifier usage and update ux from the given map (110) =

-
-# examine source for occurrences of all names in ux.keys()
-for id in ux.keys():
-    self.logger.debug( "References to {!r}".format(id) )
-    idpat= re.compile( r'\W{!s}\W'.format(id) )
-    for n,cList in chunkMap.items():
-        for c in cList:
-            if c.seq != ux[id][0] and c.searchForRE( idpat ):
-                ux[id][1].append( c.seq )
-
- -
-

find user identifier usage and update ux from the given map (110). Used by: Web Chunk cross reference methods... (108)

-
-
-
-

Loop Detection

-

How do we assure that the web is a proper tree and doesn't contain any loops?

-

Consider this example web

-
-@o example1 @{
-    @<part 1A@>
-@}
-
-@d part 1A @{
-    @<part 1B@>
-@}
-
-@d part 1B @{
-    @<part 1A@>
-@}
-
-

All valid chunks are must be referenced from a @o chunk, either directly, -or indirectly via one or more @<name@> references. This defines a -proper tree with @o at the root and children at each @d.

-

Each chunk can have multiple references to further @d definitions. -No chunk can reference the @o definition at the root.

-

To be circular, two @d chunks must reference each other.

-

To be valid, either (or both) must be named by the @o. There will, therefore, -be two references: from the @o and a @d. Our check for duplicate references will spot this.

-

We do not need to do a proper BFS or DFS through the graph to check for loops. -The simple reference count will do.

-
-
-

Tangle and Weave Support

-

The language() method makes a stab at determining the output language. -The determination of the language can be done a variety of ways. -One is to use command line parameters, another is to use the filename -extension on the input file.

-

We examine the first few characters of input. A proper HTML, XHTML or -XML file begins with '<!', '<?' or '<H'. -LaTeX files typically begin with '%' or ''. -Everything else is probably RST.

-

Web determination of the language from the first chunk (111) =

-
-def language( self, preferredWeaverClass=None ):
-    """Construct a weaver appropriate to the document's language"""
-    if preferredWeaverClass:
-        return preferredWeaverClass()
-    self.logger.debug( "Picking a weaver based on first chunk {!r}".format(self.chunkSeq[0][:4]) )
-    if self.chunkSeq[0].startswith('<'):
-        return HTML()
-    if self.chunkSeq[0].startswith('%') or self.chunkSeq[0].startswith('\\'):
-        return LaTeX()
-    return RST()
-
- -
-

Web determination of the language from the first chunk (111). Used by: Web class... (95)

-
-

The tangle() method of the Web class performs -the tangle() method for each Chunk of each -named output file. Note that several Chunks may share the file name, requiring -the file be composed of material from each Chunk, in order.

-

Web tangle the output files (112) =

-
-def tangle( self, aTangler ):
-    for f, c in self.output.items():
-        with aTangler.open(f):
-            for p in c:
-                p.tangle( self, aTangler )
-
- -
-

Web tangle the output files (112). Used by: Web class... (95)

-
-

The weave() method of the Web class creates the final documentation. -This is done by stepping through each Chunk in sequence -and weaving the chunk into the resulting file via the Chunk class weave() method.

-

During weaving of a chunk, the chunk may reference another -chunk. When weaving a reference to a named chunk (output or ordinary programming -source defined with @{), this does not lead to transitive weaving: only a -reference is put in from one chunk to another. However, when weaving -a chunk defined with @[, the chunk is expanded when weaving. -The decision is delegated to the referenced chunk.

-
-
TODO Can we refactor weaveChunk out of here entirely?
-
Should it go in ReferenceCommand weave...?
-
-

Web weave the output document (113) =

-
-def weave( self, aWeaver ):
-    self.logger.debug( "Weaving file from {!r}".format(self.webFileName) )
-    basename, _ = os.path.splitext( self.webFileName )
-    with aWeaver.open(basename):
-        for c in self.chunkSeq:
-            c.weave( self, aWeaver )
-def weaveChunk( self, name, aWeaver ):
-    self.logger.debug( "Weaving chunk {!r}".format(name) )
-    chunkList= self.getchunk(name)
-    if not chunkList:
-        raise Error( "No Definition for {!r}".format(name) )
-    chunkList[0].weaveReferenceTo( self, aWeaver )
-    for p in chunkList[1:]:
-        aWeaver.write( aWeaver.referenceSep() )
-        p.weaveShortReferenceTo( self, aWeaver )
-
- -
-

Web weave the output document (113). Used by: Web class... (95)

-
-
-
-

The WebReader Class

-

There are two forms of the constructor for a WebReader. The -initial WebReader instance is created with code like the following:

-
-p= WebReader()
-p.command = options.commandCharacter
-
-

This will define the command character; usually provided as a command-line parameter to the application.

-

When processing an include file (with the @i command), a child WebReader -instance is created with code like the following:

-
-c= WebReader( parent=parentWebReader )
-
-

This will inherit the configuration from the parent WebReader. -This will also include a reference from child to parent so that embedded Python expressions -can view the entire input context.

-

The WebReader class parses the input file into command blocks. -These are assembled into Chunks, and the Chunks are assembled into the document -Web. Once this input pass is complete, the resulting Web can be tangled or -woven.

-

"Structural" commands define the structure of the Chunks. The structural commands -are @d and @o, as well as the @{, @}, @[, @] brackets, -and the @i command to include another file.

-

"Inline" commands are inline within a Chunk: they define internal Commands. -Blocks of text are minor commands, as well as the @<name@> references. -The @@ escape is also -handled here so that all further processing is independent of any parsing.

-

"Content" commands generate woven content. These include -the various cross-reference commands (@f, @m and @u).

-

There are two class-level OptionParser instances used by this class.

- --- - - - - - - - -
output_option_parser:
 An OptionParser used to parse the @o command.
definition_option_parser:
 An OptionParser used to parse the @d command.
-

The class has the following attributes:

- --- - - - - - - - - - - - - - - - - - - - - - -
parent:is the outer WebReader when processing a @i command.
command:is the command character; a WebReader will use the parent command -character if the parent is not None.
permitList:is the list of commands that are permitted to fail. This is generally -an empty list or ('@i',).
_source:The open source being used by load().
fileName:is used to pass the file name to the Web instance.
theWeb:is the current open Web.
tokenizer:An instance of Tokenizer used to parse the input. This is built -when load() is called.
aChunk:is the current open Chunk being built.
totalLines:
totalFiles:Summaries
-

WebReader class - parses the input file, building the Web structure (114) =

-
-class WebReader:
-    """Parse an input file, creating Chunks and Commands."""
-
-    output_option_parser= OptionParser(
-        OptionDef( "-start", nargs=1, default=None ),
-        OptionDef( "-end", nargs=1, default="" ),
-        OptionDef( "argument", nargs='*' ),
-        )
-
-    definition_option_parser= OptionParser(
-        OptionDef( "-indent", nargs=0 ),
-        OptionDef( "-noindent", nargs=0 ),
-        OptionDef( "argument", nargs='*' ),
-        )
-
-    def __init__( self, parent=None ):
-        self.logger= logging.getLogger( self.__class__.__qualname__ )
-
-        # Configuration of this reader.
-        self.parent= parent
-        if self.parent:
-            self.command= self.parent.command
-            self.permitList= self.parent.permitList
-        else: # Defaults until overridden
-            self.command= '@'
-            self.permitList= []
-
-        # Load options
-        self._source= None
-        self.fileName= None
-        self.theWeb= None
-
-        # State of reading and parsing.
-        self.tokenizer= None
-        self.aChunk= None
-
-        # Summary
-        self.totalLines= 0
-        self.totalFiles= 0
-        self.errors= 0
-
-        →WebReader command literals (130)
-    def __str__( self ):
-        return self.__class__.__name__
-    →WebReader location in the input stream (128)
-    →WebReader load the web (129)
-    →WebReader handle a command string (115), →(127)
-
- -
-

WebReader class - parses the input file, building the Web structure (114). Used by: Base Class Definitions (1)

-
-

Command recognition is done via a Chain of Command-like design. -There are two conditions: the command string is recognized or it is not recognized. -If the command is recognized, handleCommand() either:

-
-
    -
  • (for "structural" commands) attaches the current Chunk (self.aChunk) to the -current Web (self.aWeb), or
  • -
  • (for "inline" and "content" commands) create a Command, attach it to the current -Chunk (self.aChunk)
  • -
-
-

and returns a true result.

-

If the command is not recognized, handleCommand() returns false.

-

A subclass can override handleCommand() to

-
    -
  1. call this superclass version;
  2. -
  3. if the command is unknown to the superclass, -then the subclass can attempt to process it;
  4. -
  5. if the command is unknown to both classes, -then return false. Either a subclass will handle it, or the default activity taken -by load() is to treat the command a text, but also issue a warning.
  6. -
-

WebReader handle a command string (115) =

-
-def handleCommand( self, token ):
-    self.logger.debug( "Reading {!r}".format(token) )
-    →major commands segment the input into separate Chunks (116)
-    →minor commands add Commands to the current Chunk (121)
-    elif token[:2] in (self.cmdlcurl,self.cmdlbrak):
-        # These should have been consumed as part of @o and @d parsing
-        self.logger.error( "Extra {!r} (possibly missing chunk name) near {!r}".format(token, self.location()) )
-        self.errors += 1
-    else:
-        return None # did not recogize the command
-    return True # did recognize the command
-
- -
-

WebReader handle a command string (115). Used by: WebReader class... (114)

-
-

The following sequence of if-elif statements identifies -the structural commands that partition the input into separate Chunks.

-

major commands segment the input into separate Chunks (116) =

-
-if token[:2] == self.cmdo:
-    →start an OutputChunk, adding it to the web (117)
-elif token[:2] == self.cmdd:
-    →start a NamedChunk or NamedDocumentChunk, adding it to the web (118)
-elif token[:2] == self.cmdi:
-    →import another file (119)
-elif token[:2] in (self.cmdrcurl,self.cmdrbrak):
-    →finish a chunk, start a new Chunk adding it to the web (120)
-
- -
-

major commands segment the input into separate Chunks (116). Used by: WebReader handle a command... (115)

-
-

An output chunk has the form @o name @{ content @}. -We use the first two tokens to name the OutputChunk. We simply expect -the @{ separator. We then attach all subsequent commands -to this chunk while waiting for the final @} token to end the chunk.

-

We'll use an OptionParser to locate the optional parameters. This will then let -us build an appropriate instance of OutputChunk.

-

With some small additional changes, we could use OutputChunk( **options ).

-

start an OutputChunk, adding it to the web (117) =

-
-args= next(self.tokenizer)
-self.expect( (self.cmdlcurl,) )
-options= self.output_option_parser.parse( args )
-self.aChunk= OutputChunk( name=options['argument'],
-        comment_start= options.get('start',None),
-        comment_end= options.get('end',""),
-        )
-self.aChunk.fileName= self.fileName
-self.aChunk.webAdd( self.theWeb )
-# capture an OutputChunk up to @}
-
- -
-

start an OutputChunk, adding it to the web (117). Used by: major commands... (116)

-
-

A named chunk has the form @d name @{ content @} for -code and @d name @[ content @] for document source. -We use the first two tokens to name the NamedChunk or NamedDocumentChunk. -We expect either the @{ or @[ separator, and use the actual -token found to choose which subclass of Chunk to create. -We then attach all subsequent commands -to this chunk while waiting for the final @} or @] token to -end the chunk.

-

We'll use an OptionParser to locate the optional parameter of -noindent.

-

[Or possibly -indent number?]

-

Then we can use options to create an appropriate subclass of NamedChunk.

-

If "-indent" is in options, this is the default. -If both are in the options, we can provide a warning, I guess.

-
-TODO Add a warning for conflicting options.
-

start a NamedChunk or NamedDocumentChunk, adding it to the web (118) =

-
-args= next(self.tokenizer)
-brack= self.expect( (self.cmdlcurl,self.cmdlbrak) )
-options= self.output_option_parser.parse( args )
-name=options['argument']
-
-if brack == self.cmdlbrak:
-    self.aChunk= NamedDocumentChunk( name )
-elif brack == self.cmdlcurl:
-    if '-noindent' in options:
-        self.aChunk= NamedChunk_Noindent( name )
-    else:
-        self.aChunk= NamedChunk( name )
-elif brack == None:
-    pass # Error noted by expect()
-else:
-    raise Error( "Design Error" )
-
-self.aChunk.fileName= self.fileName
-self.aChunk.webAdd( self.theWeb )
-# capture a NamedChunk up to @} or @]
-
- -
-

start a NamedChunk or NamedDocumentChunk, adding it to the web (118). Used by: major commands... (116)

-
-

An import command has the unusual form of @i name, with no trailing -separator. When we encounter the @i token, the next token will start with the -file name, but may continue with an anonymous chunk. We require that all @i commands -occur at the end of a line, and break on the '\n' which must occur after the file name. -This permits file names with embedded spaces. It also permits arguments and options, -if really necessary.

-

Once we have split the file name away from the rest of the following anonymous chunk, -we push the following token back into the token stream, so that it will be the -first token examined at the top of the load() loop.

-

We create a child WebReader instance to process the included file. The entire file -is loaded into the current Web instance. A new, empty Chunk is created at the end -of the file so that processing can resume with an anonymous Chunk.

-

The reader has a permitList attribute. -This lists any commands where failure is permitted. Currently, only the @i command -can be set to permit failure; this allows a .w to include -a file that does not yet exist.

-

The primary use case for this feature is when weaving test output. -The first pass of pyWeb tangles the program source files; they are -then run to create test output; the second pass of pyWeb weaves this -test output into the final document via the @i command.

-

import another file (119) =

-
-incFile= next(self.tokenizer).strip()
-try:
-    self.logger.info( "Including {!r}".format(incFile) )
-    include= WebReader( parent=self )
-    include.load( self.theWeb, incFile )
-    self.totalLines += include.tokenizer.lineNumber
-    self.totalFiles += include.totalFiles
-    if include.errors:
-        self.errors += include.errors
-        self.logger.error(
-            "Errors in included file {!s}, output is incomplete.".format(
-            incFile) )
-except Error as e:
-    self.logger.error(
-        "Problems with included file {!s}, output is incomplete.".format(
-        incFile) )
-    self.errors += 1
-except IOError as e:
-    self.logger.error(
-        "Problems with included file {!s}, output is incomplete.".format(
-        incFile) )
-    # Discretionary -- sometimes we want to continue
-    if self.cmdi in self.permitList: pass
-    else: raise # TODO: Seems heavy-handed
-self.aChunk= Chunk()
-self.aChunk.webAdd( self.theWeb )
-
- -
-

import another file (119). Used by: major commands... (116)

-
-

When a @} or @] are found, this finishes a named chunk. The next -text is therefore part of an anonymous chunk.

-

Note that no check is made to assure that the previous Chunk was indeed a named -chunk or output chunk started with @{ or @[. -To do this, an attribute would be -needed for each Chunk subclass that indicated if a trailing bracket was necessary. -For the base Chunk class, this would be false, but for all other subclasses of -Chunk, this would be true.

-

finish a chunk, start a new Chunk adding it to the web (120) =

-
-self.aChunk= Chunk()
-self.aChunk.webAdd( self.theWeb )
-
- -
-

finish a chunk, start a new Chunk adding it to the web (120). Used by: major commands... (116)

-
-

The following sequence of elif statements identifies -the minor commands that add Command instances to the current open Chunk.

-

minor commands add Commands to the current Chunk (121) =

-
-elif token[:2] == self.cmdpipe:
-    →assign user identifiers to the current chunk (122)
-elif token[:2] == self.cmdf:
-    self.aChunk.append( FileXrefCommand(self.tokenizer.lineNumber) )
-elif token[:2] == self.cmdm:
-    self.aChunk.append( MacroXrefCommand(self.tokenizer.lineNumber) )
-elif token[:2] == self.cmdu:
-    self.aChunk.append( UserIdXrefCommand(self.tokenizer.lineNumber) )
-elif token[:2] == self.cmdlangl:
-    →add a reference command to the current chunk (123)
-elif token[:2] == self.cmdlexpr:
-    →add an expression command to the current chunk (125)
-elif token[:2] == self.cmdcmd:
-    →double at-sign replacement, append this character to previous TextCommand (126)
-
- -
-

minor commands add Commands to the current Chunk (121). Used by: WebReader handle a command... (115)

-
-

User identifiers occur after a @| in a NamedChunk.

-

Note that no check is made to assure that the previous Chunk was indeed a named -chunk or output chunk started with @{. -To do this, an attribute would be -needed for each Chunk subclass that indicated if user identifiers are permitted. -For the base Chunk class, this would be false, but for the NamedChunk class and -OutputChunk class, this would be true.

-

User identifiers are name references at the end of a NamedChunk -These are accumulated and expanded by @u reference

-

assign user identifiers to the current chunk (122) =

-
-try:
-    self.aChunk.setUserIDRefs( next(self.tokenizer).strip() )
-except AttributeError:
-    # Out of place @| user identifier command
-    self.logger.error( "Unexpected references near {!s}: {!s}".format(self.location(),token) )
-    self.errors += 1
-
- -
-

assign user identifiers to the current chunk (122). Used by: minor commands... (121)

-
-

A reference command has the form @<name@>. We accept three -tokens from the input, the middle token is the referenced name.

-

add a reference command to the current chunk (123) =

-
-# get the name, introduce into the named Chunk dictionary
-expand= next(self.tokenizer).strip()
-closing= self.expect( (self.cmdrangl,) )
-self.theWeb.addDefName( expand )
-self.aChunk.append( ReferenceCommand( expand, self.tokenizer.lineNumber ) )
-self.aChunk.appendText( "", self.tokenizer.lineNumber ) # to collect following text
-self.logger.debug( "Reading {!r} {!r}".format(expand, closing) )
-
- -
-

add a reference command to the current chunk (123). Used by: minor commands... (121)

-
-

An expression command has the form @(Python Expression@). -We accept three -tokens from the input, the middle token is the expression.

-

There are two alternative semantics for an embedded expression.

-
    -
  • Deferred Execution. This requires definition of a new subclass of Command, -ExpressionCommand, and appends it into the current Chunk. At weave and -tangle time, this expression is evaluated. The insert might look something like this: -aChunk.append( ExpressionCommand(expression, self.tokenizer.lineNumber) ).
  • -
  • Immediate Execution. This simply creates a context and evaluates -the Python expression. The output from the expression becomes a TextCommand, and -is append to the current Chunk.
  • -
-

We use the Immediate Execution semantics.

-

Note that we've removed the blanket os. We only provide os.path. -An os.getcwd() must be changed to os.path.realpath('.').

-

Imports (124) +=

-
-import builtins
-import sys
-import platform
-
- -
-

Imports (124). Used by: pyweb.py (153)

-
-

add an expression command to the current chunk (125) =

-
-# get the Python expression, create the expression result
-expression= next(self.tokenizer)
-self.expect( (self.cmdrexpr,) )
-try:
-    # Build Context
-    safe= types.SimpleNamespace( **dict( (name,obj)
-        for name,obj in builtins.__dict__.items()
-        if name not in ('eval', 'exec', 'open', '__import__')))
-    globals= dict(
-        __builtins__= safe,
-        os= types.SimpleNamespace(path=os.path),
-        datetime= datetime,
-        platform= platform,
-        theLocation= self.location(),
-        theWebReader= self,
-        theFile= self.theWeb.webFileName,
-        thisApplication= sys.argv[0],
-        __version__= __version__,
-        )
-    # Evaluate
-    result= str(eval(expression, globals))
-except Exception as e:
-    self.logger.error( 'Failure to process {!r}: result is {!r}'.format(expression, e) )
-    self.errors += 1
-    result= "@({!r}: Error {!r}@)".format(expression, e)
-self.aChunk.appendText( result, self.tokenizer.lineNumber )
-
- -
-

add an expression command to the current chunk (125). Used by: minor commands... (121)

-
-

A double command sequence ('@@', when the command is an '@') has the -usual meaning of '@' in the input stream. We do this via -the appendText() method of the current Chunk. This will append the -character on the end of the most recent TextCommand; if this fails, it will -create a new, empty TextCommand.

-

We replace with '@' here and now! This is put this at the end of the previous chunk. -And we make sure the next chunk will be appended to this so that it's -largely seamless.

-

double at-sign replacement, append this character to previous TextCommand (126) =

-
-self.aChunk.appendText( self.command, self.tokenizer.lineNumber )
-
- -
-

double at-sign replacement, append this character to previous TextCommand (126). Used by: minor commands... (121)

-
-

The expect() method examines the -next token to see if it is the expected item. '\n' are absorbed. -If this is not found, a standard type of error message is raised. -This is used by handleCommand().

-

WebReader handle a command string (127) +=

-
-def expect( self, tokens ):
-    try:
-        t= next(self.tokenizer)
-        while t == '\n':
-            t= next(self.tokenizer)
-    except StopIteration:
-        self.logger.error( "At {!r}: end of input, {!r} not found".format(self.location(),tokens) )
-        self.errors += 1
-        return
-    if t not in tokens:
-        self.logger.error( "At {!r}: expected {!r}, found {!r}".format(self.location(),tokens,t) )
-        self.errors += 1
-        return
-    return t
-
- -
-

WebReader handle a command string (127). Used by: WebReader class... (114)

-
-

The location() provides the file name and line number. -This allows error messages as well as tangled or woven output -to correctly reference the original input files.

-

WebReader location in the input stream (128) =

-
-def location( self ):
-    return (self.fileName, self.tokenizer.lineNumber+1)
-
- -
-

WebReader location in the input stream (128). Used by: WebReader class... (114)

-
-

The load() method reads the entire input file as a sequence -of tokens, split up by the Tokenizer. Each token that appears -to be a command is passed to the handleCommand() method. If -the handleCommand() method returns a True result, the command was recognized -and placed in the Web. If handleCommand() returns a False result, the command -was unknown, and we write a warning but treat it as text.

-

The load() method is used recursively to handle the @i command. The issue -is that it's always loading a single top-level web.

-

WebReader load the web (129) =

-
-def load( self, web, filename, source=None ):
-    self.theWeb= web
-    self.fileName= filename
-
-    # Only set the a web filename once using the first file.
-    # This should be a setter property of the web.
-    if self.theWeb.webFileName is None:
-        self.theWeb.webFileName= self.fileName
-
-    if source:
-        self._source= source
-        self.parse_source()
-    else:
-        with open( self.fileName, "r" ) as self._source:
-            self.parse_source()
-
-def parse_source( self ):
-        self.tokenizer= Tokenizer( self._source, self.command )
-        self.totalFiles += 1
-
-        self.aChunk= Chunk() # Initial anonymous chunk of text.
-        self.aChunk.webAdd( self.theWeb )
-
-        for token in self.tokenizer:
-            if len(token) >= 2 and token.startswith(self.command):
-                if self.handleCommand( token ):
-                    continue
-                else:
-                    self.logger.warn( 'Unknown @-command in input: {!r}'.format(token) )
-                    self.aChunk.appendText( token, self.tokenizer.lineNumber )
-            elif token:
-                # Accumulate a non-empty block of text in the current chunk.
-                self.aChunk.appendText( token, self.tokenizer.lineNumber )
-
- -
-

WebReader load the web (129). Used by: WebReader class... (114)

-
-

The command character can be changed to permit -some flexibility when working with languages that make extensive -use of the @ symbol, i.e., PERL. -The initialization of the WebReader is based on the selected -command character.

-

WebReader command literals (130) =

-
-# Structural ("major") commands
-self.cmdo= self.command+'o'
-self.cmdd= self.command+'d'
-self.cmdlcurl= self.command+'{'
-self.cmdrcurl= self.command+'}'
-self.cmdlbrak= self.command+'['
-self.cmdrbrak= self.command+']'
-self.cmdi= self.command+'i'
-
-# Inline ("minor") commands
-self.cmdlangl= self.command+'<'
-self.cmdrangl= self.command+'>'
-self.cmdpipe= self.command+'|'
-self.cmdlexpr= self.command+'('
-self.cmdrexpr= self.command+')'
-self.cmdcmd= self.command+self.command
-
-# Content "minor" commands
-self.cmdf= self.command+'f'
-self.cmdm= self.command+'m'
-self.cmdu= self.command+'u'
-
- -
-

WebReader command literals (130). Used by: WebReader class... (114)

-
-
-
-

The Tokenizer Class

-

The WebReader requires a tokenizer. The tokenizer breaks the input text -into a stream of tokens. There are two broad classes of tokens:

-
    -
  • @. command tokens, including the structural, inline, and content -commands.
  • -
  • \n. Inside text, these matter. Within structure command tokens, these don't matter. -Except after the filename after an @i command, where it ends the command.
  • -
  • The remaining text.
  • -
-

The tokenizer works by reading the entire file and splitting on @. patterns. -The split() method of the Python re module will separate the input -and preserve the actual character sequence on which the input was split. -This breaks the input into blocks of text separated by the @. characters.

-

This tokenizer splits the input using (r'@.|\n'). The idea is that -we locate commands, newlines and the interstitial text as three classes of tokens. -We can then assemble each Command instance from a short sequence of tokens. -The core TextCommand and CodeCommand will be a line of text ending with -the \n.

-

The re.split() method will include an empty string when the split pattern occurs -at the very beginning or very end of the input. For example:

-
->>> pat.split( "@{hi mom@}")
-['', '@{', 'hi mom', '@}', '']
-
-

We can safely filter these via a generator expression.

-

The tokenizer counts newline characters for us, so that error messages can include -a line number. Also, we can tangle comments into the file that include line numbers.

-

Since the tokenizer is a proper iterator, we can use tokens= iter(Tokenizer(source)) -and next(tokens) to step through the sequence of tokens until we raise a StopIteration -exception.

-

Imports (131) +=

-
-import re
-
- -
-

Imports (131). Used by: pyweb.py (153)

-
-

Tokenizer class - breaks input into tokens (132) =

-
-class Tokenizer:
-    def __init__( self, stream, command_char='@' ):
-        self.command= command_char
-        self.parsePat= re.compile( r'({!s}.|\n)'.format(self.command) )
-        self.token_iter= (t for t in self.parsePat.split( stream.read() ) if len(t) != 0)
-        self.lineNumber= 0
-    def __next__( self ):
-        token= next(self.token_iter)
-        self.lineNumber += token.count('\n')
-        return token
-    def __iter__( self ):
-        return self
-
- -
-

Tokenizer class - breaks input into tokens (132). Used by: Base Class Definitions (1)

-
-
-
-

The Option Parser Class

-

For some commands (@d and @o) we have options as well as the chunk name -or file name. This roughly parallels the way Tcl or the shell works.

-

The two examples are

-
    -
  • @o which has an optional -start and -end that are used to -provide comment bracketing information. For example:

    -

    @0 -start /* -end */ something.css

    -

    Provides two options in addition to the required filename.

    -
  • -
  • @d which has an optional -noident or -indent that is used to -provide the indentation rules for this chunk. Some chunks are not indented -automatically. It's up to the author to get the indentation right. This is -used in the case of a Python """ string that would be ruined by indentation.

    -
  • -
-

To handle this, we have a separate lexical scanner and parser for these -two commands.

-

Imports (133) +=

-
-import shlex
-
- -
-

Imports (133). Used by: pyweb.py (153)

-
-

Here's how we can define an option.

-
-OptionParser(
-    OptionDef( "-start", nargs=1, default=None ),
-    OptionDef( "-end", nargs=1, default="" ),
-    OptionDef( "-indent", nargs=0 ), # A default
-    OptionDef( "-noindent", nargs=0 ),
-    OptionDef( "argument", nargs='*' ),
-    )
-
-

The idea is to parallel argparse.add_argument() syntax.

-

Option Parser class - locates optional values on commands (134) =

-
-class OptionDef:
-    def __init__( self, name, **kw ):
-        self.name= name
-        self.__dict__.update( kw )
-
- -
-

Option Parser class - locates optional values on commands (134). Used by: Base Class Definitions (1)

-
-

The parser breaks the text into words using shelex rules. -It then steps through the words, accumulating the options and the -final argument value.

-

Option Parser class - locates optional values on commands (135) +=

-
-class OptionParser:
-    def __init__( self, *arg_defs ):
-        self.args= dict( (arg.name,arg) for arg in arg_defs )
-        self.trailers= [k for k in self.args.keys() if not k.startswith('-')]
-    def parse( self, text ):
-        try:
-            word_iter= iter(shlex.split(text))
-        except ValueError as e:
-            raise Error( "Error parsing options in {!r}".format(text) )
-        options = dict( s for s in self._group( word_iter ) )
-        return options
-    def _group( self, word_iter ):
-        option, value, final= None, [], []
-        for word in word_iter:
-            if word == '--':
-                if option:
-                    yield option, value
-                try:
-                    final= [next(word_iter)]
-                except StopIteration:
-                    final= [] # Special case of '--' at the end.
-                break
-            elif word.startswith('-'):
-                if word in self.args:
-                    if option:
-                        yield option, value
-                    option, value = word, []
-                else:
-                    raise ParseError( "Unknown option {0}".format(word) )
-            else:
-                if option:
-                    if self.args[option].nargs == len(value):
-                        yield option, value
-                        final= [word]
-                        break
-                    else:
-                        value.append( word )
-                else:
-                    final= [word]
-                    break
-        # In principle, we step through the trailers based on nargs counts.
-        for word in word_iter:
-            final.append( word )
-        yield self.trailers[0], " ".join(final)
-
- -
-

Option Parser class - locates optional values on commands (135). Used by: Base Class Definitions (1)

-
-

In principle, we step through the trailers based on nargs counts. -Since we only ever have the one trailer, we skate by.

-

The loop becomes a bit more complex to capture the positional arguments, in order. -First, we have to use an OrderedDict instead of a dict.

-

Then we'd have a loop something like this. (Untested, incomplete, just hand-waving.)

-
-trailers= self.trailers[:] # Stateful shallow copy
-for word in word_iter:
-    if len(final) == trailers[-1].nargs: # nargs=='*' vs. nargs=int??
-        yield trailers[0], " ".join(final)
-        final= 0
-        trailers.pop(0)
-yield trailers[0], " ".join(final)
-
-
-
-
-

Action Class Hierarchy

-

This application performs three major actions: loading the document web, -weaving and tangling. Generally, -the use case is to perform a load, weave and tangle. However, a less common use case -is to first load and tangle output files, run a regression test and then -load and weave a result that includes the test output file.

-

The -x option excludes one of the two output actions. The -xw -excludes the weave pass, doing only the tangle action. The -xt excludes -the tangle pass, doing the weave action.

-

This two pass action might be embedded in the following type of Python program.

-
-import pyweb, os, runpy, sys
-pyweb.tangle( "source.w" )
-with open("source.log", "w") as target:
-    sys.stdout= target
-    runpy.run_path( 'source.py' )
-    sys.stdout= sys.__stdout__
-pyweb.weave( "source.w" )
-
-

The first step runs pyWeb, excluding the final weaving pass. The second -step runs the tangled program, source.py, and produces test results in -some log file, source.log. The third step runs pyWeb excluding the -tangle pass. This produces a final document that includes the source.log -test results.

-

To accomplish this, we provide a class hierarchy that defines the various -actions of the pyWeb application. This class hierarchy defines an extensible set of -fundamental actions. This gives us the flexibility to create a simple sequence -of actions and execute any combination of these. It eliminates the need for a -forest of if-statements to determine precisely what will be done.

-

Each action has the potential to update the state of the overall -application. A partner with this command hierarchy is the Application class -that defines the application options, inputs and results.

-

Action class hierarchy - used to describe basic actions of the application (136) =

-
-→Action superclass has common features of all actions (137)
-→ActionSequence subclass that holds a sequence of other actions (140)
-→WeaveAction subclass initiates the weave action (144)
-→TangleAction subclass initiates the tangle action (147)
-→LoadAction subclass loads the document web (150)
-
- -
-

Action class hierarchy - used to describe basic actions of the application (136). Used by: Base Class Definitions (1)

-
-
-

Action Class

-

The Action class embodies the basic operations of pyWeb. -The intent of this hierarchy is to both provide an easily expanded method of -adding new actions, but an easily specified list of actions for a particular -run of pyWeb.

-

The overall process of the application is defined by an instance of Action. -This instance may be the WeaveAction instance, the TangleAction instance -or a ActionSequence instance.

-

The instance is constructed during parsing of the input parameters. Then the -Action class perform() method is called to actually perform the -action. There are three standard Action instances available: an instance -that is a macro and does both tangling and weaving, an instance that excludes tangling, -and an instance that excludes weaving. These correspond to the command-line options.

-
-anOp= SomeAction( parameters )
-anOp.options= argparse.Namespace
-anOp.web = Current web
-anOp()
-
-

The Action is the superclass for all actions. -An Action has a number of common attributes.

- --- - - - - - - - -
name:A name for this action.
options:The argparse.Namespace object.
web:The current web that's being processed.
-
-
!start:
-
The time at which the action started.
-
-

Action superclass has common features of all actions (137) =

-
-class Action:
-    """An action performed by pyWeb."""
-    def __init__( self, name ):
-        self.name= name
-        self.web= None
-        self.options= None
-        self.start= None
-        self.logger= logging.getLogger( self.__class__.__qualname__ )
-    def __str__( self ):
-        return "{!s} [{!s}]".format( self.name, self.web )
-    →Action call method actually does the real work (138)
-    →Action final summary of what was done (139)
-
- -
-

Action superclass has common features of all actions (137). Used by: Action class hierarchy... (136)

-
-

The __call__() method does the real work of the action. -For the superclass, it merely logs a message. This is overridden -by a subclass.

-

Action call method actually does the real work (138) =

-
-def __call__( self ):
-    self.logger.info( "Starting {!s}".format(self.name) )
-    self.start= time.process_time()
-
- -
-

Action call method actually does the real work (138). Used by: Action superclass... (137)

-
-

The summary() method returns some basic processing -statistics for this action.

-

Action final summary of what was done (139) =

-
-def duration( self ):
-    """Return duration of the action."""
-    return (self.start and time.process_time()-self.start) or 0
-def summary( self ):
-    return "{!s} in {:0.2f} sec.".format( self.name, self.duration() )
-
- -
-

Action final summary of what was done (139). Used by: Action superclass... (137)

-
-
-
-

ActionSequence Class

-

A ActionSequence defines a composite action; it is a sequence of -other actions. When the macro is performed, it delegates to the -sub-actions.

-

The instance is created during parsing of input parameters. An instance of -this class is one of -the three standard actions available; it generally is the default, "do everything" -action.

-

This class overrides the perform() method of the superclass. It also adds -an append() method that is used to construct the sequence of actions.

-

ActionSequence subclass that holds a sequence of other actions (140) =

-
-class ActionSequence( Action ):
-    """An action composed of a sequence of other actions."""
-    def __init__( self, name, opSequence=None ):
-        super().__init__( name )
-        if opSequence: self.opSequence= opSequence
-        else: self.opSequence= []
-    def __str__( self ):
-        return "; ".join( [ str(x) for x in self.opSequence ] )
-    →ActionSequence call method delegates the sequence of ations (141)
-    →ActionSequence append adds a new action to the sequence (142)
-    →ActionSequence summary summarizes each step (143)
-
- -
-

ActionSequence subclass that holds a sequence of other actions (140). Used by: Action class hierarchy... (136)

-
-

Since the macro __call__() method delegates to other Actions, -it is possible to short-cut argument processing by using the Python -*args construct to accept all arguments and pass them to each -sub-action.

-

ActionSequence call method delegates the sequence of ations (141) =

-
-def __call__( self ):
-    for o in self.opSequence:
-        o.web= self.web
-        o.options= self.options
-        o()
-
- -
-

ActionSequence call method delegates the sequence of ations (141). Used by: ActionSequence subclass... (140)

-
-

Since this class is essentially a wrapper around the built-in sequence type, -we delegate sequence related actions directly to the underlying sequence.

-

ActionSequence append adds a new action to the sequence (142) =

-
-def append( self, anAction ):
-    self.opSequence.append( anAction )
-
- -
-

ActionSequence append adds a new action to the sequence (142). Used by: ActionSequence subclass... (140)

-
-

The summary() method returns some basic processing -statistics for each step of this action.

-

ActionSequence summary summarizes each step (143) =

-
-def summary( self ):
-    return ", ".join( [ o.summary() for o in self.opSequence ] )
-
- -
-

ActionSequence summary summarizes each step (143). Used by: ActionSequence subclass... (140)

-
-
-
-

WeaveAction Class

-

The WeaveAction defines the action of weaving. This action -logs a message, and invokes the weave() method of the Web instance. -This method also includes the basic decision on which weaver to use. If a Weaver was -specified on the command line, this instance is used. Otherwise, the first few characters -are examined and a weaver is selected.

-

This class overrides the __call__() method of the superclass.

-

If the options include theWeaver, that Weaver instance will be used. -Otherwise, the web.language() method function is used to guess what weaver to use.

-

WeaveAction subclass initiates the weave action (144) =

-
-class WeaveAction( Action ):
-    """Weave the final document."""
-    def __init__( self ):
-        super().__init__( "Weave" )
-    def __str__( self ):
-        return "{!s} [{!s}, {!s}]".format( self.name, self.web, self.theWeaver )
-
-    →WeaveAction call method to pick the language (145)
-    →WeaveAction summary of language choice (146)
-
- -
-

WeaveAction subclass initiates the weave action (144). Used by: Action class hierarchy... (136)

-
-

The language is picked just prior to weaving. It is either (1) the language -specified on the command line, or, (2) if no language was specified, a language -is selected based on the first few characters of the input.

-

Weaving can only raise an exception when there is a reference to a chunk that -is never defined.

-

WeaveAction call method to pick the language (145) =

-
-def __call__( self ):
-    super().__call__()
-    if not self.options.theWeaver:
-        # Examine first few chars of first chunk of web to determine language
-        self.options.theWeaver= self.web.language()
-        self.logger.info( "Using {0}".format(self.options.theWeaver.__class__.__name__) )
-    self.options.theWeaver.reference_style= self.options.reference_style
-    try:
-        self.web.weave( self.options.theWeaver )
-        self.logger.info( "Finished Normally" )
-    except Error as e:
-        self.logger.error(
-            "Problems weaving document from {!s} (weave file is faulty).".format(
-            self.web.webFileName) )
-        #raise
-
- -
-

WeaveAction call method to pick the language (145). Used by: WeaveAction subclass... (144)

-
-

The summary() method returns some basic processing -statistics for the weave action.

-

WeaveAction summary of language choice (146) =

-
-def summary( self ):
-    if self.options.theWeaver and self.options.theWeaver.linesWritten > 0:
-        return "{!s} {:d} lines in {:0.2f} sec.".format( self.name,
-        self.options.theWeaver.linesWritten, self.duration() )
-    return "did not {!s}".format( self.name, )
-
- -
-

WeaveAction summary of language choice (146). Used by: WeaveAction subclass... (144)

-
-
-
-

TangleAction Class

-

The TangleAction defines the action of tangling. This operation -logs a message, and invokes the weave() method of the Web instance. -This method also includes the basic decision on which weaver to use. If a Weaver was -specified on the command line, this instance is used. Otherwise, the first few characters -are examined and a weaver is selected.

-

This class overrides the __call__() method of the superclass.

-

The options must include theTangler, with the Tangler instance to be used.

-

TangleAction subclass initiates the tangle action (147) =

-
-class TangleAction( Action ):
-    """Tangle source files."""
-    def __init__( self ):
-        super().__init__( "Tangle" )
-    →TangleAction call method does tangling of the output files (148)
-    →TangleAction summary method provides total lines tangled (149)
-
- -
-

TangleAction subclass initiates the tangle action (147). Used by: Action class hierarchy... (136)

-
-

Tangling can only raise an exception when a cross reference request (@f, @m or @u) -occurs in a program code chunk. Program code chunks are defined -with any of @d or @o and use @{ @} brackets.

-

TangleAction call method does tangling of the output files (148) =

-
-def __call__( self ):
-    super().__call__()
-    self.options.theTangler.include_line_numbers= self.options.tangler_line_numbers
-    try:
-        self.web.tangle( self.options.theTangler )
-    except Error as e:
-        self.logger.error(
-            "Problems tangling outputs from {!r} (tangle files are faulty).".format(
-            self.web.webFileName) )
-        #raise
-
- -
-

TangleAction call method does tangling of the output files (148). Used by: TangleAction subclass... (147)

-
-

The summary() method returns some basic processing -statistics for the tangle action.

-

TangleAction summary method provides total lines tangled (149) =

-
-def summary( self ):
-    if self.options.theTangler and self.options.theTangler.linesWritten > 0:
-        return "{!s} {:d} lines in {:0.2f} sec.".format( self.name,
-        self.options.theTangler.totalLines, self.duration() )
-    return "did not {!r}".format( self.name, )
-
- -
-

TangleAction summary method provides total lines tangled (149). Used by: TangleAction subclass... (147)

-
-
-
-

LoadAction Class

-

The LoadAction defines the action of loading the web structure. This action -uses the application's webReader to actually do the load.

-

An instance is created during parsing of the input parameters. An instance of -this class is part of any of the weave, tangle and "do everything" action.

-

This class overrides the __call__() method of the superclass.

-

The options must include webReader, with the WebReader instance to be used.

-

LoadAction subclass loads the document web (150) =

-
-class LoadAction( Action ):
-    """Load the source web."""
-    def __init__( self ):
-        super().__init__( "Load" )
-    def __str__( self ):
-        return "Load [{!s}, {!s}]".format( self.webReader, self.web )
-    →LoadAction call method loads the input files (151)
-    →LoadAction summary provides lines read (152)
-
- -
-

LoadAction subclass loads the document web (150). Used by: Action class hierarchy... (136)

-
-

Trying to load the web involves two steps, either of which can raise -exceptions due to incorrect inputs.

-
    -
  1. The WebReader class load() method can raise exceptions for a number of -syntax errors as well as OS errors.
      -
    • Missing closing brackets (@}, @] or @>).
    • -
    • Missing opening bracket (@{ or @[) after a chunk name (@d or @o).
    • -
    • Extra brackets (@{, @[, @}, @]).
    • -
    • Extra @|.
    • -
    • The input file does not exist or is not readable.
    • -
    -
  2. -
  3. The Web class createUsedBy() method can raise an exception when a -chunk reference cannot be resolved to a named chunk.
  4. -
-

LoadAction call method loads the input files (151) =

-
-def __call__( self ):
-    super().__call__()
-    self.webReader= self.options.webReader
-    self.webReader.command= self.options.command
-    self.webReader.permitList= self.options.permitList
-    self.web.webFileName= self.options.webFileName
-    error= "Problems with source file {!r}, no output produced.".format(
-            self.options.webFileName)
-    try:
-        self.webReader.load( self.web, self.options.webFileName )
-        if self.webReader.errors != 0:
-            self.logger.error( error )
-            raise Error( "Syntax Errors in the Web" )
-        self.web.createUsedBy()
-        if self.webReader.errors != 0:
-            self.logger.error( error )
-            raise Error( "Internal Reference Errors in the Web" )
-    except Error as e:
-        self.logger.error(error)
-        raise # Older design.
-    except IOError as e:
-        self.logger.error(error)
-        raise
-
- -
-

LoadAction call method loads the input files (151). Used by: LoadAction subclass... (150)

-
-

The summary() method returns some basic processing -statistics for the load action.

-

LoadAction summary provides lines read (152) =

-
-def summary( self ):
-    return "{!s} {:d} lines from {:d} files in {:0.2f} sec.".format(
-        self.name, self.webReader.totalLines,
-        self.webReader.totalFiles, self.duration() )
-
- -
-

LoadAction summary provides lines read (152). Used by: LoadAction subclass... (150)

-
-
-
-
-

pyWeb Module File

-

The pyWeb application file is shown below:

-

pyweb.py (153) =

-
-→Overheads (155), →(156), →(157)
-→Imports (11), →(47), →(96), →(124), →(131), →(133), →(154), →(158), →(164)
-→Base Class Definitions (1)
-→Application Class (159), →(160)
-→Logging Setup (165), →(166)
-→Interface Functions (167)
-
- -
-

pyweb.py (153).

-
-

The Overheads are described below, they include things like:

-
    -
  • shell escape
  • -
  • doc string
  • -
  • __version__ setting
  • -
-

Python Library Imports are actually scattered in various places in this description.

-

The more important elements are described in separate sections:

-
    -
  • Base Class Definitions
  • -
  • Application Class and Main Functions
  • -
  • Interface Functions
  • -
-
-

Python Library Imports

-

Numerous Python library modules are used by this application.

-

A few are listed here because they're used widely. Others are listed -closer to where they're referenced.

-
    -
  • The os module provide os-specific file and path manipulations; it is used -to transform the input file name into the output file name as well as track down file modification -times.
  • -
  • The time module provides a handy current-time string; this is used -to by the HTML Weaver to write a closing timestamp on generated HTML files, -as well as log messages.
  • -
  • The datetime module is used to format times, phasing out use of time.
  • -
  • The types module is used to get at SimpleNamespace for configuration.
  • -
-

Imports (154) +=

-
-import os
-import time
-import datetime
-import types
-
- -
-

Imports (154). Used by: pyweb.py (153)

-
-

Note that os.path, time, datetime and platform` -are provided in the expression context.

-
-
-

Overheads

-

The shell escape is provided so that the user can define this -file as executable, and launch it directly from their shell. -The shell reads the first line of a file; when it finds the '#!' shell -escape, the remainder of the line is taken as the path to the binary program -that should be run. The shell runs this binary, providing the -file as standard input.

-

Overheads (155) =

-
-#!/usr/bin/env python
-
- -
-

Overheads (155). Used by: pyweb.py (153)

-
-

A Python __doc__ string provides a standard vehicle for documenting -the module or the application program. The usual style is to provide -a one-sentence summary on the first line. This is followed by more -detailed usage information.

-

Overheads (156) +=

-
-"""pyWeb Literate Programming - tangle and weave tool.
-
-Yet another simple literate programming tool derived from nuweb,
-implemented entirely in Python.
-This produces any markup for any programming language.
-
-Usage:
-    pyweb.py [-dvs] [-c x] [-w format] file.w
-
-Options:
-    -v           verbose output (the default)
-    -s           silent output
-    -d           debugging output
-    -c x         change the command character from '@' to x
-    -w format    Use the given weaver for the final document.
-                 Choices are rst, html, latex and htmlshort.
-                 Additionally, a `module.class` name can be used.
-    -xw          Exclude weaving
-    -xt          Exclude tangling
-    -pi          Permit include-command errors
-    -rt          Transitive references
-    -rs          Simple references (default)
-    -n           Include line number comments in the tangled source; requires
-                 comment start and stop on the @o commands.
-
-    file.w       The input file, with @o, @d, @i, @[, @{, @|, @<, @f, @m, @u commands.
-"""
-
- -
-

Overheads (156). Used by: pyweb.py (153)

-
-

The keyword cruft is a standard way of placing version control information into -a Python module so it is preserved. See PEP (Python Enhancement Proposal) #8 for information -on recommended styles.

-

We also sneak in a "DO NOT EDIT" warning that belongs in all generated application -source files.

-

Overheads (157) +=

-
-__version__ = """2.3.2"""
-
-### DO NOT EDIT THIS FILE!
-### It was created by /Users/slott/Documents/Projects/pyWeb-2.3/pyweb/pyweb.py, __version__='2.3.2'.
-### From source pyweb.w modified Mon Mar 17 10:13:24 2014.
-### In working directory '/Users/slott/Documents/Projects/pyWeb-2.3/pyweb'.
-
- -
-

Overheads (157). Used by: pyweb.py (153)

-
-
-
-
-

The Application Class

-

The Application class is provided so that the Action instances -have an overall application to update. This allows the WeaveAction to -provide the selected Weaver instance to the application. It also provides a -central location for the various options and alternatives that might be accepted from -the command line.

-

The constructor creates a default argparse.Namespace with values -suitable for weaving and tangling.

-

The parseArgs() method uses the sys.argv sequence to -parse the command line arguments and update the options. This allows a -program to pre-process the arguments, passing other arguments to this module.

-

The process() method processes a list of files. This is either -the list of files passed as an argument, or it is the list of files -parsed by the parseArgs() method.

-

The parseArgs() and process() functions are separated so that -another application can import pyweb, bypass command-line parsing, yet still perform -the basic actionss simply and consistently. -For example:

-
-import pyweb, argparse
-
-p= argparse.ArgumentParser()
-argument definition
-config = p.parse_args()
-
-a= pyweb.Application()
-Configure the Application based on options
-a.process( config )
-
-

The main() function creates an Application instance and -calls the parseArgs() and process() methods to provide the -expected default behavior for this module when it is used as the main program.

-

The configuration can be either a types.SimpleNamespace or an -argparse.Namespace instance.

-

Imports (158) +=

-
-import argparse
-
- -
-

Imports (158). Used by: pyweb.py (153)

-
-

Application Class (159) =

-
-class Application:
-    def __init__( self ):
-        self.logger= logging.getLogger( self.__class__.__qualname__ )
-        →Application default options (161)
-    →Application parse command line (162)
-    →Application class process all files (163)
-
- -
-

Application Class (159). Used by: pyweb.py (153)

-
-

The first part of parsing the command line is -setting default values that apply when parameters are omitted. -The default values are set as follows:

- --- - - - - - - - - - - - -
defaults:A default configuration.
webReader:is the WebReader instance created for the current -input file.
doWeave:instance of Action -that does weaving only.
doTangle:instance of Action -that does tangling only.
theAction:is an instance of Action that describes -the default overall action: load, tangle and weave. This is the default unless -overridden by an option.
-

Here are the configuration values. These are attributes -of the argparse.namespace default as well as the updated -namespace returned by parseArgs().

- --- - - - - - - - - - - - -
verbosity:Either logging.INFO, logging.WARN or logging.DEBUG
command:is set to @ as the default command introducer.
permit:The raw list of permitted command characters, perhaps 'i'.
permitList:provides a list of commands that are permitted -to fail. Typically this is empty, or contains @i to allow the include -command to fail.
files:is the final list of argument files from the command line; -these will be processed unless overridden in the call to process().
-
-
!skip:
-
a list of steps to skip: perhaps 'w' or 't' to skip weaving or tangling.
-
- --- - - - - - - - -
weaver:the short name of the weaver.
theTangler:is set to a TanglerMake instance -to create the output files.
theWeaver:is set to an instance of a subclass of Weaver based on weaver
-

Other instance variables.

-

Here's the global list of available weavers. Essentially this is the subclass -list of Weaver. Essentially, the list is this:

-
-weavers = dict(
-    (x.__class__.__name__.lower(), x)
-    for x in Weaver.__subclasses__()
-)
-
-

Rather than automate this, and potentially expose elements of the class hierarchy -that aren't really meant to be used, we provide a manually-developed list.

-

Application Class (160) +=

-
-# Global list of available weaver classes.
-weavers = {
-    'html':  HTML,
-    'htmlshort': HTMLShort,
-    'latex': LaTeX,
-    'rst': RST,
-}
-
- -
-

Application Class (160). Used by: pyweb.py (153)

-
-

The defaults used for application configuration. The expand() method expands -on these simple text values to create more useful objects.

-

Application default options (161) =

-
-self.defaults= argparse.Namespace(
-    verbosity= logging.INFO,
-    command= '@',
-    weaver= 'rst',
-    skip= '', # Don't skip any steps
-    permit= '', # Don't tolerate missing includes
-    reference= 's', # Simple references
-    tangler_line_numbers= False,
-    )
-self.expand( self.defaults )
-
-# Primitive Actions
-self.loadOp= LoadAction()
-self.weaveOp= WeaveAction()
-self.tangleOp= TangleAction()
-
-# Composite Actions
-self.doWeave= ActionSequence( "load and weave", [self.loadOp, self.weaveOp] )
-self.doTangle= ActionSequence( "load and tangle", [self.loadOp, self.tangleOp] )
-self.theAction= ActionSequence( "load, tangle and weave", [self.loadOp, self.tangleOp, self.weaveOp] )
-
- -
-

Application default options (161). Used by: Application Class... (159)

-
-

The algorithm for parsing the command line parameters uses the built in -argparse module. We have to build a parser, define the options, -and the parse the command-line arguments, updating the default namespace.

-

We further expand on the arguments. This transforms simple strings into object -instances.

-

Application parse command line (162) =

-
-def parseArgs( self ):
-    p = argparse.ArgumentParser()
-    p.add_argument( "-v", "--verbose", dest="verbosity", action="store_const", const=logging.INFO )
-    p.add_argument( "-s", "--silent", dest="verbosity", action="store_const", const=logging.WARN )
-    p.add_argument( "-d", "--debug", dest="verbosity", action="store_const", const=logging.DEBUG )
-    p.add_argument( "-c", "--command", dest="command", action="store" )
-    p.add_argument( "-w", "--weaver", dest="weaver", action="store" )
-    p.add_argument( "-x", "--except", dest="skip", action="store", choices=('w','t') )
-    p.add_argument( "-p", "--permit", dest="permit", action="store" )
-    p.add_argument( "-r", "--reference", dest="reference", action="store", choices=('t', 's') )
-    p.add_argument( "-n", "--linenumbers", dest="tangler_line_numbers", action="store_true" )
-    p.add_argument( "files", nargs='+' )
-    config= p.parse_args( namespace=self.defaults )
-    self.expand( config )
-    return config
-
-def expand( self, config ):
-    """Translate the argument values from simple text to useful objects.
-    Weaver. Tangler. WebReader.
-    """
-    if config.reference == 't':
-        config.reference_style = TransitiveReference()
-    elif config.reference == 's':
-        config.reference_style = SimpleReference()
-    else:
-        raise Error( "Improper configuration" )
-
-    try:
-        weaver_class= weavers[config.weaver.lower()]
-    except KeyError:
-        module_name, _, class_name = config.weaver.partition('.')
-        weaver_module = __import__(module_name)
-        weaver_class = weaver_module.__dict__[class_name]
-        if not issubclass(weaver_class, Weaver):
-            raise TypeError( "{0!r} not a subclass of Weaver".format(weaver_class) )
-    config.theWeaver= weaver_class()
-
-    config.theTangler= TanglerMake()
-
-    if config.permit:
-        # save permitted errors, usual case is ``-pi`` to permit ``@i`` include errors
-        config.permitList= [ '{!s}{!s}'.format( config.command, c ) for c in config.permit ]
-    else:
-        config.permitList= []
-
-    config.webReader= WebReader()
-
-    return config
-
- -
-

Application parse command line (162). Used by: Application Class... (159)

-
-

The process() function uses the current Application settings -to process each file as follows:

-
    -
  1. Create a new WebReader for the Application, providing -the parameters required to process the input file.
  2. -
  3. Create a Web instance, w -and set the Web's sourceFileName from the WebReader's fileName.
  4. -
  5. Perform the given command, typically a ActionSequence, -which does some combination of load, tangle the output files and -weave the final document in the target language; if -necessary, examine the Web to determine the documentation language.
  6. -
  7. Print a performance summary line that shows lines processed per second.
  8. -
-

In the event of failure in any of the major processing steps, -a summary message is produced, to clarify the state of -the output files, and the exception is reraised. -The re-raising is done so that all exceptions are handled by the -outermost main program.

-

Application class process all files (163) =

-
-def process( self, config ):
-    root= logging.getLogger()
-    root.setLevel( config.verbosity )
-    self.logger.debug( "Setting root log level to {!r}".format(
-        logging.getLevelName(root.getEffectiveLevel()) ) )
-
-    if config.command:
-        self.logger.debug( "Command character {!r}".format(config.command) )
-
-    if config.skip:
-        if config.skip.lower().startswith('w'): # not weaving == tangling
-            self.theAction= self.doTangle
-        elif config.skip.lower().startswith('t'): # not tangling == weaving
-            self.theAction= self.doWeave
-        else:
-            raise Exception( "Unknown -x option {!r}".format(config.skip) )
-
-    self.logger.info( "Weaver {!s}".format(config.theWeaver) )
-
-    for f in config.files:
-        w= Web() # New, empty web to load and process.
-        self.logger.info( "{!s} {!r}".format(self.theAction.name, f) )
-        config.webFileName= f
-        self.theAction.web= w
-        self.theAction.options= config
-        self.theAction()
-        self.logger.info( self.theAction.summary() )
-
- -
-

Application class process all files (163). Used by: Application Class... (159)

-
-
-
-

Logging Setup

-

We'll create a logging context manager. This allows us to wrap the main() -function in an explicit with statement that assures that logging is -configured and cleaned up politely.

-

Imports (164) +=

-
-import logging
-import logging.config
-
- -
-

Imports (164). Used by: pyweb.py (153)

-
-

This has two configuration approaches. If a positional argument is given, -that dictionary is used for logging.config.dictConfig. Otherwise, -keyword arguments are provided to logging.basicConfig.

-

A subclass might properly load a dictionary -encoded in YAML and use that with logging.config.dictConfig.

-

Logging Setup (165) =

-
-class Logger:
-    def __init__( self, dict_config=None, **kw_config ):
-        self.dict_config= dict_config
-        self.kw_config= kw_config
-    def __enter__( self ):
-        if self.dict_config:
-            logging.config.dictConfig( self.dict_config )
-        else:
-            logging.basicConfig( **self.kw_config )
-        return self
-    def __exit__( self, *args ):
-        logging.shutdown()
-        return False
-
- -
-

Logging Setup (165). Used by: pyweb.py (153)

-
-

Here's a sample logging setup. This creates a simple console handler and -a formatter that matches the basicConfig formatter.

-

It defines the root logger plus two overrides for class loggers that might be -used to gather additional information.

-

Logging Setup (166) +=

-
-log_config= dict(
-    version= 1,
-    disable_existing_loggers= False, # Allow pre-existing loggers to work.
-    handlers= {
-        'console': {
-            'class': 'logging.StreamHandler',
-            'stream': 'ext://sys.stderr',
-            'formatter': 'basic',
-        },
-    },
-    formatters = {
-        'basic': {
-            'format': "{levelname}:{name}:{message}",
-            'style': "{",
-        }
-    },
-
-    root= { 'handlers': ['console'], 'level': logging.INFO, },
-
-    #For specific debugging support...
-    loggers= {
-    #    'RST': { 'level': logging.DEBUG },
-    #    'TanglerMake': { 'level': logging.DEBUG },
-    #    'WebReader': { 'level': logging.DEBUG },
-    },
-)
-
- -
-

Logging Setup (166). Used by: pyweb.py (153)

-
-

This seems a bit verbose; a separate configuration file might be better.

-

Also, we might want a decorator to define loggers consistently for each class.

-
-
-

The Main Function

-

The top-level interface is the main() function. -This function creates an Application instance.

-

The Application object parses the command-line arguments. -Then the Application object does the requested processing. -This two-step process allows for some dependency injection to customize argument processing.

-

We might also want to parse a logging configuration file, as well -as a weaver template configuration file.

-

Interface Functions (167) =

-
-def main():
-    a= Application()
-    config= a.parseArgs()
-    a.process(config)
-
-if __name__ == "__main__":
-    with Logger( log_config ):
-        main( )
-
- -
-

Interface Functions (167). Used by: pyweb.py (153)

-
-

This can be extended by doing something like the following.

-
    -
  1. Subclass Weaver create a subclass with different templates.
  2. -
  3. Update the pyweb.weavers dictionary.
  4. -
  5. Call pyweb.main() to run the existing -main program with extra classes available to it.
  6. -
-
-import pyweb
-class MyWeaver( HTML ):
-   Any template changes
-
-pyweb.weavers['myweaver']= MyWeaver()
-pyweb.main()
-
-

This will create a variant on pyWeb that will handle a different -weaver via the command-line option -w myweaver.

- -
-
-
-

Unit Tests

-

The test directory includes pyweb_test.w, which will create a -complete test suite.

-

This source will weaves a pyweb_test.html file. See file:test/pyweb_test.html

-

This source will tangle several test modules: test.py, test_tangler.py, test_weaver.py, -test_loader.py and test_unit.py. Running the test.py module will include and -execute all 78 tests.

-

Here's a script that works out well for running this without disturbing the development -environment. The PYTHONPATH setting is essential to support importing pyweb.

-
-cd test
-python ../pyweb.py pyweb_test.w
-PYTHONPATH=.. python test.py
-
-

Note that the last line really does set an environment variable and run -a program on a single line.

- -
-
-

Additional Files

-

Two aditional scripts, tangle.py and weave.py, are provided as examples -which an be customized.

-

The README and setup.py files are also an important part of the -distribution.

-

The .CSS file and .conf file for RST production are also provided here.

-
-

tangle.py Script

-

This script shows a simple version of Tangling. This has a permitted -error for '@i' commands to allow an include file (for example test results) -to be omitted from the tangle operation.

-

Note the general flow of this top-level script.

-
    -
  1. Create the logging context.
  2. -
  3. Create the options. This hard-coded object is a stand-in for -parsing command-line options.
  4. -
  5. Create the web object.
  6. -
  7. For each action (LoadAction and TangleAction in this example) -Set the web, set the options, execute the callable action, and write -a summary.
  8. -
-

tangle.py (168) =

-
-#!/usr/bin/env python3
-"""Sample tangle.py script."""
-import pyweb
-import logging
-import argparse
-
-with pyweb.Logger( pyweb.log_config ):
-    logger= logging.getLogger(__file__)
-
-    options = argparse.Namespace(
-            webFileName= "pyweb.w",
-            verbosity= logging.INFO,
-            command= '@',
-            permitList= ['@i'],
-            tangler_line_numbers= False,
-            reference_style = pyweb.SimpleReference(),
-            theTangler= pyweb.TanglerMake(),
-            webReader= pyweb.WebReader(),
-            )
-
-    w= pyweb.Web()
-
-    for action in LoadAction(), TangleAction():
-            action.web= w
-            action.options= options
-            action()
-            logger.info( action.summary() )
-
- -
-

tangle.py (168).

-
-
-
-

weave.py Script

-

This script shows a simple version of Weaving. This shows how -to define a customized set of templates for a different markup language.

-

A customized weaver generally has three parts.

-

weave.py (169) =

-
-→weave.py overheads for correct operation of a script (170)
-→weave.py custom weaver definition to customize the Weaver being used (171)
-→weaver.py processing: load and weave the document (172)
-
- -
-

weave.py (169).

-
-

weave.py overheads for correct operation of a script (170) =

-
-#!/usr/bin/env python3
-"""Sample weave.py script."""
-import pyweb
-import logging
-import argparse
-import string
-
- -
-

weave.py overheads for correct operation of a script (170). Used by: weave.py (169)

-
-

weave.py custom weaver definition to customize the Weaver being used (171) =

-
-class MyHTML( pyweb.HTML ):
-    """HTML formatting templates."""
-    extension= ".html"
-
-    cb_template= string.Template("""<a name="pyweb${seq}"></a>
-    <!--line number ${lineNumber}-->
-    <p><em>${fullName}</em> (${seq})&nbsp;${concat}</p>
-    <code><pre>\n""")
-
-    ce_template= string.Template("""
-    </pre></code>
-    <p>&loz; <em>${fullName}</em> (${seq}).
-    ${references}
-    </p>\n""")
-
-    fb_template= string.Template("""<a name="pyweb${seq}"></a>
-    <!--line number ${lineNumber}-->
-    <p>``${fullName}`` (${seq})&nbsp;${concat}</p>
-    <code><pre>\n""") # Prevent indent
-
-    fe_template= string.Template( """</pre></code>
-    <p>&loz; ``${fullName}`` (${seq}).
-    ${references}
-    </p>\n""")
-
-    ref_item_template = string.Template(
-    '<a href="#pyweb${seq}"><em>${fullName}</em>&nbsp;(${seq})</a>'
-    )
-
-    ref_template = string.Template( '  Used by ${refList}.'  )
-
-    refto_name_template = string.Template(
-    '<a href="#pyweb${seq}">&rarr;<em>${fullName}</em>&nbsp;(${seq})</a>'
-    )
-    refto_seq_template = string.Template( '<a href="#pyweb${seq}">(${seq})</a>' )
-
-    xref_head_template = string.Template( "<dl>\n" )
-    xref_foot_template = string.Template( "</dl>\n" )
-    xref_item_template = string.Template( "<dt>${fullName}</dt><dd>${refList}</dd>\n" )
-
-    name_def_template = string.Template( '<a href="#pyweb${seq}"><b>&bull;${seq}</b></a>' )
-    name_ref_template = string.Template( '<a href="#pyweb${seq}">${seq}</a>' )
-
- -
-

weave.py custom weaver definition to customize the Weaver being used (171). Used by: weave.py (169)

-
-

weaver.py processing: load and weave the document (172) =

-
-with pyweb.Logger( pyweb.log_config ):
-    logger= logging.getLogger(__file__)
-
-    options = argparse.Namespace(
-            webFileName= "pyweb.w",
-            verbosity= logging.INFO,
-            command= '@',
-            theWeaver= MyHTML(),
-            permitList= [],
-            tangler_line_numbers= False,
-            reference_style = pyweb.SimpleReference(),
-            theTangler= pyweb.TanglerMake(),
-            webReader= pyweb.WebReader(),
-            )
-
-    w= pyweb.Web()
-
-    for action in LoadAction(), WeaveAction():
-            action.web= w
-            action.options= options
-            action()
-            logger.info( action.summary() )
-
- -
-

weaver.py processing: load and weave the document (172). Used by: weave.py (169)

-
-
-
-

The setup.py and MANIFEST.in files

-

In order to support a pleasant installation, the setup.py file is helpful.

-

setup.py (173) =

-
-#!/usr/bin/env python3
-"""Setup for pyWeb."""
-
-from distutils.core import setup
-
-setup(name='pyweb',
-      version='2.3.2',
-      description='pyWeb 2.3: Yet Another Literate Programming Tool',
-      author='S. Lott',
-      author_email='s_lott@yahoo.com',
-      url='http://slott-softwarearchitect.blogspot.com/',
-      py_modules=['pyweb'],
-      classifiers=[
-      'Intended Audience :: Developers',
-      'Topic :: Documentation',
-      'Topic :: Software Development :: Documentation',
-      'Topic :: Text Processing :: Markup',
-      ]
-   )
-
- -
-

setup.py (173).

-
-

In order build a source distribution kit the python3 setup.py sdist requires a -MANIFEST. We can either list all files or provide a MANIFEST.in -that specifies additional rules. -We use a simple inclusion to augment the default manifest rules.

-

MANIFEST.in (174) =

-
-include *.w *.css *.html *.conf *.rst
-include test/*.w test/*.css test/*.html test/*.conf test/*.py
-include jedit/*.xml
-
- -
-

MANIFEST.in (174).

-
-
-
-

The README file

-

Here's the README file.

-

README (175) =

-
-pyWeb 2.3: In Python, Yet Another Literate Programming Tool
-
-Literate programming is an attempt to reconcile the opposing needs
-of clear presentation to people with the technical issues of
-creating code that will work with our current set of tools.
-
-Presentation to people requires extensive and sophisticated typesetting
-techniques.  Further, the "narrative arc" of a presentation may not
-follow the source code as layed out for the compiler.
-
-pyWeb is a literate programming tool that combines the actions
-of weaving a document with tangling source files.
-It is independent of any particular document markup or source language.
-Is uses a simple set of markup tags to define chunks of code and
-documentation.
-
-The pyweb.w file is the source for the various pyweb module and script files, plus
-the pyweb.html file.  The various source code files are created by applying a
-tangle operation to the .w file.  The final documentation is created by
-applying a weave operation to the .w file.
-
-Installation
--------------
-
-::
-
-    python3 setup.py install
-
-This will install the pyweb module.
-
-Document production
---------------------
-
-The supplied documentation uses RST markup and requires docutils.
-
-::
-
-    python3 -m pyweb pyweb.w
-    rst2html.py pyweb.rst pyweb.html
-
-Authoring
----------
-
-The pyweb document describes the simple markup used to define code chunks
-and assemble those code chunks into a coherent document as well as working code.
-
-If you're a JEdit user, the ``jedit`` directory can be used
-to configure syntax highlighting that includes PyWeb and RST.
-
-Operation
----------
-
-You can then run pyweb with
-
-::
-
-    python3 -m pyweb pyweb.w
-
-This will create the various output files from the source .w file.
-
--   ``pyweb.html`` is the final woven document.
-
--   ``pyweb.py``, ``tangle.py``, ``weave.py``, ``README``, ``setup.py`` and ``MANIFEST.in``
-    are tangled output files.
-
-Testing
--------
-
-The test directory includes ``pyweb_test.w``, which will create a
-complete test suite.
-
-This weaves a ``pyweb_test.html`` file.
-
-This tangles several test modules:  ``test.py``, ``test_tangler.py``, ``test_weaver.py``,
-``test_loader.py`` and ``test_unit.py``.  Running the ``test.py`` module will include and
-execute all tests.
-
-::
-
-    cd test
-    python3 -m pyweb pyweb_test.w
-    PYTHONPATH=.. python3 test.py
-    rst2html.py pyweb_test.rst pyweb_test.html
-
- -
-

README (175).

-
-
-
-

The CSS Files

-

To get the RST to look good, there are two additional files.

-

docutils.conf defines the CSS files to use. -The default CSS file (stylesheet-path) may need to be customized for your -installation of docutils.

-

docutils.conf (176) =

-
-# docutils.conf
-
-[html4css1 writer]
-stylesheet-path: /Library/Frameworks/Python.framework/Versions/3.3/lib/python3.3/site-packages/docutils-0.11-py3.3.egg/docutils/writers/html4css1/html4css1.css,
-    page-layout.css
-syntax-highlight: long
-
- -
-

docutils.conf (176).

-
-

page-layout.css This tweaks one CSS to be sure that -the resulting HTML pages are easier to read.

-

page-layout.css (177) =

-
-/* Page layout tweaks */
-div.document { width: 7in; }
-.small { font-size: smaller; }
-.code
-{
-    color: #101080;
-    display: block;
-    border-color: black;
-    border-width: thin;
-    border-style: solid;
-    background-color: #E0FFFF;
-    /*#99FFFF*/
-    padding: 0 0 0 1%;
-    margin: 0 6% 0 6%;
-    text-align: left;
-    font-size: smaller;
-}
-
- -
-

page-layout.css (177).

-
- -
-
-
-

JEdit Configuration

-

Here's the pyweb.xml file that you'll need to configure -JEdit so that it properly highlights your PyWeb commands.

-

We'll define the overall properties plus two sets of rules.

-

jedit/pyweb.xml (178) =

-
-<?xml version="1.0"?>
-<!DOCTYPE MODE SYSTEM "xmode.dtd">
-
-<MODE>
-    →props for JEdit mode (179)
-    →rules for JEdit PyWeb and RST (180)
-    →rules for JEdit PyWeb XML-Like Constructs (181)
-</MODE>
-
- -
-

jedit/pyweb.xml (178).

-
-

Here are some properties to define RST constructs to JEdit

-

props for JEdit mode (179) =

-
-<PROPS>
-    <PROPERTY NAME="lineComment" VALUE=".. "/>
-    <!-- indent after literal blocks and directives -->
-    <PROPERTY NAME="indentNextLines" VALUE=".*::$"/>
-    <!--
-    <PROPERTY NAME="commentStart" VALUE="@{" />
-    <PROPERTY NAME="commentEnd" VALUE="@}" />
-    -->
-</PROPS>
-
- -
-

props for JEdit mode (179). Used by: jedit/pyweb.xml (178)

-
-

Here are some rules to define PyWeb and RST constructs to JEdit.

-

rules for JEdit PyWeb and RST (180) =

-
-<RULES IGNORE_CASE="FALSE" HIGHLIGHT_DIGITS="FALSE">
-
-    <!-- targets -->
-    <EOL_SPAN AT_LINE_START="TRUE" TYPE="KEYWORD3">__</EOL_SPAN>
-    <EOL_SPAN AT_LINE_START="TRUE" TYPE="KEYWORD3">.. _</EOL_SPAN>
-
-    <!-- section titles -->
-    <SEQ_REGEXP HASH_CHAR="===" TYPE="LABEL">={3,}</SEQ_REGEXP>
-    <SEQ_REGEXP HASH_CHAR="---" TYPE="LABEL">-{3,}</SEQ_REGEXP>
-    <SEQ_REGEXP HASH_CHAR="~~~" TYPE="LABEL">~{3,}</SEQ_REGEXP>
-    <SEQ_REGEXP HASH_CHAR="###" TYPE="LABEL">#{3,}</SEQ_REGEXP>
-    <SEQ_REGEXP HASH_CHAR='"""' TYPE="LABEL">"{3,}</SEQ_REGEXP>
-    <SEQ_REGEXP HASH_CHAR="^^^" TYPE="LABEL">\^{3,}</SEQ_REGEXP>
-    <SEQ_REGEXP HASH_CHAR="+++" TYPE="LABEL">\+{3,}</SEQ_REGEXP>
-    <SEQ_REGEXP HASH_CHAR="***" TYPE="LABEL">\*{3,}</SEQ_REGEXP>
-
-    <!-- replacement -->
-    <SEQ_REGEXP
-        HASH_CHAR=".."
-        AT_LINE_START="TRUE"
-        TYPE="LITERAL3"
-    >\.\.\s\|[^|]+\|</SEQ_REGEXP>
-
-    <!-- substitution -->
-    <SEQ_REGEXP
-        HASH_CHAR="|"
-        AT_LINE_START="FALSE"
-        TYPE="LITERAL4"
-    >\|[^|]+\|</SEQ_REGEXP>
-
-    <!-- directives: .. name:: -->
-    <SEQ_REGEXP
-        HASH_CHAR=".."
-        AT_LINE_START="TRUE"
-        TYPE="LITERAL2"
-    >\.\.\s[A-z][A-z0-9-_]+::</SEQ_REGEXP>
-
-    <!-- strong emphasis: **...** -->
-    <SEQ_REGEXP
-        HASH_CHAR="**"
-        AT_LINE_START="FALSE"
-        TYPE="KEYWORD2"
-    >\*\*[^*]+\*\*</SEQ_REGEXP>
-
-    <!-- emphasis: *...* -->
-    <SEQ_REGEXP
-        HASH_CHAR="*"
-        AT_LINE_START="FALSE"
-        TYPE="KEYWORD4"
-    >\*[^\s*][^*]*\*</SEQ_REGEXP>
-
-    <!-- comments -->
-    <EOL_SPAN AT_LINE_START="TRUE" TYPE="COMMENT1">.. </EOL_SPAN>
-
-    <!-- links: `...`_ or `...`__ -->
-    <SEQ_REGEXP
-        HASH_CHAR="`"
-        TYPE="LABEL"
-    >`[A-z0-9]+[^`]+`_{1,2}</SEQ_REGEXP>
-
-    <!-- footnote reference: [0]_ -->
-    <SEQ_REGEXP
-        HASH_CHAR="["
-        TYPE="LABEL"
-    >\[[0-9]+\]_</SEQ_REGEXP>
-
-    <!-- footnote reference: [#]_ or [#foo]_ -->
-    <SEQ_REGEXP
-        HASH_CHAR="[#"
-        TYPE="LABEL"
-    >\[#[A-z0-9_]*\]_</SEQ_REGEXP>
-
-    <!-- footnote reference: [*]_ -->
-    <SEQ TYPE="LABEL">[*]_</SEQ>
-
-    <!-- citation reference: [foo]_ -->
-    <SEQ_REGEXP
-        HASH_CHAR="["
-        TYPE="LABEL"
-    >\[[A-z][A-z0-9_-]*\]_</SEQ_REGEXP>
-
-    <!-- inline literal: ``...``-->
-    <!--<SEQ_REGEXP
-        HASH_CHAR="``"
-        TYPE="LITERAL1"
-    >``[^`]+``</SEQ_REGEXP>-->
-    <SPAN TYPE="LITERAL1" ESCAPE="\">
-        <BEGIN>``</BEGIN>
-        <END>``</END>
-    </SPAN>
-
-    <!-- interpreted text: `...` -->
-    <!--
-    <SEQ_REGEXP
-        HASH_CHAR="`"
-        TYPE="KEYWORD1"
-    >`[^`]+`</SEQ_REGEXP>
-
-    -->
-    <EOL_SPAN TYPE="COMMENT1">@d</EOL_SPAN>
-    <EOL_SPAN TYPE="COMMENT1">@o</EOL_SPAN>
-
-    <SPAN TYPE="COMMENT1" DELEGATE="CODE">
-        <BEGIN>@{</BEGIN>
-        <END>@}</END>
-    </SPAN>
-
-    <SPAN TYPE="KEYWORD1">
-        <BEGIN>`</BEGIN>
-        <END>`</END>
-    </SPAN>
-
-    <SEQ_REGEXP HASH_CHAR="```" TYPE="LABEL">`{3,}</SEQ_REGEXP>
-
-    <!-- :field list: -->
-    <SEQ_REGEXP
-        HASH_CHAR=":"
-        TYPE="KEYWORD1"
-    >:[A-z][A-z0-9  =\s\t_]*:</SEQ_REGEXP>
-
-    <!-- table -->
-    <SEQ_REGEXP
-        HASH_CHAR="+-"
-        TYPE="LABEL"
-    >\+-[+-]+</SEQ_REGEXP>
-    <SEQ_REGEXP
-        HASH_CHAR="+?"
-        TYPE="LABEL"
-    >\+=[+=]+</SEQ_REGEXP>
-
-</RULES>
-
- -
-

rules for JEdit PyWeb and RST (180). Used by: jedit/pyweb.xml (178)

-
-

Here are some additional rules to define PyWeb constructs to JEdit -that look like XML.

-

rules for JEdit PyWeb XML-Like Constructs (181) =

-
-<RULES SET="CODE" DEFAULT="KEYWORD1">
-    <SPAN TYPE="MARKUP">
-        <BEGIN>@&lt;</BEGIN>
-        <END>@&gt;</END>
-    </SPAN>
-</RULES>
-
- -
-

rules for JEdit PyWeb XML-Like Constructs (181). Used by: jedit/pyweb.xml (178)

-
-

Additionally, you'll want to update the JEdit catalog.

-
-<?xml version="1.0"?>
-<!DOCTYPE MODES SYSTEM "catalog.dtd">
-<MODES>
-
-<!-- Add lines like the following, one for each edit mode you add: -->
-<MODE NAME="pyweb" FILE="pyweb.xml" FILE_NAME_GLOB="*.w"/>
-
-</MODES>
-
- - -
-
-

To Do

-
    -
  1. Fix name definition order. There's no good reason why a full name should -be first and elided names defined later.

    -
  2. -
  3. Silence the logging during testing.

    -
  4. -
  5. Add a JSON-based configuration file to configure templates.

    -
      -
    • See the weave.py example. -This removes any need for a weaver command-line option; its defined within the source. -Also, setting the command character can be done in this configuration, too.

      -
    • -
    • An alternative is to get markup templates from a "header" section in the .w file.

      -

      To support reuse over multiple projects, a header could be included with @i. -The downside is that we have a lot of variable = value syntax that makes it -more like a properties file than a .w syntax file. It seems needless to invent -a lot of new syntax just for configuration.

      -
    • -
    -
  6. -
  7. JSON-based logging configuration file would be helpful. -Should be separate from template configuration.

    -
  8. -
  9. We might want to decompose the impl.w file: it's huge.

    -
  10. -
  11. We might want to interleave code and test into a document that presents both -side-by-side. They get routed to different output files.

    -
  12. -
  13. Add a @h "header goes here" command to allow weaving any pyWeb required addons to -a LaTeX header, HTML header or RST header. -These are extra ..  include::, \\usepackage{fancyvrb} or maybe an HTML CSS reference -that come from pyWeb and need to be folded into otherwise boilerplate documents.

    -
  14. -
  15. Update the -indent option to accept a numeric argument with the -specific indentation value. This becomes a kind of "noindent" with a given -value. The -noindent would then be the same as -indent 0.

    -
  16. -
  17. Offer a basic XHTML template that uses CDATA sections instead of quoting. -Does require the standard quoting for the CDATA end tag.

    -
  18. -
  19. The createUsedBy() method can be done incrementally by -accumulating a list of forward references to chunks; as each -new chunk is added, any references to the chunk are removed from -the forward references list, and a call is made to the Web's -setUsage method. References backward to already existing chunks -are easily resolved with a simple lookup.

    -
  20. -
  21. Note that the overall Web is a bit like a NamedChunk that contains Chunks. -This similarity could be factored out. -While this will create a more proper Composition pattern implementation, it -leads to the question of why nest @d or @o chunks in the first place?

    -
  22. -
-
-

Other Thoughts

-

There are two possible projects that might prove useful.

-
    -
  • Jinja2 for better templates.
  • -
  • pyYAML for slightly cleaner encoding of logging configuration -or other configuration.
  • -
-

There are advantages and disadvantages to depending on other projects. -The disadvantage is a (very low, but still present) barrier to adoption. -The advantage of adding these two projects might be some simplification.

- -
-
-
-

Change Log

-

Changes for 2.3.2.

-
    -
  • Fix all {:s} format strings to be {!s}.
  • -
-

Changes for 2.3.1.

-
    -
  • Cleanup some stray comment errors.
  • -
  • Revise the documentation structure and organization.
  • -
  • Tweak the error messages.
  • -
-

Changes for 2.3.

-
    -
  • Changed to Python 3.3 -- Fixed except, raise and %.
  • -
  • Removed doWrite() and simplified doOpen() and doClose().
  • -
  • Cleaned up RST output to be much nicer.
  • -
  • Change the baseline pyweb.w file to be RST instead of HTML. -docutils required to produce HTML from the woven output.
  • -
  • Removed the unconstrained eval() function. Provided a slim set of globals. -os is really just os.path. -Any os.getcwd() can be changed to os.path.realpath('.'). -time was removed and replaced with datetime. -Any time.asctime() must be datetime.datetime.now().ctime().
  • -
  • Resolved a small dispute between weaveReferenceTo() (wrong) and tangle() (right). -for NamedChunks. The issue was one of failure to understand the differences -between weaving -- where indentation is localized -- and tangling -- where indentation -must be tracked globally. Root cause was a huge problem in codeBlock() which didn't -really weave properly at all.
  • -
  • Fix the tokenizer and parsing. Stop using a complex tokenizer and use a simpler -iterator over the tokens with StopIteration exception handling.
  • -
  • Replace optparse with argparse.
  • -
  • Get rid of the global logger variable.
  • -
  • Remove the filename as part of Web() initial creation. -A basename comes from the initial .w file loaded by the WebReader.
  • -
  • Fix the Action class hierarchy so that composite actions are simpler.
  • -
  • Change references to return Chunk objects, not (name,sequence) pairs.
  • -
  • Make the ref list separator in Weaver reference summary... a proper template -feature, not a hidden punctuation mark in the code.
  • -
  • Configure Web.reference_style properly so that simple or transitive references -can be included as a command-line option. The default is Simple. -Add the -r option so that -rt includes transitive references.
  • -
  • Reduce the "hard-coded" punctuation. For example, the ", " in -@d Web weave... weaveChunk(). This was moved into a template.
  • -
  • Add an __enter__() and __exit__() to make an Emitter -into a proper Context Manager that can be used with a with statement.
  • -
  • Add the -n option to include tangler line numbers if the @o includes -the comment characters.
  • -
  • Cleanup the TanglerMake unit tests to remove the sleep() -used to assure that the timestamps really are different.
  • -
  • Cleanup the syntax for adding a comment template to @o. Use -start and -end -before the filename.
  • -
  • Cleanup the syntax for noindent named chunks. Use -noindent before the chunk name. -This creates a distinct NamedChunk_Noindent instance that handles indentation -differently from other Chunk subclasses.
  • -
  • Cleanup the TangleAction summary.
  • -
  • Clean up the error messages. Raising an exception seems -heavy-handed and confusing. Count errors instead.
  • -
-

Changes since version 1.4

-
    -
  • Removed home-brewed logger.
  • -
  • Replaced getopt with optparse.
  • -
  • Replaced LaTeX markup.
  • -
  • Corrected significant problems in cross-reference resolution.
  • -
  • Replaced all HTML and LaTeX-specific features with a much simpler template -engine which applies a template to a Chunk. The Templates are separate -configuration items. The big issue with templates are conditional processing -and the use of loops to handle multiple references in a transitive closure. -While it's nice to depend on Jinja2, it's also nice to be totally stand-alone. -Sigh. Choices include the no-logic string.Template in the standard library -or the Templite+ Recipe 576663.
  • -
  • Looked at SCons API. Renamed "Operation" to "Action"; renamed "perform" to "__call__". -Consider having "__call__" which does logging, then call "execute".
  • -
  • Eliminated the EmitterFactory; replace this with simple injection of -the proper template configuration.
  • -
  • Removed the @O command; it was essentially a variant template for LaTeX.
  • -
  • Disentangled indentation and quoting in the codeBlock. -Indentation rules vary between Tangling and Weaving. -Quoting is unique to a woven codeBlock. Fix referenceTo() to write -indented without code quoting.
  • -
  • Offer a basic RST template. -Note that colorizing may be easier to handle with an RST template. -The weaving markup template degenerates -to ..   parsed-literal:: and indent. By doing this, -the RST output from pyWeb can be run through DocUtils rst2html.py -or perhaps Sphix to create final HTML. The hard part is the indent.
  • -
  • Tweaked (but didn't fix) ReferenceCommand tangle and all setIndent/clrIndent operations. -Only a ReferenceCommand actually cares about indentation. And that indentation -is totally based on the "context" plus the text in the Command immediate in front -of the ReferenceCommand.
  • -
-
-
-

Indices

-
-

Files

- --- - - - - - - - - - - - - - - - - - - - - - -
MANIFEST.in:→(174)
README:→(175)
docutils.conf:→(176)
jedit/pyweb.xml:
 →(178)
page-layout.css:
 →(177)
pyweb.py:→(153)
setup.py:→(173)
tangle.py:→(168)
weave.py:→(169)
-
-
-

Macros

- --- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Action call method actually does the real work:
 →(138)
Action class hierarchy - used to describe basic actions of the application:
 →(136)
Action final summary of what was done:
 →(139)
Action superclass has common features of all actions:
 →(137)
ActionSequence append adds a new action to the sequence:
 →(142)
ActionSequence call method delegates the sequence of ations:
 →(141)
ActionSequence subclass that holds a sequence of other actions:
 →(140)
ActionSequence summary summarizes each step:
 →(143)
Application Class:
 →(159) →(160)
Application class process all files:
 →(163)
Application default options:
 →(161)
Application parse command line:
 →(162)
Base Class Definitions:
 →(1)
Chunk add to the web:
 →(55)
Chunk append a command:
 →(53)
Chunk append text:
 →(54)
Chunk class:→(52)
Chunk class hierarchy - used to describe input chunks:
 →(51)
Chunk examination:
 starts with, matches pattern: -→(57)
Chunk generate references from this Chunk:
 →(58)
Chunk indent adjustments:
 →(62)
Chunk references to this Chunk:
 →(59)
Chunk superclass make Content definition:
 →(56)
Chunk tangle this Chunk into a code file:
 →(61)
Chunk weave this Chunk into the documentation:
 →(60)
CodeCommand class to contain a program source code block:
 →(81)
Command analysis features:
 starts-with and Regular Expression search: -→(78)
Command class hierarchy - used to describe individual commands:
 →(76)
Command superclass:
 →(77)
Command tangle and weave functions:
 →(79)
Emitter class hierarchy - used to control output files:
 →(2)
Emitter core open, close and write:
 →(4)
Emitter doClose, to be overridden by subclasses:
 →(6)
Emitter doOpen, to be overridden by subclasses:
 →(5)
Emitter indent control:
 set, clear and reset: -→(10)
Emitter superclass:
 →(3)
Emitter write a block of code:
 →(7) →(8) →(9)
Error class - defines the errors raised:
 →(94)
FileXrefCommand class for an output file cross-reference:
 →(83)
HTML code chunk begin:
 →(33)
HTML code chunk end:
 →(34)
HTML output file begin:
 →(35)
HTML output file end:
 →(36)
HTML reference to a chunk:
 →(39)
HTML references summary at the end of a chunk:
 →(37)
HTML short references summary at the end of a chunk:
 →(42)
HTML simple cross reference markup:
 →(40)
HTML subclass of Weaver:
 →(31) →(32)
HTML write a line of code:
 →(38)
HTML write user id cross reference line:
 →(41)
Imports:→(11) →(47) →(96) →(124) →(131) →(133) →(154) →(158) →(164)
Interface Functions:
 →(167)
LaTeX code chunk begin:
 →(24)
LaTeX code chunk end:
 →(25)
LaTeX file output begin:
 →(26)
LaTeX file output end:
 →(27)
LaTeX reference to a chunk:
 →(30)
LaTeX references summary at the end of a chunk:
 →(28)
LaTeX subclass of Weaver:
 →(23)
LaTeX write a line of code:
 →(29)
LoadAction call method loads the input files:
 →(151)
LoadAction subclass loads the document web:
 →(150)
LoadAction summary provides lines read:
 →(152)
Logging Setup:→(165) →(166)
MacroXrefCommand class for a named chunk cross-reference:
 →(84)
NamedChunk add to the web:
 →(65)
NamedChunk class:
 →(63) →(68)
NamedChunk tangle into the source file:
 →(67)
NamedChunk user identifiers set and get:
 →(64)
NamedChunk weave into the documentation:
 →(66)
NamedDocumentChunk class:
 →(73)
NamedDocumentChunk tangle:
 →(75)
NamedDocumentChunk weave:
 →(74)
Option Parser class - locates optional values on commands:
 →(134) →(135)
OutputChunk add to the web:
 →(70)
OutputChunk class:
 →(69)
OutputChunk tangle:
 →(72)
OutputChunk weave:
 →(71)
Overheads:→(155) →(156) →(157)
RST subclass of Weaver:
 →(22)
Reference class hierarchy - strategies for references to a chunk:
 →(91) →(92) →(93)
ReferenceCommand class for chunk references:
 →(86)
ReferenceCommand refers to a chunk:
 →(88)
ReferenceCommand resolve a referenced chunk name:
 →(87)
ReferenceCommand tangle a referenced chunk:
 →(90)
ReferenceCommand weave a reference to a chunk:
 →(89)
TangleAction call method does tangling of the output files:
 →(148)
TangleAction subclass initiates the tangle action:
 →(147)
TangleAction summary method provides total lines tangled:
 →(149)
Tangler code chunk begin:
 →(45)
Tangler code chunk end:
 →(46)
Tangler doOpen, and doClose overrides:
 →(44)
Tangler subclass of Emitter to create source files with no markup:
 →(43)
TanglerMake doClose override, comparing temporary to original:
 →(50)
TanglerMake doOpen override, using a temporary file:
 →(49)
TanglerMake subclass which is make-sensitive:
 →(48)
TextCommand class to contain a document text block:
 →(80)
Tokenizer class - breaks input into tokens:
 →(132)
UserIdXrefCommand class for a user identifier cross-reference:
 →(85)
WeaveAction call method to pick the language:
 →(145)
WeaveAction subclass initiates the weave action:
 →(144)
WeaveAction summary of language choice:
 →(146)
Weaver code chunk begin-end:
 →(17)
Weaver cross reference output methods:
 →(20) →(21)
Weaver doOpen, doClose and addIndent overrides:
 →(13)
Weaver document chunk begin-end:
 →(15)
Weaver file chunk begin-end:
 →(18)
Weaver quoted characters:
 →(14)
Weaver reference command output:
 →(19)
Weaver reference summary, used by code chunk and file chunk:
 →(16)
Weaver subclass of Emitter to create documentation:
 →(12)
Web Chunk check reference counts are all one:
 →(105)
Web Chunk cross reference methods:
 →(104) →(106) →(107) →(108)
Web Chunk name resolution methods:
 →(102) →(103)
Web add a named macro chunk:
 →(100)
Web add an anonymous chunk:
 →(99)
Web add an output file definition chunk:
 →(101)
Web add full chunk names, ignoring abbreviated names:
 →(98)
Web class - describes the overall "web" of chunks:
 →(95)
Web construction methods used by Chunks and WebReader:
 →(97)
Web determination of the language from the first chunk:
 →(111)
Web tangle the output files:
 →(112)
Web weave the output document:
 →(113)
WebReader class - parses the input file, building the Web structure:
 →(114)
WebReader command literals:
 →(130)
WebReader handle a command string:
 →(115) →(127)
WebReader load the web:
 →(129)
WebReader location in the input stream:
 →(128)
XrefCommand superclass for all cross-reference commands:
 →(82)
add a reference command to the current chunk:
 →(123)
add an expression command to the current chunk:
 →(125)
assign user identifiers to the current chunk:
 →(122)
collect all user identifiers from a given map into ux:
 →(109)
double at-sign replacement, append this character to previous TextCommand:
 →(126)
find user identifier usage and update ux from the given map:
 →(110)
finish a chunk, start a new Chunk adding it to the web:
 →(120)
import another file:
 →(119)
major commands segment the input into separate Chunks:
 →(116)
minor commands add Commands to the current Chunk:
 →(121)
props for JEdit mode:
 →(179)
rules for JEdit PyWeb XML-Like Constructs:
 →(181)
rules for JEdit PyWeb and RST:
 →(180)
start a NamedChunk or NamedDocumentChunk, adding it to the web:
 →(118)
start an OutputChunk, adding it to the web:
 →(117)
weave.py custom weaver definition to customize the Weaver being used:
 →(171)
weave.py overheads for correct operation of a script:
 →(170)
weaver.py processing:
 load and weave the document: -→(172)
-
-
-

User Identifiers

- --- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Action:[137] 140 144 147 150
ActionSequence:[140] 161
Application:[159] 167
Chunk:[52] 58 63 90 95 104 119 120 123 129
CodeCommand:63 [81]
Command:53 [77] 80 82 86 163
Emitter:[3] 12 43
Error:58 61 67 75 82 90 [94] 100 102 103 113 118 119 125 135 145 148 151 162
FileXrefCommand:
 [83] 121
HTML:31 [32] 111 160 171
LaTeX:[23] 111 160
LoadAction:[150] 161 168 172
MacroXrefCommand:
 [84] 121
NamedChunk:[63] 68 69 73 118
NamedDocumentChunk:
 [73] 118
OutputChunk:[69] 117
ReferenceCommand:
 [86] 123
TangleAction:[147] 161 168
Tangler:3 [43] 48 162
TanglerMake:[48] 162 166 168 172
TextCommand:54 56 67 73 [80] 81
Tokenizer:129 [132]
UserIdXrefCommand:
 [85] 121
WeaveAction:[144] 161 172
Weaver:[12] 22 23 31 162 163
Web:45 55 65 70 [95] 151 163 168 172
WebReader:[114] 119 162 166 168 172
XrefCommand:[82] 83 84 85
__version__:125 [157]
_gatherUserId:[108]
_updateUserId:[108]
add:55 [99]
addDefName:[98] 100 123
addIndent:10 [13] 62 66
addNamed:65 [100]
addOutput:70 [101]
append:10 13 [53] 54 93 99 100 101 104 110 121 123 135 142
appendText:[54] 123 125 126 129
argparse:[158] 161 162 168 170 172
builtins:[124] 125
chunkXref:84 [107]
close:[4] 13 44 50
clrIndent:[10] 62 66 68
codeBegin:17 [33] 45 66 67
codeBlock:[7] 66 81
codeEnd:[17] 46 66 67
codeFinish:4 [9] 13
createUsedBy:[104] 151
datetime:125 [154]
doClose:4 6 13 44 [50]
doOpen:4 5 13 44 [49]
docBegin:[15] 60
docEnd:[15] 60
duration:[139] 146 149 152
expand:74 123 161 [162]
expect:117 118 123 125 [127]
fileBegin:[18] 71
fileEnd:18 [36] 71
fileXref:83 [107]
filecmp:[47] 50
formatXref:[82] 83 84
fullNameFor:66 71 87 98 [102] 103 104
genReferences:[58] 104
getUserIDRefs:57 [64] 109
getchunk:87 [103] 104 113
handleCommand:[115] 129
language:[111] 145 156 175
lineNumber:17 18 33 35 45 54 56 [57] 63 67 73 77 80 82 86 119 121 123 125 126 128 129 132 171
load:119 [129] 151 161 163
location:115 122 125 127 [128]
logging:3 77 91 95 114 137 159 161 162 163 [164] 165 166 168 170 172
logging.config:[164] 165
main:[167]
makeContent:54 56 63 [73]
multi_reference:
 105 [106]
no_definition:105 [106]
no_reference:105 [106]
open:[4] 13 44 112 113 125 129
os:44 49 50 113 125 [154]
parse:117 118 [129] 135
parseArgs:[162] 167
perform:[148]
platform:[124] 125
process:125 [163] 167
quote:[8] 81
quoted_chars:8 14 [29] 38
re:110 [131] 132 175
readdIndent:3 [10] 13
ref:28 58 [79] 88 99 100 101
referenceSep:[19] 113
referenceTo:19 20 [30] 66
references:16 17 18 19 25 32 34 36 [42] 52 58 105 122 156 161 171
resolve:67 [87] 88 89 90 103
searchForRE:57 78 [80] 110
setUserIDRefs:[64] 122
shlex:[133] 135
startswith:57 78 [80] 102 111 129 135 163
string:[11] 16 17 18 19 20 21 24 25 28 30 33 34 35 36 37 39 40 41 42 170 171
summary:139 [143] 146 149 152 163 168 172
sys:[124] 125 166
tangle:45 61 67 69 72 73 75 79 80 [81] 82 90 112 148 156 161 168 175
tempfile:[47] 49
time:138 139 [154]
types:12 125 [154]
usedBy:[88]
userNamesXref:85 [108]
weakref:[96] 99 100 101
weave:60 66 71 74 79 80 [81] 83 84 85 89 113 145 156 161 170 175
weaveChunk:89 [113]
weaveReferenceTo:
 [60] 66 74 113
weaveShortReferenceTo:
 [60] 66 74 113
webAdd:[55] 65 70 117 118 119 120 129
write:[4] 7 9 17 18 20 21 45 80 113
xrefDefLine:21 [41] 85
xrefFoot:20 [21] 82 85
xrefHead:20 [21] 82 85
xrefLine:20 [21] 82
-
-
-Created by /Users/slott/Documents/Projects/pyWeb-2.3/pyweb/pyweb.py at Thu Dec 4 18:45:16 2014.
-

Source pyweb.w modified Mon Mar 17 10:13:24 2014.

-
-

pyweb.__version__ '2.3.2'.

-

Working directory '/Users/slott/Documents/Projects/pyWeb-2.3/pyweb'.

-
-
-
-
- - diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..8eaeb4b --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,5 @@ + +docutils==0.18.1 +tox==3.25.0 +mypy==0.910 +pytest==7.1.2 diff --git a/setup.py b/setup.py index 44efb59..fcaf2e3 100644 --- a/setup.py +++ b/setup.py @@ -3,17 +3,17 @@ from distutils.core import setup -setup(name='pyweb', - version='3.0', - description='pyWeb 3.0: Yet Another Literate Programming Tool', +setup(name='py-web-tool', + version='3.1', + description='py-web-tool 3.1: Yet Another Literate Programming Tool', author='S. Lott', - author_email='s_lott@yahoo.com', + author_email='slott56@gmail.com', url='http://slott-softwarearchitect.blogspot.com/', - py_modules=['pyweb'], + py_modules=['src/pyweb'], classifiers=[ - 'Intended Audience :: Developers', - 'Topic :: Documentation', - 'Topic :: Software Development :: Documentation', - 'Topic :: Text Processing :: Markup', + 'Intended Audience :: Developers', + 'Topic :: Documentation', + 'Topic :: Software Development :: Documentation', + 'Topic :: Text Processing :: Markup', ] ) diff --git a/done.w b/src/done.w similarity index 85% rename from done.w rename to src/done.w index 08e9ad5..fa372d7 100644 --- a/done.w +++ b/src/done.w @@ -1,8 +1,34 @@ -.. pyweb/done.w +.. py-web-tool/src/done.w Change Log =========== +Changes for 3.1 + +- Change to Python 3.10 as the supported version. + +- Add type hints, f-strings, ``pathlib``, ``abc.ABC``. + +- Replace some complex ``elif`` blocks with ``match`` statements. + +- Use **pytest** as a test runner. + +- Add a ``Makefile``, ``pyproject.toml``, ``requirements.txt`` and ``requirements-dev.txt``. + +- Implement ``-o dir`` option to write output to a directory of choice, simplifying **tox** setup. + +- Add ``bootstrap`` directory with a snapshot of a previous working release to simplify development. + +- Add Test cases for ``weave.py`` and ``tangle.py`` + +- Replace hand-build mock classes with ``unittest.mock.Mock`` objects + +- Separate the projec into ``src``, ``tests``, ``examples``. Cleanup ``Makefile``, ``pyproject.toml``, etc. + +- Silence the ERROR-level logging during testing. + +- Clean up the examples + Changes for 3.0 - Move to GitHub diff --git a/impl.w b/src/impl.w similarity index 69% rename from impl.w rename to src/impl.w index 2ea409f..951446c 100644 --- a/impl.w +++ b/src/impl.w @@ -1,4 +1,4 @@ -.. pyweb/impl.w +.. py-web-tool/src/impl.w Implementation ============== @@ -85,23 +85,36 @@ fit elsewhere @d Base Class Definitions @{ + @ + @ + @ + @ + @ + @
+ +
+

Chunk base class for anonymous chunks of the file (53). Used by: Chunk class hierarchy... (52)

The append() method simply appends a Command instance to this chunk.

-

Chunk append a command (53) =

+

Chunk append a command (54) =

-def append( self, command ):
+def append(self, command: Command) -> None:
     """Add another Command to this chunk."""
-    self.commands.append( command )
-    command.chunk= self
+    self.commands.append(command)
+    command.chunk = self
 
-

Chunk append a command (53). Used by: Chunk class (52)

+

Chunk append a command (54). Used by: Chunk base class... (53)

The appendText() method appends a TextCommand to this chunk, or it concatenates it to the most recent TextCommand.

@@ -2949,52 +3282,48 @@

Chunk Superclass

any previous TextCommand, or new TextCommand will be built.

The reason for appending is that a TextCommand has an implicit indentation. The "@" cannot be a separate TextCommand because it will wind up indented.

-

Chunk append text (54) =

+

Chunk append text (55) =

-def appendText( self, text, lineNumber=0 ):
-    """Append a single character to the most recent TextCommand."""
-    try:
-        # Works for TextCommand, otherwise breaks
-        self.commands[-1].text += text
-    except IndexError as e:
-        # First command?  Then the list will have been empty.
-        self.commands.append( self.makeContent(text,lineNumber) )
-    except AttributeError as e:
-        # Not a TextCommand?  Then there won't be a text attribute.
-        self.commands.append( self.makeContent(text,lineNumber) )
+def appendText(self, text: str, lineNumber: int = 0) -> None:
+    """Append a string to the most recent TextCommand."""
+    match self.commands:
+        case [*Command, TextCommand()]:
+            self.commands[-1].text += text
+        case _:
+            self.commands.append(self.makeContent(text, lineNumber))
 
-

Chunk append text (54). Used by: Chunk class (52)

+

Chunk append text (55). Used by: Chunk base class... (53)

The webAdd() method adds this chunk to the given document web. Each subclass of the Chunk class must override this to be sure that the various Chunk subclasses are indexed properly. The Chunk class uses the add() method of the Web class to append an anonymous, unindexed chunk.

-

Chunk add to the web (55) =

+

Chunk add to the web (56) =

-def webAdd( self, web ):
+def webAdd(self, web: "Web") -> None:
     """Add self to a Web as anonymous chunk."""
-    web.add( self )
+    web.add(self)
 
-

Chunk add to the web (55). Used by: Chunk class (52)

+

Chunk add to the web (56). Used by: Chunk base class... (53)

This superclass creates a specific Command for a given piece of content. A subclass can override this to change the underlying assumptions of that Chunk. The generic chunk doesn't contain code, it contains text and can only be woven, never tangled. A Named Chunk using @{ and @} creates code. A Named Chunk using @[ and @] creates text.

-

Chunk superclass make Content definition (56) =

+

Chunk superclass make Content definition (57) =

-def makeContent( self, text, lineNumber=0 ):
-    return TextCommand( text, lineNumber )
+def makeContent(self, text: str, lineNumber: int = 0) -> Command:
+    return TextCommand(text, lineNumber)
 
-

Chunk superclass make Content definition (56). Used by: Chunk class (52)

+

Chunk superclass make Content definition (57). Used by: Chunk base class... (53)

The startsWith() method examines a the first Command instance this Chunk instance to see if it starts @@ -3010,30 +3339,43 @@

Chunk Superclass

The searchForRE() method examines each Command instance to see if it matches with the given regular expression. If so, this can be reported to the Web instance and accumulated as part of a cross reference for this Chunk.

-

Chunk examination: starts with, matches pattern (57) =

+

Imports (58) +=

+
+from typing import Pattern, Match, Optional, Any, Literal
+
+ +
+

Imports (58). Used by: pyweb.py (155)

+
+

Chunk examination: starts with, matches pattern (59) =

-def startswith( self, prefix ):
+def startswith(self, prefix: str) -> bool:
     """Examine the first command's starting text."""
-    return len(self.commands) >= 1 and self.commands[0].startswith( prefix )
+    return len(self.commands) >= 1 and self.commands[0].startswith(prefix)
 
-def searchForRE( self, rePat ):
+def searchForRE(self, rePat: Pattern[str]) -> Optional["Chunk"]:
     """Visit each command, applying the pattern."""
     for c in self.commands:
-        if c.searchForRE( rePat ):
+        if c.searchForRE(rePat):
             return self
     return None
 
 @property
-def lineNumber( self ):
+def lineNumber(self) -> int | None:
     """Return the first command's line number or None."""
     return self.commands[0].lineNumber if len(self.commands) >= 1 else None
 
-def getUserIDRefs( self ):
+def setUserIDRefs(self, text: str) -> None:
+    """Used by NamedChunk subclass."""
+    pass
+
+def getUserIDRefs(self) -> list[str]:
+    """Used by NamedChunk subclass."""
     return []
 
-

Chunk examination: starts with, matches pattern (57). Used by: Chunk class (52)

+

Chunk examination: starts with, matches pattern (59). Used by: Chunk base class... (53)

The chunk search in the searchForRE() method parallels weaving and tangling a Chunk. The operation is delegated to each Command instance within the Chunk instance.

@@ -3043,13 +3385,13 @@

Chunk Superclass

Chunk does not actually exist. If a reference Command does raise an error, we append this Chunk information and reraise the error with the additional context information.

-

Chunk generate references from this Chunk (58) =

+

Chunk generate references from this Chunk (60) =

-def genReferences( self, aWeb ):
+def genReferences(self, aWeb: "Web") -> Iterator[str]:
     """Generate references from this Chunk."""
     try:
         for t in self.commands:
-            ref= t.ref( aWeb )
+            ref = t.ref(aWeb)
             if ref is not None:
                 yield ref
     except Error as e:
@@ -3057,23 +3399,25 @@ 

Chunk Superclass

-

Chunk generate references from this Chunk (58). Used by: Chunk class (52)

+

Chunk generate references from this Chunk (60). Used by: Chunk base class... (53)

The list of references to a Chunk uses a Strategy plug-in to either generate a simple parent or a transitive closure of all parents.

Note that we need to get the Weaver.reference_style which is a configuration item. This is a Strategy showing how to compute the list of references. The Weaver pushed it into the Web so that it is available for each Chunk.

-

Chunk references to this Chunk (59) =

+

Chunk references to this Chunk (61) =

-def references_list( self, theWeaver ):
+def references(self, theWeaver: "Weaver") -> list[tuple[str, int]]:
     """Extract name, sequence from Chunks into a list."""
-    return [ (c.name, c.seq)
-        for c in theWeaver.reference_style.chunkReferencedBy( self ) ]
+    return [
+        (c.name, c.seq)
+        for c in theWeaver.reference_style.chunkReferencedBy(self)
+    ]
 
-

Chunk references to this Chunk (59). Used by: Chunk class (52)

+

Chunk references to this Chunk (61). Used by: Chunk base class... (53)

The weave() method weaves this chunk into the final document as follows:

    @@ -3086,56 +3430,56 @@

    Chunk Superclass

    Chunk does not actually exist. If a reference Command does raise an error, we append this Chunk information and reraise the error with the additional context information.

    -

    Chunk weave this Chunk into the documentation (60) =

    +

    Chunk weave this Chunk into the documentation (62) =

    -def weave( self, aWeb, aWeaver ):
    +def weave(self, aWeb: "Web", aWeaver: "Weaver") -> None:
         """Create the nicely formatted document from an anonymous chunk."""
    -    aWeaver.docBegin( self )
    +    aWeaver.docBegin(self)
         for cmd in self.commands:
    -        cmd.weave( aWeb, aWeaver )
    -    aWeaver.docEnd( self )
    -def weaveReferenceTo( self, aWeb, aWeaver ):
    +        cmd.weave(aWeb, aWeaver)
    +    aWeaver.docEnd(self)
    +def weaveReferenceTo(self, aWeb: "Web", aWeaver: "Weaver") -> None:
         """Create a reference to this chunk -- except for anonymous chunks."""
         raise Exception( "Cannot reference an anonymous chunk.""")
    -def weaveShortReferenceTo( self, aWeb, aWeaver ):
    +def weaveShortReferenceTo(self, aWeb: "Web", aWeaver: "Weaver") -> None:
         """Create a short reference to this chunk -- except for anonymous chunks."""
         raise Exception( "Cannot reference an anonymous chunk.""")
     
    -

    Chunk weave this Chunk into the documentation (60). Used by: Chunk class (52)

    +

    Chunk weave this Chunk into the documentation (62). Used by: Chunk base class... (53)

    Anonymous chunks cannot be tangled. Any attempt indicates a serious problem with this program or the input file.

    -

    Chunk tangle this Chunk into a code file (61) =

    +

    Chunk tangle this Chunk into a code file (63) =

    -def tangle( self, aWeb, aTangler ):
    +def tangle(self, aWeb: "Web", aTangler: "Tangler") -> None:
         """Create source code -- except anonymous chunks should not be tangled"""
    -    raise Error( 'Cannot tangle an anonymous chunk', self )
    +    raise Error('Cannot tangle an anonymous chunk', self)
     
    -

    Chunk tangle this Chunk into a code file (61). Used by: Chunk class (52)

    +

    Chunk tangle this Chunk into a code file (63). Used by: Chunk base class... (53)

    Generally, a Chunk with a reference will adjust the indentation for that referenced material. However, this is not universally true, a subclass may not indent when tangling and may -- instead -- put stuff flush at the left margin by forcing the local indent to zero.

    -

    Chunk indent adjustments (62) =

    +

    Chunk indent adjustments (64) =

    -def reference_indent( self, aWeb, aTangler, amount ):
    -    aTangler.addIndent( amount )  # Or possibly set indent to local zero.
    +def reference_indent(self, aWeb: "Web", aTangler: "Tangler", amount: int) -> None:
    +    aTangler.addIndent(amount)  # Or possibly set indent to local zero.
     
    -def reference_dedent( self, aWeb, aTangler ):
    +def reference_dedent(self, aWeb: "Web", aTangler: "Tangler") -> None:
         aTangler.clrIndent()
     
    -

    Chunk indent adjustments (62). Used by: Chunk class (52)

    +

    Chunk indent adjustments (64). Used by: Chunk base class... (53)

    -

    NamedChunk class

    +

    NamedChunk class

    A NamedChunk is created and used almost identically to an anonymous Chunk. The most significant difference is that a name is provided when the NamedChunk is created. This name is used by the Web to organize the chunks.

    @@ -3178,56 +3522,59 @@

    NamedChunk class

    has the sequence number associated with this chunk. This is set by the Web by the webAdd() method.
    -

    NamedChunk class (63) =

    +

    NamedChunk class for defined names (65) =

    -class NamedChunk( Chunk ):
    +class NamedChunk(Chunk):
         """Named piece of input file: will be output as both tangler and weaver."""
    -    def __init__( self, name ):
    +    def __init__(self, name: str) -> None:
             super().__init__()
    -        self.name= name
    -        self.user_id_list= []
    -        self.refCount= 0
    -    def __str__( self ):
    -        return "{!r}: {!s}".format( self.name, Chunk.__str__(self) )
    -    def makeContent( self, text, lineNumber=0 ):
    -        return CodeCommand( text, lineNumber )
    -    →NamedChunk user identifiers set and get (64)
    -    →NamedChunk add to the web (65)
    -    →NamedChunk weave into the documentation (66)
    -    →NamedChunk tangle into the source file (67)
    +        self.name = name
    +        self.user_id_list = []
    +        self.refCount = 0
    +
    +    def __str__(self) -> str:
    +        return f"{self.name!r}: {self!s}"
    +
    +    def makeContent(self, text: str, lineNumber: int = 0) -> Command:
    +        return CodeCommand(text, lineNumber)
    +
    +    →NamedChunk user identifiers set and get (66)
    +    →NamedChunk add to the web (67)
    +    →NamedChunk weave into the documentation (68)
    +    →NamedChunk tangle into the source file (69)
     
    -

    NamedChunk class (63). Used by: Chunk class hierarchy... (51)

    +

    NamedChunk class for defined names (65). Used by: Chunk class hierarchy... (52)

    The setUserIDRefs() method accepts a list of user identifiers that are associated with this chunk. These are provided after the @| separator in a @d named chunk. These are used by the @u cross reference generator.

    -

    NamedChunk user identifiers set and get (64) =

    +

    NamedChunk user identifiers set and get (66) =

    -def setUserIDRefs( self, text ):
    +def setUserIDRefs(self, text: str) -> None:
         """Save user ID's associated with this chunk."""
    -    self.user_id_list= text.split()
    -def getUserIDRefs( self ):
    +    self.user_id_list = text.split()
    +def getUserIDRefs(self) -> list[str]:
         return self.user_id_list
     
    -

    NamedChunk user identifiers set and get (64). Used by: NamedChunk class (63)

    +

    NamedChunk user identifiers set and get (66). Used by: NamedChunk class... (65)

    The webAdd() method adds this chunk to the given document Web instance. Each class of Chunk must override this to be sure that the various Chunk classes are indexed properly. This class uses the Web.addNamed() method of the Web class to append a named chunk.

    -

    NamedChunk add to the web (65) =

    +

    NamedChunk add to the web (67) =

    -def webAdd( self, web ):
    +def webAdd(self, web: "Web") -> None:
         """Add self to a Web as named chunk, update xrefs."""
    -    web.addNamed( self )
    +    web.addNamed(self)
     
    -

    NamedChunk add to the web (65). Used by: NamedChunk class (63)

    +

    NamedChunk add to the web (67). Used by: NamedChunk class... (65)

    The weave() method weaves this chunk into the final document as follows:

      @@ -3245,30 +3592,30 @@

      NamedChunk class

      These references are created by ReferenceCommand instances within a chunk being woven.

      The woven references simply follow whatever preceded them on the line; the indent (if any) doesn't change from the default.

      -

      NamedChunk weave into the documentation (66) =

      +

      NamedChunk weave into the documentation (68) =

      -def weave( self, aWeb, aWeaver ):
      +def weave(self, aWeb: "Web", aWeaver: "Weaver") -> None:
           """Create the nicely formatted document from a chunk of code."""
      -    self.fullName= aWeb.fullNameFor( self.name )
      +    self.fullName = aWeb.fullNameFor(self.name)
           aWeaver.addIndent()
      -    aWeaver.codeBegin( self )
      +    aWeaver.codeBegin(self)
           for cmd in self.commands:
      -        cmd.weave( aWeb, aWeaver )
      +        cmd.weave(aWeb, aWeaver)
           aWeaver.clrIndent( )
      -    aWeaver.codeEnd( self )
      -def weaveReferenceTo( self, aWeb, aWeaver ):
      +    aWeaver.codeEnd(self)
      +def weaveReferenceTo(self, aWeb: "Web", aWeaver: "Weaver") -> None:
           """Create a reference to this chunk."""
      -    self.fullName= aWeb.fullNameFor( self.name )
      -    txt= aWeaver.referenceTo( self.fullName, self.seq )
      -    aWeaver.codeBlock( txt )
      -def weaveShortReferenceTo( self, aWeb, aWeaver ):
      +    self.fullName = aWeb.fullNameFor(self.name)
      +    txt = aWeaver.referenceTo(self.fullName, self.seq)
      +    aWeaver.codeBlock(txt)
      +def weaveShortReferenceTo(self, aWeb: "Web", aWeaver: "Weaver") -> None:
           """Create a shortened reference to this chunk."""
      -    txt= aWeaver.referenceTo( None, self.seq )
      -    aWeaver.codeBlock( txt )
      +    txt = aWeaver.referenceTo(None, self.seq)
      +    aWeaver.codeBlock(txt)
       
      -

      NamedChunk weave into the documentation (66). Used by: NamedChunk class (63)

      +

      NamedChunk weave into the documentation (68). Used by: NamedChunk class... (65)

      The tangle() method tangles this chunk into the final document as follows:

        @@ -3279,46 +3626,46 @@

        NamedChunk class

        If a ReferenceCommand does raise an error during tangling, we append this Chunk information and reraise the error with the additional context information.

        -

        NamedChunk tangle into the source file (67) =

        +

        NamedChunk tangle into the source file (69) =

        -def tangle( self, aWeb, aTangler ):
        +def tangle(self, aWeb: "Web", aTangler: "Tangler") -> None:
             """Create source code.
             Use aWeb to resolve @<namedChunk@>.
             Format as correctly indented source text
             """
        -    self.previous_command= TextCommand( "", self.commands[0].lineNumber )
        -    aTangler.codeBegin( self )
        +    self.previous_command = TextCommand("", self.commands[0].lineNumber)
        +    aTangler.codeBegin(self)
             for t in self.commands:
                 try:
        -            t.tangle( aWeb, aTangler )
        +            t.tangle(aWeb, aTangler)
                 except Error as e:
                     raise
        -        self.previous_command= t
        -    aTangler.codeEnd( self )
        +        self.previous_command = t
        +    aTangler.codeEnd(self)
         
        -

        NamedChunk tangle into the source file (67). Used by: NamedChunk class (63)

        +

        NamedChunk tangle into the source file (69). Used by: NamedChunk class... (65)

        There's a second variation on NamedChunk, one that doesn't indent based on context. It simply sets an indent at the left margin.

        -

        NamedChunk class (68) +=

        +

        NamedChunk class for defined names (70) +=

        -class NamedChunk_Noindent( NamedChunk ):
        +class NamedChunk_Noindent(NamedChunk):
             """Named piece of input file: will be output as both tangler and weaver."""
        -    def reference_indent( self, aWeb, aTangler, amount ):
        -        aTangler.setIndent( 0 )
        +    def reference_indent(self, aWeb: "Web", aTangler: "Tangler", amount: int) -> None:
        +        aTangler.setIndent(0)
         
        -    def reference_dedent( self, aWeb, aTangler ):
        +    def reference_dedent(self, aWeb: "Web", aTangler: "Tangler") -> None:
                 aTangler.clrIndent()
         
        -

        NamedChunk class (68). Used by: Chunk class hierarchy... (51)

        +

        NamedChunk class for defined names (70). Used by: Chunk class hierarchy... (52)

    -

    OutputChunk class

    +

    OutputChunk class

    A OutputChunk is created and used identically to a NamedChunk. The difference between this class and the parent class is the decoration of the markup when weaving.

    @@ -3330,35 +3677,35 @@

    OutputChunk class

    markup to make it stand out from documentation. Other subclasses could override this to use different Weaver methods for different kinds of text.

    All other methods, including the tangle method are identical to NamedChunk.

    -

    OutputChunk class (69) =

    +

    OutputChunk class (71) =

    -class OutputChunk( NamedChunk ):
    +class OutputChunk(NamedChunk):
         """Named piece of input file, defines an output tangle."""
    -    def __init__( self, name, comment_start=None, comment_end="" ):
    -        super().__init__( name )
    -        self.comment_start= comment_start
    -        self.comment_end= comment_end
    -    →OutputChunk add to the web (70)
    -    →OutputChunk weave (71)
    -    →OutputChunk tangle (72)
    +    def __init__(self, name: str, comment_start: str = "", comment_end: str = "") -> None:
    +        super().__init__(name)
    +        self.comment_start = comment_start
    +        self.comment_end = comment_end
    +    →OutputChunk add to the web (72)
    +    →OutputChunk weave (73)
    +    →OutputChunk tangle (74)
     
    -

    OutputChunk class (69). Used by: Chunk class hierarchy... (51)

    +

    OutputChunk class (71). Used by: Chunk class hierarchy... (52)

    The webAdd() method adds this chunk to the given document Web. Each class of Chunk must override this to be sure that the various Chunk classes are indexed properly. This class uses the addOutput() method of the Web class to append a file output chunk.

    -

    OutputChunk add to the web (70) =

    +

    OutputChunk add to the web (72) =

    -def webAdd( self, web ):
    +def webAdd(self, web: "Web") -> None:
         """Add self to a Web as output chunk, update xrefs."""
    -    web.addOutput( self )
    +    web.addOutput(self)
     
    -

    OutputChunk add to the web (70). Used by: OutputChunk class (69)

    +

    OutputChunk add to the web (72). Used by: OutputChunk class... (71)

    The weave() method weaves this chunk into the final document as follows:

      @@ -3371,36 +3718,36 @@

      OutputChunk class

      If a ReferenceCommand does raise an error during weaving, we append this Chunk information and reraise the error with the additional context information.

      -

      OutputChunk weave (71) =

      +

      OutputChunk weave (73) =

      -def weave( self, aWeb, aWeaver ):
      +def weave(self, aWeb: "Web", aWeaver: "Weaver") -> None:
           """Create the nicely formatted document from a chunk of code."""
      -    self.fullName= aWeb.fullNameFor( self.name )
      -    aWeaver.fileBegin( self )
      +    self.fullName = aWeb.fullNameFor(self.name)
      +    aWeaver.fileBegin(self)
           for cmd in self.commands:
      -        cmd.weave( aWeb, aWeaver )
      -    aWeaver.fileEnd( self )
      +        cmd.weave(aWeb, aWeaver)
      +    aWeaver.fileEnd(self)
       
      -

      OutputChunk weave (71). Used by: OutputChunk class (69)

      +

      OutputChunk weave (73). Used by: OutputChunk class... (71)

      When we tangle, we provide the output Chunk's comment information to the Tangler to be sure that -- if line numbers were requested -- they can be included properly.

      -

      OutputChunk tangle (72) =

      +

      OutputChunk tangle (74) =

      -def tangle( self, aWeb, aTangler ):
      -    aTangler.comment_start= self.comment_start
      -    aTangler.comment_end= self.comment_end
      -    super().tangle( aWeb, aTangler )
      +def tangle(self, aWeb: "Web", aTangler: "Tangler") -> None:
      +    aTangler.comment_start = self.comment_start
      +    aTangler.comment_end = self.comment_end
      +    super().tangle(aWeb, aTangler)
       
      -

      OutputChunk tangle (72). Used by: OutputChunk class (69)

      +

      OutputChunk tangle (74). Used by: OutputChunk class... (71)

    -

    NamedDocumentChunk class

    +

    NamedDocumentChunk class

    A NamedDocumentChunk is created and used identically to a NamedChunk. The difference between this class and the parent class is that this chunk is only woven when referenced. The original definition is silently skipped.

    @@ -3413,18 +3760,20 @@

    NamedDocumentChunk class

    by anonymous chunks. While this chunk subclass participates in this data gathering, it is ignored for reporting purposes.

    All other methods, including the tangle method are identical to NamedChunk.

    -

    NamedDocumentChunk class (73) =

    +

    NamedDocumentChunk class (75) =

    -class NamedDocumentChunk( NamedChunk ):
    +class NamedDocumentChunk(NamedChunk):
         """Named piece of input file with document source, defines an output tangle."""
    -    def makeContent( self, text, lineNumber=0 ):
    -        return TextCommand( text, lineNumber )
    -    →NamedDocumentChunk weave (74)
    -    →NamedDocumentChunk tangle (75)
    +
    +    def makeContent(self, text: str, lineNumber: int = 0) -> Command:
    +        return TextCommand(text, lineNumber)
    +
    +    →NamedDocumentChunk weave (76)
    +    →NamedDocumentChunk tangle (77)
     
    -

    NamedDocumentChunk class (73). Used by: Chunk class hierarchy... (51)

    +

    NamedDocumentChunk class (75). Used by: Chunk class hierarchy... (52)

    The weave() method quietly ignores this chunk in the document. A named document chunk is only included when it is referenced @@ -3435,37 +3784,37 @@

    NamedDocumentChunk class

    ReferenceCommand in another chunk. The weaveShortReferenceTo() method calls the weaveReferenceTo() to insert the entire chunk.

    -

    NamedDocumentChunk weave (74) =

    +

    NamedDocumentChunk weave (76) =

    -def weave( self, aWeb, aWeaver ):
    +def weave(self, aWeb: "Web", aWeaver: "Weaver") -> None:
         """Ignore this when producing the document."""
         pass
    -def weaveReferenceTo( self, aWeb, aWeaver ):
    +def weaveReferenceTo(self, aWeb: "Web", aWeaver: "Weaver") -> None:
         """On a reference to this chunk, expand the body in place."""
         for cmd in self.commands:
    -        cmd.weave( aWeb, aWeaver )
    -def weaveShortReferenceTo( self, aWeb, aWeaver ):
    +        cmd.weave(aWeb, aWeaver)
    +def weaveShortReferenceTo(self, aWeb: "Web", aWeaver: "Weaver") -> None:
         """On a reference to this chunk, expand the body in place."""
    -    self.weaveReferenceTo( aWeb, aWeaver )
    +    self.weaveReferenceTo(aWeb, aWeaver)
     
    -

    NamedDocumentChunk weave (74). Used by: NamedDocumentChunk class (73)

    +

    NamedDocumentChunk weave (76). Used by: NamedDocumentChunk class... (75)

    -

    NamedDocumentChunk tangle (75) =

    +

    NamedDocumentChunk tangle (77) =

    -def tangle( self, aWeb, aTangler ):
    +def tangle(self, aWeb: "Web", aTangler: "Tangler") -> None:
         """Raise an exception on an attempt to tangle."""
    -    raise Error( "Cannot tangle a chunk defined with @[.""" )
    +    raise Error("Cannot tangle a chunk defined with @[.""")
     
    -

    NamedDocumentChunk tangle (75). Used by: NamedDocumentChunk class (73)

    +

    NamedDocumentChunk tangle (77). Used by: NamedDocumentChunk class... (75)

    -

    Commands

    +

    Commands

    The input stream is broken into individual commands, based on the various @*x* strings in the file. There are several subclasses of Command, each used to describe a different command or block of text in the input.

    @@ -3476,30 +3825,37 @@

    Commands

    of commands.

    Each Command instance responds to methods to examine the content, gather cross reference information and tangle a file or weave the final document.

    -

    Command class hierarchy - used to describe individual commands (76) =

    +

    Command class hierarchy - used to describe individual commands (78) =

    -→Command superclass (77)
    -→TextCommand class to contain a document text block (80)
    -→CodeCommand class to contain a program source code block (81)
    -→XrefCommand superclass for all cross-reference commands (82)
    -→FileXrefCommand class for an output file cross-reference (83)
    -→MacroXrefCommand class for a named chunk cross-reference (84)
    -→UserIdXrefCommand class for a user identifier cross-reference (85)
    -→ReferenceCommand class for chunk references (86)
    +→Command superclass (79)
    +
    +→TextCommand class to contain a document text block (82)
    +
    +→CodeCommand class to contain a program source code block (83)
    +
    +→XrefCommand superclass for all cross-reference commands (84)
    +
    +→FileXrefCommand class for an output file cross-reference (85)
    +
    +→MacroXrefCommand class for a named chunk cross-reference (86)
    +
    +→UserIdXrefCommand class for a user identifier cross-reference (87)
    +
    +→ReferenceCommand class for chunk references (88)
     
    -

    Command class hierarchy - used to describe individual commands (76). Used by: Base Class Definitions (1)

    +

    Command class hierarchy - used to describe individual commands (78). Used by: Base Class Definitions (1)

    -

    Command Superclass

    +

    Command Superclass

    A Command is created by the WebReader, and attached to a Chunk. The Command participates in cross reference creation, weaving and tangling.

    The Command superclass is abstract, and has default methods factored out of the various subclasses. When a subclass is created, it will override some of the methods provided in this superclass.

    -class MyNewCommand( Command ):
    +class MyNewCommand(Command):
         ... overrides for various methods ...
     

    Additionally, a subclass of WebReader must be defined to parse the new command @@ -3537,52 +3893,66 @@

    Command Superclass

    The attributes of a Command instance includes the line number on which the command began, in lineNumber.

    -

    Command superclass (77) =

    +

    Command superclass (79) =

    -class Command:
    +class Command(abc.ABC):
         """A Command is the lowest level of granularity in the input stream."""
    -    def __init__( self, fromLine=0 ):
    -        self.lineNumber= fromLine+1 # tokenizer is zero-based
    -        self.chunk= None
    -        self.logger= logging.getLogger( self.__class__.__qualname__ )
    -    def __str__( self ):
    -        return "at {!r}".format(self.lineNumber)
    -    →Command analysis features: starts-with and Regular Expression search (78)
    -    →Command tangle and weave functions (79)
    +    chunk : "Chunk"
    +    text : str
    +    def __init__(self, fromLine: int = 0) -> None:
    +        self.lineNumber = fromLine+1 # tokenizer is zero-based
    +        self.logger = logging.getLogger(self.__class__.__qualname__)
    +
    +    def __str__(self) -> str:
    +        return f"at {self.lineNumber!r}"
    +
    +    def __eq__(self, other: Any) -> bool:
    +        match other:
    +            case Command():
    +                return self.lineNumber == other.lineNumber and self.text == other.text
    +            case _:
    +                return NotImplemented
    +
    +    →Command analysis features: starts-with and Regular Expression search (80)
    +    →Command tangle and weave functions (81)
     
    -

    Command superclass (77). Used by: Command class hierarchy... (76)

    +

    Command superclass (79). Used by: Command class hierarchy... (78)

    -

    Command analysis features: starts-with and Regular Expression search (78) =

    +

    Command analysis features: starts-with and Regular Expression search (80) =

    -def startswith( self, prefix ):
    -    return None
    -def searchForRE( self, rePat ):
    -    return None
    -def indent( self ):
    +def startswith(self, prefix: str) -> bool:
    +    return False
    +def searchForRE(self, rePat: Pattern[str]) -> Match[str] | None:
         return None
    +def indent(self) -> int:
    +    return 0
     
    -

    Command analysis features: starts-with and Regular Expression search (78). Used by: Command superclass (77)

    +

    Command analysis features: starts-with and Regular Expression search (80). Used by: Command superclass (79)

    -

    Command tangle and weave functions (79) =

    +

    Command tangle and weave functions (81) =

    -def ref( self, aWeb ):
    +def ref(self, aWeb: "Web") -> str | None:
         return None
    -def weave( self, aWeb, aWeaver ):
    -    pass
    -def tangle( self, aWeb, aTangler ):
    -    pass
    +
    +@abc.abstractmethod
    +def weave(self, aWeb: "Web", aWeaver: "Weaver") -> None:
    +    ...
    +
    +@abc.abstractmethod
    +def tangle(self, aWeb: "Web", aTangler: "Tangler") -> None:
    +    ...
     
    -

    Command tangle and weave functions (79). Used by: Command superclass (77)

    +

    Command tangle and weave functions (81). Used by: Command superclass (79)

    -

    TextCommand class

    +

    TextCommand class

    A TextCommand is created by a Chunk or a NamedDocumentChunk when a WebReader calls the chunk's appendText() method.

    This Command participates in cross reference creation, weaving and tangling. When it is @@ -3593,20 +3963,21 @@

    TextCommand class

    This subclass provides a concrete implementation for all of the methods. Since text is the author's original markup language, it is emitted directly to the weaver or tangler.

    -

    TextCommand class to contain a document text block (80) =

    +

    TODO: Use textwrap.shorten to snip off first 32 chars of the text.

    +

    TextCommand class to contain a document text block (82) =

    -class TextCommand( Command ):
    +class TextCommand(Command):
         """A piece of document source text."""
    -    def __init__( self, text, fromLine=0 ):
    -        super().__init__( fromLine )
    -        self.text= text
    -    def __str__( self ):
    -        return "at {!r}: {!r}...".format(self.lineNumber,self.text[:32])
    -    def startswith( self, prefix ):
    -        return self.text.startswith( prefix )
    -    def searchForRE( self, rePat ):
    -        return rePat.search( self.text )
    -    def indent( self ):
    +    def __init__(self, text: str, fromLine: int = 0) -> None:
    +        super().__init__(fromLine)
    +        self.text = text
    +    def __str__(self) -> str:
    +        return f"at {self.lineNumber!r}: {self.text[:32]!r}..."
    +    def startswith(self, prefix: str) -> bool:
    +        return self.text.startswith(prefix)
    +    def searchForRE(self, rePat: Pattern[str]) -> Match[str] | None:
    +        return rePat.search(self.text)
    +    def indent(self) -> int:
             if self.text.endswith('\n'):
                 return 0
             try:
    @@ -3614,18 +3985,18 @@ 

    TextCommand class

    return len(last_line) except IndexError: return 0 - def weave( self, aWeb, aWeaver ): - aWeaver.write( self.text ) - def tangle( self, aWeb, aTangler ): - aTangler.write( self.text ) + def weave(self, aWeb: "Web", aWeaver: "Weaver") -> None: + aWeaver.write(self.text) + def tangle(self, aWeb: "Web", aTangler: "Tangler") -> None: + aTangler.write(self.text)
    -

    TextCommand class to contain a document text block (80). Used by: Command class hierarchy... (76)

    +

    TextCommand class to contain a document text block (82). Used by: Command class hierarchy... (78)

    -

    CodeCommand class

    +

    CodeCommand class

    A CodeCommand is created by a NamedChunk when a WebReader calls the appendText() method. The Command participates in cross reference creation, weaving and tangling. When it is @@ -3637,22 +4008,22 @@

    CodeCommand class

    It uses the codeBlock() methods of a Weaver or Tangler. The weaver will insert appropriate markup for this code. The tangler will assure that the prevailing indentation is maintained.

    -

    CodeCommand class to contain a program source code block (81) =

    +

    CodeCommand class to contain a program source code block (83) =

    -class CodeCommand( TextCommand ):
    +class CodeCommand(TextCommand):
         """A piece of program source code."""
    -    def weave( self, aWeb, aWeaver ):
    -        aWeaver.codeBlock( aWeaver.quote( self.text ) )
    -    def tangle( self, aWeb, aTangler ):
    -        aTangler.codeBlock( self.text )
    +    def weave(self, aWeb: "Web", aWeaver: "Weaver") -> None:
    +        aWeaver.codeBlock(aWeaver.quote(self.text))
    +    def tangle(self, aWeb: "Web", aTangler: "Tangler") -> None:
    +        aTangler.codeBlock(self.text)
     
    -

    CodeCommand class to contain a program source code block (81). Used by: Command class hierarchy... (76)

    +

    CodeCommand class to contain a program source code block (83). Used by: Command class hierarchy... (78)

    -

    XrefCommand superclass

    +

    XrefCommand superclass

    An XrefCommand is created by the WebReader when any of the @f, @m, @u commands are found in the input stream. The Command is then appended to the current Chunk being built by the WebReader.

    @@ -3668,69 +4039,71 @@

    XrefCommand superclass

If this command winds up in a tangle action, that use is illegal. An exception is raised and processing stops.

-

XrefCommand superclass for all cross-reference commands (82) =

+

XrefCommand superclass for all cross-reference commands (84) =

-class XrefCommand( Command ):
+class XrefCommand(Command):
     """Any of the Xref-goes-here commands in the input."""
-    def __str__( self ):
-        return "at {!r}: cross reference".format(self.lineNumber)
-    def formatXref( self, xref, aWeaver ):
+    def __str__(self) -> str:
+        return f"at {self.lineNumber!r}: cross reference"
+
+    def formatXref(self, xref: dict[str, list[int]], aWeaver: "Weaver") -> None:
         aWeaver.xrefHead()
         for n in sorted(xref):
-            aWeaver.xrefLine( n, xref[n] )
+            aWeaver.xrefLine(n, xref[n])
         aWeaver.xrefFoot()
-    def tangle( self, aWeb, aTangler ):
+
+    def tangle(self, aWeb: "Web", aTangler: "Tangler") -> None:
         raise Error('Illegal tangling of a cross reference command.')
 
-

XrefCommand superclass for all cross-reference commands (82). Used by: Command class hierarchy... (76)

+

XrefCommand superclass for all cross-reference commands (84). Used by: Command class hierarchy... (78)

-

FileXrefCommand class

+

FileXrefCommand class

A FileXrefCommand is created by the WebReader when the @f command is found in the input stream. The Command is then appended to the current Chunk being built by the WebReader.

The FileXrefCommand class weave method gets the file cross reference from the overall web instance, and uses the formatXref() method of the XrefCommand superclass for format this result.

-

FileXrefCommand class for an output file cross-reference (83) =

+

FileXrefCommand class for an output file cross-reference (85) =

-class FileXrefCommand( XrefCommand ):
+class FileXrefCommand(XrefCommand):
     """A FileXref command."""
-    def weave( self, aWeb, aWeaver ):
+    def weave(self, aWeb: "Web", aWeaver: "Weaver") -> None:
         """Weave a File Xref from @o commands."""
-        self.formatXref( aWeb.fileXref(), aWeaver )
+        self.formatXref(aWeb.fileXref(), aWeaver)
 
-

FileXrefCommand class for an output file cross-reference (83). Used by: Command class hierarchy... (76)

+

FileXrefCommand class for an output file cross-reference (85). Used by: Command class hierarchy... (78)

-

MacroXrefCommand class

+

MacroXrefCommand class

A MacroXrefCommand is created by the WebReader when the @m command is found in the input stream. The Command is then appended to the current Chunk being built by the WebReader.

The MacroXrefCommand class weave method gets the named chunk (macro) cross reference from the overall web instance, and uses the formatXref() method of the XrefCommand superclass method for format this result.

-

MacroXrefCommand class for a named chunk cross-reference (84) =

+

MacroXrefCommand class for a named chunk cross-reference (86) =

-class MacroXrefCommand( XrefCommand ):
+class MacroXrefCommand(XrefCommand):
     """A MacroXref command."""
-    def weave( self, aWeb, aWeaver ):
+    def weave(self, aWeb: "Web", aWeaver: "Weaver") -> None:
         """Weave the Macro Xref from @d commands."""
-        self.formatXref( aWeb.chunkXref(), aWeaver )
+        self.formatXref(aWeb.chunkXref(), aWeaver)
 
-

MacroXrefCommand class for a named chunk cross-reference (84). Used by: Command class hierarchy... (76)

+

MacroXrefCommand class for a named chunk cross-reference (86). Used by: Command class hierarchy... (78)

-

UserIdXrefCommand class

+

UserIdXrefCommand class

A MacroXrefCommand is created by the WebReader when the @u command is found in the input stream. The Command is then appended to the current Chunk being built by the WebReader.

@@ -3744,29 +4117,29 @@

UserIdXrefCommand class

  • Use the Weaver class xrefDefLine() method to emit each line of the cross-reference definition mapping.
  • Use the Weaver class xrefFoor() method to emit the cross-reference footer.
  • -

    UserIdXrefCommand class for a user identifier cross-reference (85) =

    +

    UserIdXrefCommand class for a user identifier cross-reference (87) =

    -class UserIdXrefCommand( XrefCommand ):
    +class UserIdXrefCommand(XrefCommand):
         """A UserIdXref command."""
    -    def weave( self, aWeb, aWeaver ):
    +    def weave(self, aWeb: "Web", aWeaver: "Weaver") -> None:
             """Weave a user identifier Xref from @d commands."""
    -        ux= aWeb.userNamesXref()
    +        ux = aWeb.userNamesXref()
             if len(ux) != 0:
                 aWeaver.xrefHead()
                 for u in sorted(ux):
    -                defn, refList= ux[u]
    -                aWeaver.xrefDefLine( u, defn, refList )
    +                defn, refList = ux[u]
    +                aWeaver.xrefDefLine(u, defn, refList)
                 aWeaver.xrefFoot()
             else:
                 aWeaver.xrefEmpty()
     
    -

    UserIdXrefCommand class for a user identifier cross-reference (85). Used by: Command class hierarchy... (76)

    +

    UserIdXrefCommand class for a user identifier cross-reference (87). Used by: Command class hierarchy... (78)

    -

    ReferenceCommand class

    +

    ReferenceCommand class

    A ReferenceCommand instance is created by a WebReader when a @<name@> construct in is found in the input stream. This is attached to the current Chunk being built by the WebReader.

    @@ -3788,70 +4161,72 @@

    ReferenceCommand class

    -

    ReferenceCommand class for chunk references (86) =

    +

    ReferenceCommand class for chunk references (88) =

    -class ReferenceCommand( Command ):
    +class ReferenceCommand(Command):
         """A reference to a named chunk, via @<name@>."""
    -    def __init__( self, refTo, fromLine=0 ):
    -        super().__init__( fromLine )
    -        self.refTo= refTo
    -        self.fullname= None
    -        self.sequenceList= None
    -        self.chunkList= []
    -    def __str__( self ):
    -        return "at {!r}: reference to chunk {!r}".format(self.lineNumber,self.refTo)
    -    →ReferenceCommand resolve a referenced chunk name (87)
    -    →ReferenceCommand refers to a chunk (88)
    -    →ReferenceCommand weave a reference to a chunk (89)
    -    →ReferenceCommand tangle a referenced chunk (90)
    +    def __init__(self, refTo: str, fromLine: int = 0) -> None:
    +        super().__init__(fromLine)
    +        self.refTo = refTo
    +        self.fullname = None
    +        self.sequenceList = None
    +        self.chunkList: list[Chunk] = []
    +
    +    def __str__(self) -> str:
    +        return "at {self.lineNumber!r}: reference to chunk {self.refTo!r}"
    +
    +    →ReferenceCommand resolve a referenced chunk name (89)
    +    →ReferenceCommand refers to a chunk (90)
    +    →ReferenceCommand weave a reference to a chunk (91)
    +    →ReferenceCommand tangle a referenced chunk (92)
     
    -

    ReferenceCommand class for chunk references (86). Used by: Command class hierarchy... (76)

    +

    ReferenceCommand class for chunk references (88). Used by: Command class hierarchy... (78)

    The resolve() method queries the overall Web instance for the full name and sequence number for this chunk reference. This is used by the Weaver class referenceTo() method to write the markup reference to the chunk.

    -

    ReferenceCommand resolve a referenced chunk name (87) =

    +

    ReferenceCommand resolve a referenced chunk name (89) =

    -def resolve( self, aWeb ):
    +def resolve(self, aWeb: "Web") -> None:
         """Expand our chunk name and list of parts"""
    -    self.fullName= aWeb.fullNameFor( self.refTo )
    -    self.chunkList= aWeb.getchunk( self.refTo )
    +    self.fullName = aWeb.fullNameFor(self.refTo)
    +    self.chunkList = aWeb.getchunk(self.refTo)
     
    -

    ReferenceCommand resolve a referenced chunk name (87). Used by: ReferenceCommand class... (86)

    +

    ReferenceCommand resolve a referenced chunk name (89). Used by: ReferenceCommand class... (88)

    The ref() method is a request that is delegated by a Chunk; it resolves the reference this Command makes within the containing Chunk. When the Chunk iterates through the Commands, it can accumulate a list of Chinks to which it refers.

    -

    ReferenceCommand refers to a chunk (88) =

    +

    ReferenceCommand refers to a chunk (90) =

    -def ref( self, aWeb ):
    +def ref(self, aWeb: "Web") -> str:
         """Find and return the full name for this reference."""
    -    self.resolve( aWeb )
    +    self.resolve(aWeb)
         return self.fullName
     
    -

    ReferenceCommand refers to a chunk (88). Used by: ReferenceCommand class... (86)

    +

    ReferenceCommand refers to a chunk (90). Used by: ReferenceCommand class... (88)

    The weave() method inserts a markup reference to a named chunk. It uses the Weaver class referenceTo() method to format this appropriately for the document type being woven.

    -

    ReferenceCommand weave a reference to a chunk (89) =

    +

    ReferenceCommand weave a reference to a chunk (91) =

    -def weave( self, aWeb, aWeaver ):
    +def weave(self, aWeb: "Web", aWeaver: "Weaver") -> None:
         """Create the nicely formatted reference to a chunk of code."""
    -    self.resolve( aWeb )
    -    aWeb.weaveChunk( self.fullName, aWeaver )
    +    self.resolve(aWeb)
    +    aWeb.weaveChunk(self.fullName, aWeaver)
     
    -

    ReferenceCommand weave a reference to a chunk (89). Used by: ReferenceCommand class... (86)

    +

    ReferenceCommand weave a reference to a chunk (91). Used by: ReferenceCommand class... (88)

    The tangle() method inserts the resolved chunk in this place. When a chunk is tangled, it sets the indent, @@ -3859,32 +4234,32 @@

    ReferenceCommand class

    This is where the Tangler indentation is updated by a reference. Or where indentation is set to a local zero because the included Chunk is a no-indent Chunk.

    -

    ReferenceCommand tangle a referenced chunk (90) =

    +

    ReferenceCommand tangle a referenced chunk (92) =

    -def tangle( self, aWeb, aTangler ):
    +def tangle(self, aWeb: "Web", aTangler: "Tangler") -> None:
         """Create source code."""
    -    self.resolve( aWeb )
    +    self.resolve(aWeb)
     
    -    self.logger.debug( "Indent {!r} + {!r}".format(aTangler.context, self.chunk.previous_command.indent()) )
    -    self.chunk.reference_indent( aWeb, aTangler, self.chunk.previous_command.indent() )
    +    self.logger.debug("Indent %r + %r", aTangler.context, self.chunk.previous_command.indent())
    +    self.chunk.reference_indent(aWeb, aTangler, self.chunk.previous_command.indent())
     
    -    self.logger.debug( "Tangling chunk {!r}".format(self.fullName) )
    +    self.logger.debug("Tangling %r with chunks %r", self.fullName, self.chunkList)
         if len(self.chunkList) != 0:
             for p in self.chunkList:
    -            p.tangle( aWeb, aTangler )
    +            p.tangle(aWeb, aTangler)
         else:
    -        raise Error( "Attempt to tangle an undefined Chunk, {!s}.".format( self.fullName, ) )
    +        raise Error(f"Attempt to tangle an undefined Chunk, {self.fullName!s}.")
     
    -    self.chunk.reference_dedent( aWeb, aTangler )
    +    self.chunk.reference_dedent(aWeb, aTangler)
     
    -

    ReferenceCommand tangle a referenced chunk (90). Used by: ReferenceCommand class... (86)

    +

    ReferenceCommand tangle a referenced chunk (92). Used by: ReferenceCommand class... (88)

    -

    Reference Strategy

    +

    Reference Strategy

    The Reference Strategy has two implementations. An instance of this is injected into each Chunk by the Web. By injecting this algorithm, we assure that:

    @@ -3893,70 +4268,72 @@

    Reference Strategy

  • a simple configuration change can be applied to the document.
  • -

    Reference Superclass

    +

    Reference Superclass

    The superclass is an abstract class that defines the interface for this object.

    -

    Reference class hierarchy - strategies for references to a chunk (91) =

    +

    Reference class hierarchy - strategies for references to a chunk (93) =

    -class Reference:
    -    def __init__( self ):
    -        self.logger= logging.getLogger( self.__class__.__qualname__ )
    -    def chunkReferencedBy( self, aChunk ):
    +class Reference(abc.ABC):
    +    def __init__(self) -> None:
    +        self.logger = logging.getLogger(self.__class__.__qualname__)
    +
    +    @abc.abstractmethod
    +    def chunkReferencedBy(self, aChunk: Chunk) -> list[Chunk]:
             """Return a list of Chunks."""
    -        pass
    +        ...
     
    -

    Reference class hierarchy - strategies for references to a chunk (91). Used by: Base Class Definitions (1)

    +

    Reference class hierarchy - strategies for references to a chunk (93). Used by: Base Class Definitions (1)

    -

    SimpleReference Class

    +

    SimpleReference Class

    The SimpleReference subclass does the simplest version of resolution. It returns the Chunks referenced.

    -

    Reference class hierarchy - strategies for references to a chunk (92) +=

    +

    Reference class hierarchy - strategies for references to a chunk (94) +=

    -class SimpleReference( Reference ):
    -    def chunkReferencedBy( self, aChunk ):
    -        refBy= aChunk.referencedBy
    +class SimpleReference(Reference):
    +    def chunkReferencedBy(self, aChunk: Chunk) -> list[Chunk]:
    +        refBy = aChunk.referencedBy
             return refBy
     
    -

    Reference class hierarchy - strategies for references to a chunk (92). Used by: Base Class Definitions (1)

    +

    Reference class hierarchy - strategies for references to a chunk (94). Used by: Base Class Definitions (1)

    -

    TransitiveReference Class

    +

    TransitiveReference Class

    The TransitiveReference subclass does a transitive closure of all references to this Chunk.

    This requires walking through the Web to locate "parents" of each referenced Chunk.

    -

    Reference class hierarchy - strategies for references to a chunk (93) +=

    -
    -class TransitiveReference( Reference ):
    -    def chunkReferencedBy( self, aChunk ):
    -        refBy= aChunk.referencedBy
    -        self.logger.debug( "References: {!s}({:d}) {!r}".format(aChunk.name, aChunk.seq, refBy) )
    -        return self.allParentsOf( refBy )
    -    def allParentsOf( self, chunkList, depth=0 ):
    +

    Reference class hierarchy - strategies for references to a chunk (95) +=

    +
    +class TransitiveReference(Reference):
    +    def chunkReferencedBy(self, aChunk: Chunk) -> list[Chunk]:
    +        refBy = aChunk.referencedBy
    +        self.logger.debug("References: %r(%d) %r", aChunk.name, aChunk.seq, refBy)
    +        return self.allParentsOf(refBy)
    +    def allParentsOf(self, chunkList: list[Chunk], depth: int = 0) -> list[Chunk]:
             """Transitive closure of parents via recursive ascent.
             """
             final = []
             for c in chunkList:
    -            final.append( c )
    -            final.extend( self.allParentsOf( c.referencedBy, depth+1 ) )
    -        self.logger.debug( "References: {0:>{indent}s} {1!s}".format('--', final, indent=2*depth) )
    +            final.append(c)
    +            final.extend(self.allParentsOf(c.referencedBy, depth+1))
    +        self.logger.debug(f"References: {'--':>{2*depth}s} {final!s}")
             return final
     
    -

    Reference class hierarchy - strategies for references to a chunk (93). Used by: Base Class Definitions (1)

    +

    Reference class hierarchy - strategies for references to a chunk (95). Used by: Base Class Definitions (1)

    -

    Error class

    +

    Error class

    An Error is raised whenever processing cannot continue. Since it is a subclass of Exception, it takes an arbitrary number of arguments. The first should be the basic message text. Subsequent arguments provide @@ -3968,32 +4345,32 @@

    Error class

    to the enclosing try/except statement for processing.

    The typical creation is as follows:

    -raise Error("No full name for {!r}".format(chunk.name), chunk)
    +raise Error(f"No full name for {chunk.name!r}", chunk)
     

    A typical exception-handling suite might look like this:

     try:
         ...something that may raise an Error or Exception...
     except Error as e:
    -    print( e.args ) # this is a pyWeb internal Error
    +    print(e.args) # this is a pyWeb internal Error
     except Exception as w:
    -    print( w.args ) # this is some other Python Exception
    +    print(w.args) # this is some other Python Exception
     

    The Error class is a subclass of Exception used to differentiate application-specific exceptions from other Python exceptions. It does no additional processing, but merely creates a distinct class to facilitate writing except statements.

    -

    Error class - defines the errors raised (94) =

    +

    Error class - defines the errors raised (96) =

    -class Error( Exception ): pass
    +class Error(Exception): pass
     
    -

    Error class - defines the errors raised (94). Used by: Base Class Definitions (1)

    +

    Error class - defines the errors raised (96). Used by: Base Class Definitions (1)

    -

    The Web and WebReader Classes

    +

    The Web and WebReader Classes

    The overall web of chunks is carried in a single instance of the Web class that is the principle parameter for the weaving and tangling actions. Broadly, the functionality of a Web can be separated into several areas.

    @@ -4020,7 +4397,7 @@

    The Web and WebReader Classes

    -webFileName:the name of the original .w file. +web_path:the Path of the source .w file. chunkSeq:the sequence of Chunk instances as seen in the input file. To support anonymous chunks, and to assure that the original input document order @@ -4046,33 +4423,35 @@

    The Web and WebReader Classes

    is used to assign a unique sequence number to each named chunk.
    -

    Web class - describes the overall "web" of chunks (95) =

    +

    Web class - describes the overall "web" of chunks (97) =

     class Web:
         """The overall Web of chunks."""
    -    def __init__( self ):
    -        self.webFileName= None
    -        self.chunkSeq= []
    -        self.output= {} # Map filename to Chunk
    -        self.named= {} # Map chunkname to Chunk
    -        self.sequence= 0
    -        self.logger= logging.getLogger( self.__class__.__qualname__ )
    -    def __str__( self ):
    -        return "Web {!r}".format( self.webFileName, )
    +    def __init__(self, file_path: Path | None = None) -> None:
    +        self.web_path = file_path
    +        self.chunkSeq: list[Chunk] = []
    +        self.output: dict[str, list[Chunk]] = {} # Map filename to Chunk
    +        self.named: dict[str, list[Chunk]] = {} # Map chunkname to Chunk
    +        self.sequence = 0
    +        self.errors = 0
    +        self.logger = logging.getLogger(self.__class__.__qualname__)
    +
    +    def __str__(self) -> str:
    +        return f"Web {self.web_path!r}"
     
    -    →Web construction methods used by Chunks and WebReader (97)
    -    →Web Chunk name resolution methods (102), →(103)
    -    →Web Chunk cross reference methods (104), →(106), →(107), →(108)
    -    →Web determination of the language from the first chunk (111)
    -    →Web tangle the output files (112)
    -    →Web weave the output document (113)
    +    →Web construction methods used by Chunks and WebReader (99)
    +    →Web Chunk name resolution methods (104), →(105)
    +    →Web Chunk cross reference methods (106), →(108), →(109), →(110)
    +    →Web determination of the language from the first chunk (113)
    +    →Web tangle the output files (114)
    +    →Web weave the output document (115)
     
    -

    Web class - describes the overall "web" of chunks (95). Used by: Base Class Definitions (1)

    +

    Web class - describes the overall "web" of chunks (97). Used by: Base Class Definitions (1)

    -

    Web Construction

    +

    Web Construction

    During web construction, it is convenient to capture information about the individual Chunk instances being appended to the web. This done using a Callback design pattern. @@ -4085,24 +4464,24 @@

    Web Construction

    has the elided name. This allows a reference to a chunk to contain a more complete description of the chunk.

    We include a weakref to the Web to each Chunk.

    -

    Imports (96) +=

    +

    Imports (98) +=

     import weakref
     
    -

    Imports (96). Used by: pyweb.py (153)

    +

    Imports (98). Used by: pyweb.py (155)

    -

    Web construction methods used by Chunks and WebReader (97) =

    +

    Web construction methods used by Chunks and WebReader (99) =

    -→Web add full chunk names, ignoring abbreviated names (98)
    -→Web add an anonymous chunk (99)
    -→Web add a named macro chunk (100)
    -→Web add an output file definition chunk (101)
    +→Web add full chunk names, ignoring abbreviated names (100)
    +→Web add an anonymous chunk (101)
    +→Web add a named macro chunk (102)
    +→Web add an output file definition chunk (103)
     
    -

    Web construction methods used by Chunks and WebReader (97). Used by: Web class... (95)

    +

    Web construction methods used by Chunks and WebReader (99). Used by: Web class... (97)

    A name is only added to the known names when it is a full name, not an abbreviation ending with "...". @@ -4133,36 +4512,36 @@

    Web Construction

    or lazily -- after the entire Web is built.

    We would no longer need to return a value from this function, either.

    -

    Web add full chunk names, ignoring abbreviated names (98) =

    +

    Web add full chunk names, ignoring abbreviated names (100) =

    -def addDefName( self, name ):
    +def addDefName(self, name: str) -> str | None:
         """Reference to or definition of a chunk name."""
    -    nm= self.fullNameFor( name )
    +    nm = self.fullNameFor(name)
         if nm is None: return None
         if nm[-3:] == '...':
    -        self.logger.debug( "Abbreviated reference {!r}".format(name) )
    +        self.logger.debug("Abbreviated reference %r", name)
             return None # first occurance is a forward reference using an abbreviation
         if nm not in self.named:
    -        self.named[nm]= []
    -        self.logger.debug( "Adding empty chunk {!r}".format(name) )
    +        self.named[nm] = []
    +        self.logger.debug("Adding empty chunk %r", name)
         return nm
     
    -

    Web add full chunk names, ignoring abbreviated names (98). Used by: Web construction... (97)

    +

    Web add full chunk names, ignoring abbreviated names (100). Used by: Web construction... (99)

    An anonymous Chunk is kept in a sequence of Chunks, used for tangling.

    -

    Web add an anonymous chunk (99) =

    +

    Web add an anonymous chunk (101) =

    -def add( self, chunk ):
    +def add(self, chunk: Chunk) -> None:
         """Add an anonymous chunk."""
    -    self.chunkSeq.append( chunk )
    -    chunk.web= weakref.ref(self)
    +    self.chunkSeq.append(chunk)
    +    chunk.web = weakref.ref(self)
     
    -

    Web add an anonymous chunk (99). Used by: Web construction... (97)

    +

    Web add an anonymous chunk (101). Used by: Web construction... (99)

    A named Chunk is defined with a @d command. It is collected into a mapping of NamedChunk instances. @@ -4188,27 +4567,27 @@

    Web Construction

    If we improve name resolution, then the if and exception can go away. The addDefName() no longer needs to return a value.

    -

    Web add a named macro chunk (100) =

    +

    Web add a named macro chunk (102) =

    -def addNamed( self, chunk ):
    +def addNamed(self, chunk: Chunk) -> None:
         """Add a named chunk to a sequence with a given name."""
    -    self.chunkSeq.append( chunk )
    -    chunk.web= weakref.ref(self)
    -    nm= self.addDefName( chunk.name )
    +    self.chunkSeq.append(chunk)
    +    chunk.web = weakref.ref(self)
    +    nm = self.addDefName(chunk.name)
         if nm:
             # We found the full name for this chunk
             self.sequence += 1
    -        chunk.seq= self.sequence
    -        chunk.fullName= nm
    -        self.named[nm].append( chunk )
    -        chunk.initial= len(self.named[nm]) == 1
    -        self.logger.debug( "Extending chunk {!r} from {!r}".format(nm, chunk.name) )
    +        chunk.seq = self.sequence
    +        chunk.fullName = nm
    +        self.named[nm].append(chunk)
    +        chunk.initial = len(self.named[nm]) == 1
    +        self.logger.debug("Extending chunk %r from %r", nm, chunk.name)
         else:
    -        raise Error("No full name for {!r}".format(chunk.name), chunk)
    +        raise Error(f"No full name for {chunk.name!r}", chunk)
     
    -

    Web add a named macro chunk (100). Used by: Web construction... (97)

    +

    Web add a named macro chunk (102). Used by: Web construction... (99)

    An output file definition Chunk is defined with an @o command. It is collected into a mapping of OutputChunk instances. @@ -4227,28 +4606,28 @@

    Web Construction

    unique sequence number sets the Chunk's seq attribute. If the chunk list was empty, this is the first chunk, the initial flag is True if this is the first chunk.

    -

    Web add an output file definition chunk (101) =

    +

    Web add an output file definition chunk (103) =

    -def addOutput( self, chunk ):
    +def addOutput(self, chunk: Chunk) -> None:
         """Add an output chunk to a sequence with a given name."""
    -    self.chunkSeq.append( chunk )
    -    chunk.web= weakref.ref(self)
    +    self.chunkSeq.append(chunk)
    +    chunk.web = weakref.ref(self)
         if chunk.name not in self.output:
             self.output[chunk.name] = []
    -        self.logger.debug( "Adding chunk {!r}".format(chunk.name) )
    +        self.logger.debug("Adding chunk %r", chunk.name)
         self.sequence += 1
    -    chunk.seq= self.sequence
    -    chunk.fullName= chunk.name
    -    self.output[chunk.name].append( chunk )
    +    chunk.seq = self.sequence
    +    chunk.fullName = chunk.name
    +    self.output[chunk.name].append(chunk)
         chunk.initial = len(self.output[chunk.name]) == 1
     
    -

    Web add an output file definition chunk (101). Used by: Web construction... (97)

    +

    Web add an output file definition chunk (103). Used by: Web construction... (99)

    -

    Web Chunk Name Resolution

    +

    Web Chunk Name Resolution

    Web Chunk name resolution has three aspects. The first is resolving elided names (those ending with ...) to their full names. The second is finding the named chunk @@ -4268,23 +4647,30 @@

    Web Chunk Name Resolution

    If a match is found, the dictionary key is the full name.
  • Otherwise, treat this as a full name.
  • -

    Web Chunk name resolution methods (102) =

    +

    Web Chunk name resolution methods (104) =

    -def fullNameFor( self, name ):
    +def fullNameFor(self, name: str) -> str:
         """Resolve "..." names into the full name."""
    -    if name in self.named: return name
    -    if name[-3:] == '...':
    -        best= [ n for n in self.named.keys()
    -            if n.startswith( name[:-3] ) ]
    -        if len(best) > 1:
    -            raise Error("Ambiguous abbreviation {!r}, matches {!r}".format( name, list(sorted(best)) ) )
    -        elif len(best) == 1:
    -            return best[0]
    -    return name
    +    if name in self.named:
    +        return name
    +    elif name.endswith('...'):
    +        best = [n
    +            for n in self.named
    +            if n.startswith(name[:-3])
    +        ]
    +        match best:
    +            case []:
    +                return name
    +            case [singleton]:
    +                return singleton
    +            case _:
    +                raise Error(f"Ambiguous abbreviation {name!r}, matches {sorted(best)!r}")
    +    else:
    +        return name
     
    -

    Web Chunk name resolution methods (102). Used by: Web class... (95)

    +

    Web Chunk name resolution methods (104). Used by: Web class... (97)

    The getchunk() method locates a named sequence of chunks by first determining the full name for the identifying string. If full name is in the named mapping, the sequence @@ -4292,23 +4678,23 @@

    Web Chunk Name Resolution

    is unresolvable.

    It might be more helpful for debugging to emit this as an error in the weave and tangle results and keep processing. This would allow an author to -catch multiple errors in a single run of pyWeb.

    -

    Web Chunk name resolution methods (103) +=

    +catch multiple errors in a single run of py-web-tool .

    +

    Web Chunk name resolution methods (105) +=

    -def getchunk( self, name ):
    +def getchunk(self, name: str) -> list[Chunk]:
         """Locate a named sequence of chunks."""
    -    nm= self.fullNameFor( name )
    +    nm = self.fullNameFor(name)
         if nm in self.named:
             return self.named[nm]
    -    raise Error( "Cannot resolve {!r} in {!r}".format(name,self.named.keys()) )
    +    raise Error(f"Cannot resolve {name!r} in {self.named.keys()!r}")
     
    -

    Web Chunk name resolution methods (103). Used by: Web class... (95)

    +

    Web Chunk name resolution methods (105). Used by: Web class... (97)

    -

    Web Cross-Reference Support

    +

    Web Cross-Reference Support

    Cross-reference support includes creating and reporting on the various cross-references available in a web. This includes creating the list of chunks that reference a given chunk; @@ -4329,51 +4715,52 @@

    Web Cross-Reference Support

    will resolve the name to which it refers.

    When the createUsedBy() method has accumulated the entire cross reference, it also assures that all chunks are used exactly once.

    -

    Web Chunk cross reference methods (104) =

    +

    Web Chunk cross reference methods (106) =

    -def createUsedBy( self ):
    +def createUsedBy(self) -> None:
         """Update every piece of a Chunk to show how the chunk is referenced.
         Each piece can then report where it's used in the web.
         """
         for aChunk in self.chunkSeq:
             #usage = (self.fullNameFor(aChunk.name), aChunk.seq)
    -        for aRefName in aChunk.genReferences( self ):
    -            for c in self.getchunk( aRefName ):
    -                c.referencedBy.append( aChunk )
    +        for aRefName in aChunk.genReferences(self):
    +            for c in self.getchunk(aRefName):
    +                c.referencedBy.append(aChunk)
                     c.refCount += 1
    -    →Web Chunk check reference counts are all one (105)
    +
    +    →Web Chunk check reference counts are all one (107)
     
    -

    Web Chunk cross reference methods (104). Used by: Web class... (95)

    +

    Web Chunk cross reference methods (106). Used by: Web class... (97)

    We verify that the reference count for a Chunk is exactly one. We don't gracefully tolerate multiple references to a Chunk or unreferenced chunks.

    -

    Web Chunk check reference counts are all one (105) =

    +

    Web Chunk check reference counts are all one (107) =

     for nm in self.no_reference():
    -    self.logger.warn( "No reference to {!r}".format(nm) )
    +    self.logger.warning("No reference to %r", nm)
     for nm in self.multi_reference():
    -    self.logger.warn( "Multiple references to {!r}".format(nm) )
    +    self.logger.warning("Multiple references to %r", nm)
     for nm in self.no_definition():
    -    self.logger.error( "No definition for {!r}".format(nm) )
    +    self.logger.error("No definition for %r", nm)
         self.errors += 1
     
    -

    Web Chunk check reference counts are all one (105). Used by: Web Chunk cross reference methods... (104)

    +

    Web Chunk check reference counts are all one (107). Used by: Web Chunk cross reference methods... (106)

    -

    The one-pass version

    +

    An alternative one-pass version of the above algorithm:

    -for nm,cl in self.named.items():
    +for nm, cl in self.named.items():
         if len(cl) > 0:
             if cl[0].refCount == 0:
    -           self.logger.warn( "No reference to {!r}".format(nm) )
    +           self.logger.warning("No reference to %r", nm)
             elif cl[0].refCount > 1:
    -           self.logger.warn( "Multiple references to {!r}".format(nm) )
    +           self.logger.warning("Multiple references to %r", nm)
         else:
    -        self.logger.error( "No definition for {!r}".format(nm) )
    +        self.logger.error("No definition for %r", nm)
     

    We use three methods to filter chunk names into the various warning categories. The no_reference list @@ -4382,46 +4769,46 @@

    Web Cross-Reference Support

    is a list of chunks defined by never referenced. The no_definition list is a list of chunks referenced but not defined.

    -

    Web Chunk cross reference methods (106) +=

    +

    Web Chunk cross reference methods (108) +=

    -def no_reference( self ):
    -    return [ nm for nm,cl in self.named.items() if len(cl)>0 and cl[0].refCount == 0 ]
    -def multi_reference( self ):
    -    return [ nm for nm,cl in self.named.items() if len(cl)>0 and cl[0].refCount > 1 ]
    -def no_definition( self ):
    -    return [ nm for nm,cl in self.named.items() if len(cl) == 0 ]
    +def no_reference(self) -> list[str]:
    +    return [nm for nm, cl in self.named.items() if len(cl)>0 and cl[0].refCount == 0]
    +def multi_reference(self) -> list[str]:
    +    return [nm for nm, cl in self.named.items() if len(cl)>0 and cl[0].refCount > 1]
    +def no_definition(self) -> list[str]:
    +    return [nm for nm, cl in self.named.items() if len(cl) == 0]
     
    -

    Web Chunk cross reference methods (106). Used by: Web class... (95)

    +

    Web Chunk cross reference methods (108). Used by: Web class... (97)

    The fileXref() method visits all named file output chunks in output and collects the sequence numbers of each section in the sequence of chunks.

    The chunkXref() method uses the same algorithm as a the fileXref() method, but applies it to the named mapping.

    -

    Web Chunk cross reference methods (107) +=

    +

    Web Chunk cross reference methods (109) +=

    -def fileXref( self ):
    -    fx= {}
    -    for f,cList in self.output.items():
    -        fx[f]= [ c.seq for c in cList ]
    +def fileXref(self) -> dict[str, list[int]]:
    +    fx = {}
    +    for f, cList in self.output.items():
    +        fx[f] = [c.seq for c in cList]
         return fx
    -def chunkXref( self ):
    -    mx= {}
    -    for n,cList in self.named.items():
    -        mx[n]= [ c.seq for c in cList ]
    +def chunkXref(self) -> dict[str, list[int]]:
    +    mx = {}
    +    for n, cList in self.named.items():
    +        mx[n] = [c.seq for c in cList]
         return mx
     
    -

    Web Chunk cross reference methods (107). Used by: Web class... (95)

    +

    Web Chunk cross reference methods (109). Used by: Web class... (97)

    The userNamesXref() method creates a mapping for each user identifier. The value for this mapping is a tuple with the chunk that defined the identifer (via a @| command), and a sequence of chunks that reference the identifier.

    For example: -{ 'Web': ( 87, (88,93,96,101,102,104) ), 'Chunk': ( 53, (54,55,56,60,57,58,59) ) }, +{'Web': (87, (88,93,96,101,102,104)), 'Chunk': (53, (54,55,56,60,57,58,59))}, shows that the identifier 'Web' is defined in chunk with a sequence number of 87, and referenced in the sequence of chunks that follow.

    @@ -4431,23 +4818,25 @@

    Web Cross-Reference Support

  • _updateUserId() searches all text commands for the identifiers and updates the Web class cross reference information.
  • -

    Web Chunk cross reference methods (108) +=

    -
    -def userNamesXref( self ):
    -    ux= {}
    -    self._gatherUserId( self.named, ux )
    -    self._gatherUserId( self.output, ux )
    -    self._updateUserId( self.named, ux )
    -    self._updateUserId( self.output, ux )
    +

    Web Chunk cross reference methods (110) +=

    +
    +def userNamesXref(self) -> dict[str, tuple[int, list[int]]]:
    +    ux: dict[str, tuple[int, list[int]]] = {}
    +    self._gatherUserId(self.named, ux)
    +    self._gatherUserId(self.output, ux)
    +    self._updateUserId(self.named, ux)
    +    self._updateUserId(self.output, ux)
         return ux
    -def _gatherUserId( self, chunkMap, ux ):
    -    →collect all user identifiers from a given map into ux (109)
    -def _updateUserId( self, chunkMap, ux ):
    -    →find user identifier usage and update ux from the given map (110)
    +
    +def _gatherUserId(self, chunkMap: dict[str, list[Chunk]], ux: dict[str, tuple[int, list[int]]]) -> None:
    +    →collect all user identifiers from a given map into ux (111)
    +
    +def _updateUserId(self, chunkMap: dict[str, list[Chunk]], ux: dict[str, tuple[int, list[int]]]) -> None:
    +    →find user identifier usage and update ux from the given map (112)
     
    -

    Web Chunk cross reference methods (108). Used by: Web class... (95)

    +

    Web Chunk cross reference methods (110). Used by: Web class... (97)

    User identifiers are collected by visiting each of the sequence of Chunks that share the @@ -4455,40 +4844,40 @@

    Web Cross-Reference Support

    by the @| command, these are seeded into the dictionary. If the chunk does not permit identifiers, it simply returns an empty list as a default action.

    -

    collect all user identifiers from a given map into ux (109) =

    +

    collect all user identifiers from a given map into ux (111) =

     for n,cList in chunkMap.items():
         for c in cList:
             for id in c.getUserIDRefs():
    -            ux[id]= ( c.seq, [] )
    +            ux[id] = (c.seq, [])
     
    -

    collect all user identifiers from a given map into ux (109). Used by: Web Chunk cross reference methods... (108)

    +

    collect all user identifiers from a given map into ux (111). Used by: Web Chunk cross reference methods... (110)

    User identifiers are cross-referenced by visiting each of the sequence of Chunks that share the same name; within each component chunk, visit each user identifier; if the Chunk class searchForRE() method matches an identifier, this is appended to the sequence of chunks that reference the original user identifier.

    -

    find user identifier usage and update ux from the given map (110) =

    +

    find user identifier usage and update ux from the given map (112) =

     # examine source for occurrences of all names in ux.keys()
     for id in ux.keys():
    -    self.logger.debug( "References to {!r}".format(id) )
    -    idpat= re.compile( r'\W{!s}\W'.format(id) )
    +    self.logger.debug("References to %r", id)
    +    idpat = re.compile(f'\\W{id}\\W')
         for n,cList in chunkMap.items():
             for c in cList:
    -            if c.seq != ux[id][0] and c.searchForRE( idpat ):
    -                ux[id][1].append( c.seq )
    +            if c.seq != ux[id][0] and c.searchForRE(idpat):
    +                ux[id][1].append(c.seq)
     
    -

    find user identifier usage and update ux from the given map (110). Used by: Web Chunk cross reference methods... (108)

    +

    find user identifier usage and update ux from the given map (112). Used by: Web Chunk cross reference methods... (110)

    -

    Loop Detection

    +

    Loop Detection

    How do we assure that the web is a proper tree and doesn't contain any loops?

    Consider this example web

    @@ -4516,7 +4905,7 @@ 

    Loop Detection

    The simple reference count will do.

    -

    Tangle and Weave Support

    +

    Tangle and Weave Support

    The language() method makes a stab at determining the output language. The determination of the language can be done a variety of ways. One is to use command line parameters, another is to use the filename @@ -4525,13 +4914,13 @@

    Tangle and Weave Support

    XML file begins with '<!', '<?' or '<H'. LaTeX files typically begin with '%' or ''. Everything else is probably RST.

    -

    Web determination of the language from the first chunk (111) =

    +

    Web determination of the language from the first chunk (113) =

    -def language( self, preferredWeaverClass=None ):
    +def language(self, preferredWeaverClass: type["Weaver"] | None = None) -> "Weaver":
         """Construct a weaver appropriate to the document's language"""
         if preferredWeaverClass:
             return preferredWeaverClass()
    -    self.logger.debug( "Picking a weaver based on first chunk {!r}".format(self.chunkSeq[0][:4]) )
    +    self.logger.debug("Picking a weaver based on first chunk %r", str(self.chunkSeq[0])[:4])
         if self.chunkSeq[0].startswith('<'):
             return HTML()
         if self.chunkSeq[0].startswith('%') or self.chunkSeq[0].startswith('\\'):
    @@ -4540,23 +4929,23 @@ 

    Tangle and Weave Support

    -

    Web determination of the language from the first chunk (111). Used by: Web class... (95)

    +

    Web determination of the language from the first chunk (113). Used by: Web class... (97)

    The tangle() method of the Web class performs the tangle() method for each Chunk of each named output file. Note that several Chunks may share the file name, requiring the file be composed of material from each Chunk, in order.

    -

    Web tangle the output files (112) =

    +

    Web tangle the output files (114) =

    -def tangle( self, aTangler ):
    +def tangle(self, aTangler: "Tangler") -> None:
         for f, c in self.output.items():
    -        with aTangler.open(f):
    +        with aTangler.open(Path(f)):
                 for p in c:
    -                p.tangle( self, aTangler )
    +                p.tangle(self, aTangler)
     
    -

    Web tangle the output files (112). Used by: Web class... (95)

    +

    Web tangle the output files (114). Used by: Web class... (97)

    The weave() method of the Web class creates the final documentation. This is done by stepping through each Chunk in sequence @@ -4571,42 +4960,44 @@

    Tangle and Weave Support

    TODO Can we refactor weaveChunk out of here entirely?
    Should it go in ReferenceCommand weave...?
    -

    Web weave the output document (113) =

    +

    Web weave the output document (115) =

    -def weave( self, aWeaver ):
    -    self.logger.debug( "Weaving file from {!r}".format(self.webFileName) )
    -    basename, _ = os.path.splitext( self.webFileName )
    -    with aWeaver.open(basename):
    +def weave(self, aWeaver: "Weaver") -> None:
    +    self.logger.debug("Weaving file from '%s'", self.web_path)
    +    if not self.web_path:
    +        raise Error("No filename supplied for weaving.")
    +    with aWeaver.open(self.web_path):
             for c in self.chunkSeq:
    -            c.weave( self, aWeaver )
    -def weaveChunk( self, name, aWeaver ):
    -    self.logger.debug( "Weaving chunk {!r}".format(name) )
    -    chunkList= self.getchunk(name)
    +            c.weave(self, aWeaver)
    +
    +def weaveChunk(self, name: str, aWeaver: "Weaver") -> None:
    +    self.logger.debug("Weaving chunk %r", name)
    +    chunkList = self.getchunk(name)
         if not chunkList:
    -        raise Error( "No Definition for {!r}".format(name) )
    -    chunkList[0].weaveReferenceTo( self, aWeaver )
    +        raise Error(f"No Definition for {name!r}")
    +    chunkList[0].weaveReferenceTo(self, aWeaver)
         for p in chunkList[1:]:
    -        aWeaver.write( aWeaver.referenceSep() )
    -        p.weaveShortReferenceTo( self, aWeaver )
    +        aWeaver.write(aWeaver.referenceSep())
    +        p.weaveShortReferenceTo(self, aWeaver)
     
    -

    Web weave the output document (113). Used by: Web class... (95)

    +

    Web weave the output document (115). Used by: Web class... (97)

    -

    The WebReader Class

    +

    The WebReader Class

    There are two forms of the constructor for a WebReader. The initial WebReader instance is created with code like the following:

    -p= WebReader()
    +p = WebReader()
     p.command = options.commandCharacter
     

    This will define the command character; usually provided as a command-line parameter to the application.

    When processing an include file (with the @i command), a child WebReader instance is created with code like the following:

    -c= WebReader( parent=parentWebReader )
    +c = WebReader(parent=parentWebReader)
     

    This will inherit the configuration from the parent WebReader. This will also include a reference from child to parent so that embedded Python expressions @@ -4652,7 +5043,7 @@

    The WebReader Class

    _source:The open source being used by load(). -fileName:is used to pass the file name to the Web instance. +filePath:is used to pass the file name to the Web instance. theWeb:is the current open Web. @@ -4667,59 +5058,66 @@

    The WebReader Class

    -

    WebReader class - parses the input file, building the Web structure (114) =

    +

    WebReader class - parses the input file, building the Web structure (116) =

     class WebReader:
         """Parse an input file, creating Chunks and Commands."""
     
    -    output_option_parser= OptionParser(
    -        OptionDef( "-start", nargs=1, default=None ),
    -        OptionDef( "-end", nargs=1, default="" ),
    -        OptionDef( "argument", nargs='*' ),
    -        )
    +    output_option_parser = OptionParser(
    +        OptionDef("-start", nargs=1, default=None),
    +        OptionDef("-end", nargs=1, default=""),
    +        OptionDef("argument", nargs='*'),
    +    )
     
    -    definition_option_parser= OptionParser(
    -        OptionDef( "-indent", nargs=0 ),
    -        OptionDef( "-noindent", nargs=0 ),
    -        OptionDef( "argument", nargs='*' ),
    -        )
    +    definition_option_parser = OptionParser(
    +        OptionDef("-indent", nargs=0),
    +        OptionDef("-noindent", nargs=0),
    +        OptionDef("argument", nargs='*'),
    +    )
     
    -    def __init__( self, parent=None ):
    -        self.logger= logging.getLogger( self.__class__.__qualname__ )
    +    # Configuration
    +    command: str
    +    permitList: list[str]
    +    base_path : Path
    +
    +    # State of the reader
    +    _source: TextIO
    +    filePath: Path
    +    theWeb: "Web"
    +    tokenizer: Tokenizer
    +    aChunk: Chunk
    +
    +    def __init__(self, parent: Optional["WebReader"] = None) -> None:
    +        self.logger = logging.getLogger(self.__class__.__qualname__)
     
             # Configuration of this reader.
    -        self.parent= parent
    +        self.parent = parent
             if self.parent:
    -            self.command= self.parent.command
    -            self.permitList= self.parent.permitList
    +            self.command = self.parent.command
    +            self.permitList = self.parent.permitList
             else: # Defaults until overridden
    -            self.command= '@'
    -            self.permitList= []
    -
    -        # Load options
    -        self._source= None
    -        self.fileName= None
    -        self.theWeb= None
    -
    -        # State of reading and parsing.
    -        self.tokenizer= None
    -        self.aChunk= None
    +            self.command = '@'
    +            self.permitList = []
     
             # Summary
    -        self.totalLines= 0
    -        self.totalFiles= 0
    -        self.errors= 0
    +        self.totalLines = 0
    +        self.totalFiles = 0
    +        self.errors = 0
     
    -        →WebReader command literals (130)
    -    def __str__( self ):
    +        →WebReader command literals (131)
    +
    +    def __str__(self) -> str:
             return self.__class__.__name__
    -    →WebReader location in the input stream (128)
    -    →WebReader load the web (129)
    -    →WebReader handle a command string (115), →(127)
    +
    +    →WebReader location in the input stream (128)
    +
    +    →WebReader load the web (130)
    +
    +    →WebReader handle a command string (117), →(127)
     
    -

    WebReader class - parses the input file, building the Web structure (114). Used by: Base Class Definitions (1)

    +

    WebReader class - parses the input file, building the Web structure (116). Used by: Base Class Definitions (1)

    Command recognition is done via a Chain of Command-like design. There are two conditions: the command string is recognized or it is not recognized. @@ -4743,40 +5141,45 @@

    The WebReader Class

    then return false. Either a subclass will handle it, or the default activity taken by load() is to treat the command a text, but also issue a warning. -

    WebReader handle a command string (115) =

    -
    -def handleCommand( self, token ):
    -    self.logger.debug( "Reading {!r}".format(token) )
    -    →major commands segment the input into separate Chunks (116)
    -    →minor commands add Commands to the current Chunk (121)
    -    elif token[:2] in (self.cmdlcurl,self.cmdlbrak):
    -        # These should have been consumed as part of @o and @d parsing
    -        self.logger.error( "Extra {!r} (possibly missing chunk name) near {!r}".format(token, self.location()) )
    -        self.errors += 1
    -    else:
    -        return None # did not recogize the command
    -    return True # did recognize the command
    -
    - -
    -

    WebReader handle a command string (115). Used by: WebReader class... (114)

    -
    -

    The following sequence of if-elif statements identifies -the structural commands that partition the input into separate Chunks.

    -

    major commands segment the input into separate Chunks (116) =

    -
    -if token[:2] == self.cmdo:
    -    →start an OutputChunk, adding it to the web (117)
    -elif token[:2] == self.cmdd:
    -    →start a NamedChunk or NamedDocumentChunk, adding it to the web (118)
    -elif token[:2] == self.cmdi:
    -    →import another file (119)
    -elif token[:2] in (self.cmdrcurl,self.cmdrbrak):
    -    →finish a chunk, start a new Chunk adding it to the web (120)
    -
    - -
    -

    major commands segment the input into separate Chunks (116). Used by: WebReader handle a command... (115)

    +

    WebReader handle a command string (117) =

    +
    +def handleCommand(self, token: str) -> bool:
    +    self.logger.debug("Reading %r", token)
    +
    +    match token[:2]:
    +        case self.cmdo:
    +            →start an OutputChunk, adding it to the web (118)
    +        case self.cmdd:
    +            →start a NamedChunk or NamedDocumentChunk, adding it to the web (119)
    +        case self.cmdi:
    +            →include another file (120)
    +        case self.cmdrcurl | self.cmdrbrak:
    +            →finish a chunk, start a new Chunk adding it to the web (121)
    +        case self.cmdpipe:
    +            →assign user identifiers to the current chunk (122)
    +        case self.cmdf:
    +            self.aChunk.append(FileXrefCommand(self.tokenizer.lineNumber))
    +        case self.cmdm:
    +            self.aChunk.append(MacroXrefCommand(self.tokenizer.lineNumber))
    +        case self.cmdu:
    +            self.aChunk.append(UserIdXrefCommand(self.tokenizer.lineNumber))
    +        case self.cmdlangl:
    +            →add a reference command to the current chunk (123)
    +        case self.cmdlexpr:
    +            →add an expression command to the current chunk (125)
    +        case self.cmdcmd:
    +            →double at-sign replacement, append this character to previous TextCommand (126)
    +        case self.cmdlcurl | self.cmdlbrak:
    +            # These should have been consumed as part of @o and @d parsing
    +            self.logger.error("Extra %r (possibly missing chunk name) near %r", token, self.location())
    +            self.errors += 1
    +        case _:
    +            return False  # did not recogize the command
    +    return True  # did recognize the command
    +
    + +
    +

    WebReader handle a command string (117). Used by: WebReader class... (116)

    An output chunk has the form @o name @{ content @}. We use the first two tokens to name the OutputChunk. We simply expect @@ -4784,23 +5187,24 @@

    The WebReader Class

    to this chunk while waiting for the final @} token to end the chunk.

    We'll use an OptionParser to locate the optional parameters. This will then let us build an appropriate instance of OutputChunk.

    -

    With some small additional changes, we could use OutputChunk( **options ).

    -

    start an OutputChunk, adding it to the web (117) =

    -
    -args= next(self.tokenizer)
    -self.expect( (self.cmdlcurl,) )
    -options= self.output_option_parser.parse( args )
    -self.aChunk= OutputChunk( name=options['argument'],
    -        comment_start= options.get('start',None),
    -        comment_end= options.get('end',""),
    -        )
    -self.aChunk.fileName= self.fileName
    -self.aChunk.webAdd( self.theWeb )
    +

    With some small additional changes, we could use OutputChunk(**options).

    +

    start an OutputChunk, adding it to the web (118) =

    +
    +args = next(self.tokenizer)
    +self.expect((self.cmdlcurl,))
    +options = self.output_option_parser.parse(args)
    +self.aChunk = OutputChunk(
    +    name=' '.join(options['argument']),
    +    comment_start=''.join(options.get('start', "# ")),
    +    comment_end=''.join(options.get('end', "")),
    +)
    +self.aChunk.filePath = self.filePath
    +self.aChunk.webAdd(self.theWeb)
     # capture an OutputChunk up to @}
     
    -

    start an OutputChunk, adding it to the web (117). Used by: major commands... (116)

    +

    start an OutputChunk, adding it to the web (118). Used by: WebReader handle a command... (117)

    A named chunk has the form @d name @{ content @} for code and @d name @[ content @] for document source. @@ -4815,34 +5219,33 @@

    The WebReader Class

    Then we can use options to create an appropriate subclass of NamedChunk.

    If "-indent" is in options, this is the default. If both are in the options, we can provide a warning, I guess.

    -
    -TODO Add a warning for conflicting options.
    -

    start a NamedChunk or NamedDocumentChunk, adding it to the web (118) =

    +

    TODO: Add a warning for conflicting options.

    +

    start a NamedChunk or NamedDocumentChunk, adding it to the web (119) =

    -args= next(self.tokenizer)
    -brack= self.expect( (self.cmdlcurl,self.cmdlbrak) )
    -options= self.output_option_parser.parse( args )
    -name=options['argument']
    +args = next(self.tokenizer)
    +brack = self.expect((self.cmdlcurl,self.cmdlbrak))
    +options = self.output_option_parser.parse(args)
    +name = ' '.join(options['argument'])
     
     if brack == self.cmdlbrak:
    -    self.aChunk= NamedDocumentChunk( name )
    +    self.aChunk = NamedDocumentChunk(name)
     elif brack == self.cmdlcurl:
         if '-noindent' in options:
    -        self.aChunk= NamedChunk_Noindent( name )
    +        self.aChunk = NamedChunk_Noindent(name)
         else:
    -        self.aChunk= NamedChunk( name )
    +        self.aChunk = NamedChunk(name)
     elif brack == None:
         pass # Error noted by expect()
     else:
    -    raise Error( "Design Error" )
    +    raise Error("Design Error")
     
    -self.aChunk.fileName= self.fileName
    -self.aChunk.webAdd( self.theWeb )
    +self.aChunk.filePath = self.filePath
    +self.aChunk.webAdd(self.theWeb)
     # capture a NamedChunk up to @} or @]
     
    -

    start a NamedChunk or NamedDocumentChunk, adding it to the web (118). Used by: major commands... (116)

    +

    start a NamedChunk or NamedDocumentChunk, adding it to the web (119). Used by: WebReader handle a command... (117)

    An import command has the unusual form of @i name, with no trailing separator. When we encounter the @i token, the next token will start with the @@ -4861,41 +5264,37 @@

    The WebReader Class

    can be set to permit failure; this allows a .w to include a file that does not yet exist.

    The primary use case for this feature is when weaving test output. -The first pass of pyWeb tangles the program source files; they are -then run to create test output; the second pass of pyWeb weaves this +The first pass of py-web-tool tangles the program source files; they are +then run to create test output; the second pass of py-web-tool weaves this test output into the final document via the @i command.

    -

    import another file (119) =

    +

    include another file (120) =

    -incFile= next(self.tokenizer).strip()
    +incPath = Path(next(self.tokenizer).strip())
     try:
    -    self.logger.info( "Including {!r}".format(incFile) )
    -    include= WebReader( parent=self )
    -    include.load( self.theWeb, incFile )
    +    include = WebReader(parent=self)
    +    if not incPath.is_absolute():
    +        incPath = self.base_path / incPath
    +    self.logger.info("Including '%s'", incPath)
    +    include.load(self.theWeb, incPath)
         self.totalLines += include.tokenizer.lineNumber
         self.totalFiles += include.totalFiles
         if include.errors:
             self.errors += include.errors
    -        self.logger.error(
    -            "Errors in included file {!s}, output is incomplete.".format(
    -            incFile) )
    +        self.logger.error("Errors in included file '%s', output is incomplete.", incPath)
     except Error as e:
    -    self.logger.error(
    -        "Problems with included file {!s}, output is incomplete.".format(
    -        incFile) )
    +    self.logger.error("Problems with included file '%s', output is incomplete.", incPath)
         self.errors += 1
     except IOError as e:
    -    self.logger.error(
    -        "Problems with included file {!s}, output is incomplete.".format(
    -        incFile) )
    +    self.logger.error("Problems finding included file '%s', output is incomplete.", incPath)
         # Discretionary -- sometimes we want to continue
         if self.cmdi in self.permitList: pass
    -    else: raise # TODO: Seems heavy-handed
    -self.aChunk= Chunk()
    -self.aChunk.webAdd( self.theWeb )
    +    else: raise  # Seems heavy-handed, but, the file wasn't found!
    +self.aChunk = Chunk()
    +self.aChunk.webAdd(self.theWeb)
     
    -

    import another file (119). Used by: major commands... (116)

    +

    include another file (120). Used by: WebReader handle a command... (117)

    When a @} or @] are found, this finishes a named chunk. The next text is therefore part of an anonymous chunk.

    @@ -4905,38 +5304,17 @@

    The WebReader Class

    needed for each Chunk subclass that indicated if a trailing bracket was necessary. For the base Chunk class, this would be false, but for all other subclasses of Chunk, this would be true.

    -

    finish a chunk, start a new Chunk adding it to the web (120) =

    +

    finish a chunk, start a new Chunk adding it to the web (121) =

    -self.aChunk= Chunk()
    -self.aChunk.webAdd( self.theWeb )
    +self.aChunk = Chunk()
    +self.aChunk.webAdd(self.theWeb)
     
    -

    finish a chunk, start a new Chunk adding it to the web (120). Used by: major commands... (116)

    +

    finish a chunk, start a new Chunk adding it to the web (121). Used by: WebReader handle a command... (117)

    The following sequence of elif statements identifies the minor commands that add Command instances to the current open Chunk.

    -

    minor commands add Commands to the current Chunk (121) =

    -
    -elif token[:2] == self.cmdpipe:
    -    →assign user identifiers to the current chunk (122)
    -elif token[:2] == self.cmdf:
    -    self.aChunk.append( FileXrefCommand(self.tokenizer.lineNumber) )
    -elif token[:2] == self.cmdm:
    -    self.aChunk.append( MacroXrefCommand(self.tokenizer.lineNumber) )
    -elif token[:2] == self.cmdu:
    -    self.aChunk.append( UserIdXrefCommand(self.tokenizer.lineNumber) )
    -elif token[:2] == self.cmdlangl:
    -    →add a reference command to the current chunk (123)
    -elif token[:2] == self.cmdlexpr:
    -    →add an expression command to the current chunk (125)
    -elif token[:2] == self.cmdcmd:
    -    →double at-sign replacement, append this character to previous TextCommand (126)
    -
    - -
    -

    minor commands add Commands to the current Chunk (121). Used by: WebReader handle a command... (115)

    -

    User identifiers occur after a @| in a NamedChunk.

    Note that no check is made to assure that the previous Chunk was indeed a named chunk or output chunk started with @{. @@ -4946,34 +5324,34 @@

    The WebReader Class

    OutputChunk class, this would be true.

    User identifiers are name references at the end of a NamedChunk These are accumulated and expanded by @u reference

    -

    assign user identifiers to the current chunk (122) =

    +

    assign user identifiers to the current chunk (122) =

     try:
    -    self.aChunk.setUserIDRefs( next(self.tokenizer).strip() )
    +    self.aChunk.setUserIDRefs(next(self.tokenizer).strip())
     except AttributeError:
         # Out of place @| user identifier command
    -    self.logger.error( "Unexpected references near {!s}: {!s}".format(self.location(),token) )
    +    self.logger.error("Unexpected references near %r: %r", self.location(), token)
         self.errors += 1
     
    -

    assign user identifiers to the current chunk (122). Used by: minor commands... (121)

    +

    assign user identifiers to the current chunk (122). Used by: WebReader handle a command... (117)

    A reference command has the form @<name@>. We accept three tokens from the input, the middle token is the referenced name.

    -

    add a reference command to the current chunk (123) =

    +

    add a reference command to the current chunk (123) =

     # get the name, introduce into the named Chunk dictionary
    -expand= next(self.tokenizer).strip()
    -closing= self.expect( (self.cmdrangl,) )
    -self.theWeb.addDefName( expand )
    -self.aChunk.append( ReferenceCommand( expand, self.tokenizer.lineNumber ) )
    -self.aChunk.appendText( "", self.tokenizer.lineNumber ) # to collect following text
    -self.logger.debug( "Reading {!r} {!r}".format(expand, closing) )
    +expand = next(self.tokenizer).strip()
    +closing = self.expect((self.cmdrangl,))
    +self.theWeb.addDefName(expand)
    +self.aChunk.append(ReferenceCommand(expand, self.tokenizer.lineNumber))
    +self.aChunk.appendText("", self.tokenizer.lineNumber) # to collect following text
    +self.logger.debug("Reading %r %r", expand, closing)
     
    -

    add a reference command to the current chunk (123). Used by: minor commands... (121)

    +

    add a reference command to the current chunk (123). Used by: WebReader handle a command... (117)

    An expression command has the form @(Python Expression@). We accept three @@ -4983,15 +5361,15 @@

    The WebReader Class

  • Deferred Execution. This requires definition of a new subclass of Command, ExpressionCommand, and appends it into the current Chunk. At weave and tangle time, this expression is evaluated. The insert might look something like this: -aChunk.append( ExpressionCommand(expression, self.tokenizer.lineNumber) ).
  • +aChunk.append(ExpressionCommand(expression, self.tokenizer.lineNumber)).
  • Immediate Execution. This simply creates a context and evaluates the Python expression. The output from the expression becomes a TextCommand, and is append to the current Chunk.
  • We use the Immediate Execution semantics.

    -

    Note that we've removed the blanket os. We only provide os.path. -An os.getcwd() must be changed to os.path.realpath('.').

    -

    Imports (124) +=

    +

    Note that we've removed the blanket os. We provide os.path library. +An os.getcwd() could be changed to os.path.realpath('.').

    +

    Imports (124) +=

     import builtins
     import sys
    @@ -4999,40 +5377,49 @@ 

    The WebReader Class

    -

    Imports (124). Used by: pyweb.py (153)

    +

    Imports (124). Used by: pyweb.py (155)

    -

    add an expression command to the current chunk (125) =

    +

    add an expression command to the current chunk (125) =

     # get the Python expression, create the expression result
    -expression= next(self.tokenizer)
    -self.expect( (self.cmdrexpr,) )
    +expression = next(self.tokenizer)
    +self.expect((self.cmdrexpr,))
     try:
         # Build Context
    -    safe= types.SimpleNamespace( **dict( (name,obj)
    +    # **TODO:** Parts of this are static.
    +    dangerous = {
    +        'breakpoint', 'compile', 'eval', 'exec', 'execfile', 'globals', 'help', 'input',
    +        'memoryview', 'open', 'print', 'super', '__import__'
    +    }
    +    safe = types.SimpleNamespace(**dict(
    +        (name, obj)
             for name,obj in builtins.__dict__.items()
    -        if name not in ('eval', 'exec', 'open', '__import__')))
    -    globals= dict(
    -        __builtins__= safe,
    -        os= types.SimpleNamespace(path=os.path),
    -        datetime= datetime,
    -        platform= platform,
    -        theLocation= self.location(),
    -        theWebReader= self,
    -        theFile= self.theWeb.webFileName,
    -        thisApplication= sys.argv[0],
    -        __version__= __version__,
    +        if name not in dangerous
    +    ))
    +    globals = dict(
    +        __builtins__=safe,
    +        os=types.SimpleNamespace(path=os.path, getcwd=os.getcwd, name=os.name),
    +        time=time,
    +        datetime=datetime,
    +        platform=platform,
    +        theLocation=str(self.location()),
    +        theWebReader=self,
    +        theFile=self.theWeb.web_path,
    +        thisApplication=sys.argv[0],
    +        __version__=__version__,  # Legacy compatibility. Deprecated.
    +        version=__version__,
             )
         # Evaluate
    -    result= str(eval(expression, globals))
    -except Exception as e:
    -    self.logger.error( 'Failure to process {!r}: result is {!r}'.format(expression, e) )
    +    result = str(eval(expression, globals))
    +except Exception as exc:
    +    self.logger.error('Failure to process %r: result is %r', expression, exc)
         self.errors += 1
    -    result= "@({!r}: Error {!r}@)".format(expression, e)
    -self.aChunk.appendText( result, self.tokenizer.lineNumber )
    +    result = f"@({expression!r}: Error {exc!r}@)"
    +self.aChunk.appendText(result, self.tokenizer.lineNumber)
     
    -

    add an expression command to the current chunk (125). Used by: minor commands... (121)

    +

    add an expression command to the current chunk (125). Used by: WebReader handle a command... (117)

    A double command sequence ('@@', when the command is an '@') has the usual meaning of '@' in the input stream. We do this via @@ -5042,50 +5429,50 @@

    The WebReader Class

    We replace with '@' here and now! This is put this at the end of the previous chunk. And we make sure the next chunk will be appended to this so that it's largely seamless.

    -

    double at-sign replacement, append this character to previous TextCommand (126) =

    +

    double at-sign replacement, append this character to previous TextCommand (126) =

    -self.aChunk.appendText( self.command, self.tokenizer.lineNumber )
    +self.aChunk.appendText(self.command, self.tokenizer.lineNumber)
     
    -

    double at-sign replacement, append this character to previous TextCommand (126). Used by: minor commands... (121)

    +

    double at-sign replacement, append this character to previous TextCommand (126). Used by: WebReader handle a command... (117)

    The expect() method examines the next token to see if it is the expected item. '\n' are absorbed. If this is not found, a standard type of error message is raised. This is used by handleCommand().

    -

    WebReader handle a command string (127) +=

    +

    WebReader handle a command string (127) +=

    -def expect( self, tokens ):
    +def expect(self, tokens: Iterable[str]) -> str | None:
         try:
    -        t= next(self.tokenizer)
    +        t = next(self.tokenizer)
             while t == '\n':
    -            t= next(self.tokenizer)
    +            t = next(self.tokenizer)
         except StopIteration:
    -        self.logger.error( "At {!r}: end of input, {!r} not found".format(self.location(),tokens) )
    +        self.logger.error("At %r: end of input, %r not found", self.location(), tokens)
             self.errors += 1
    -        return
    +        return None
         if t not in tokens:
    -        self.logger.error( "At {!r}: expected {!r}, found {!r}".format(self.location(),tokens,t) )
    +        self.logger.error("At %r: expected %r, found %r", self.location(), tokens, t)
             self.errors += 1
    -        return
    +        return None
         return t
     
    -

    WebReader handle a command string (127). Used by: WebReader class... (114)

    +

    WebReader handle a command string (127). Used by: WebReader class... (116)

    The location() provides the file name and line number. This allows error messages as well as tangled or woven output to correctly reference the original input files.

    -

    WebReader location in the input stream (128) =

    +

    WebReader location in the input stream (128) =

    -def location( self ):
    -    return (self.fileName, self.tokenizer.lineNumber+1)
    +def location(self) -> tuple[str, int]:
    +    return (str(self.filePath), self.tokenizer.lineNumber+1)
     
    -

    WebReader location in the input stream (128). Used by: WebReader class... (114)

    +

    WebReader location in the input stream (128). Used by: WebReader class... (116)

    The load() method reads the entire input file as a sequence of tokens, split up by the Tokenizer. Each token that appears @@ -5095,82 +5482,95 @@

    The WebReader Class

    was unknown, and we write a warning but treat it as text.

    The load() method is used recursively to handle the @i command. The issue is that it's always loading a single top-level web.

    -

    WebReader load the web (129) =

    +

    Imports (129) +=

    +
    +from typing import TextIO
    +
    + +
    +

    Imports (129). Used by: pyweb.py (155)

    +
    +

    WebReader load the web (130) =

    -def load( self, web, filename, source=None ):
    -    self.theWeb= web
    -    self.fileName= filename
    +def load(self, web: "Web", filepath: Path, source: TextIO | None = None) -> "WebReader":
    +    self.theWeb = web
    +    self.filePath = filepath
    +    self.base_path = self.filePath.parent
     
    -    # Only set the a web filename once using the first file.
    -    # This should be a setter property of the web.
    -    if self.theWeb.webFileName is None:
    -        self.theWeb.webFileName= self.fileName
    +    # Only set the a web's filename once using the first file.
    +    # **TODO:** this should be a setter property of the web.
    +    if self.theWeb.web_path is None:
    +        self.theWeb.web_path = self.filePath
     
         if source:
    -        self._source= source
    +        self._source = source
             self.parse_source()
         else:
    -        with open( self.fileName, "r" ) as self._source:
    +        with self.filePath.open() as self._source:
                 self.parse_source()
    +    return self
     
    -def parse_source( self ):
    -        self.tokenizer= Tokenizer( self._source, self.command )
    -        self.totalFiles += 1
    +def parse_source(self) -> None:
    +    self.tokenizer = Tokenizer(self._source, self.command)
    +    self.totalFiles += 1
     
    -        self.aChunk= Chunk() # Initial anonymous chunk of text.
    -        self.aChunk.webAdd( self.theWeb )
    +    self.aChunk = Chunk() # Initial anonymous chunk of text.
    +    self.aChunk.webAdd(self.theWeb)
     
    -        for token in self.tokenizer:
    -            if len(token) >= 2 and token.startswith(self.command):
    -                if self.handleCommand( token ):
    -                    continue
    -                else:
    -                    self.logger.warn( 'Unknown @-command in input: {!r}'.format(token) )
    -                    self.aChunk.appendText( token, self.tokenizer.lineNumber )
    -            elif token:
    -                # Accumulate a non-empty block of text in the current chunk.
    -                self.aChunk.appendText( token, self.tokenizer.lineNumber )
    +    for token in self.tokenizer:
    +        if len(token) >= 2 and token.startswith(self.command):
    +            if self.handleCommand(token):
    +                continue
    +            else:
    +                self.logger.error('Unknown @-command in input: %r', token)
    +                self.aChunk.appendText(token, self.tokenizer.lineNumber)
    +        elif token:
    +            # Accumulate a non-empty block of text in the current chunk.
    +            self.aChunk.appendText(token, self.tokenizer.lineNumber)
    +        else:
    +            # Whitespace
    +            pass
     
    -

    WebReader load the web (129). Used by: WebReader class... (114)

    +

    WebReader load the web (130). Used by: WebReader class... (116)

    The command character can be changed to permit some flexibility when working with languages that make extensive use of the @ symbol, i.e., PERL. The initialization of the WebReader is based on the selected command character.

    -

    WebReader command literals (130) =

    +

    WebReader command literals (131) =

     # Structural ("major") commands
    -self.cmdo= self.command+'o'
    -self.cmdd= self.command+'d'
    -self.cmdlcurl= self.command+'{'
    -self.cmdrcurl= self.command+'}'
    -self.cmdlbrak= self.command+'['
    -self.cmdrbrak= self.command+']'
    -self.cmdi= self.command+'i'
    +self.cmdo = self.command+'o'
    +self.cmdd = self.command+'d'
    +self.cmdlcurl = self.command+'{'
    +self.cmdrcurl = self.command+'}'
    +self.cmdlbrak = self.command+'['
    +self.cmdrbrak = self.command+']'
    +self.cmdi = self.command+'i'
     
     # Inline ("minor") commands
    -self.cmdlangl= self.command+'<'
    -self.cmdrangl= self.command+'>'
    -self.cmdpipe= self.command+'|'
    -self.cmdlexpr= self.command+'('
    -self.cmdrexpr= self.command+')'
    -self.cmdcmd= self.command+self.command
    +self.cmdlangl = self.command+'<'
    +self.cmdrangl = self.command+'>'
    +self.cmdpipe = self.command+'|'
    +self.cmdlexpr = self.command+'('
    +self.cmdrexpr = self.command+')'
    +self.cmdcmd = self.command+self.command
     
     # Content "minor" commands
    -self.cmdf= self.command+'f'
    -self.cmdm= self.command+'m'
    -self.cmdu= self.command+'u'
    +self.cmdf = self.command+'f'
    +self.cmdm = self.command+'m'
    +self.cmdu = self.command+'u'
     
    -

    WebReader command literals (130). Used by: WebReader class... (114)

    +

    WebReader command literals (131). Used by: WebReader class... (116)

    -

    The Tokenizer Class

    +

    The Tokenizer Class

    The WebReader requires a tokenizer. The tokenizer breaks the input text into a stream of tokens. There are two broad classes of tokens:

      @@ -5198,39 +5598,42 @@

      The Tokenizer Class

      We can safely filter these via a generator expression.

      The tokenizer counts newline characters for us, so that error messages can include a line number. Also, we can tangle comments into the file that include line numbers.

      -

      Since the tokenizer is a proper iterator, we can use tokens= iter(Tokenizer(source)) +

      Since the tokenizer is a proper iterator, we can use tokens = iter(Tokenizer(source)) and next(tokens) to step through the sequence of tokens until we raise a StopIteration exception.

      -

      Imports (131) +=

      +

      Imports (132) +=

       import re
      +from collections.abc import Iterator, Iterable
       
      -

      Imports (131). Used by: pyweb.py (153)

      +

      Imports (132). Used by: pyweb.py (155)

      -

      Tokenizer class - breaks input into tokens (132) =

      +

      Tokenizer class - breaks input into tokens (133) =

      -class Tokenizer:
      -    def __init__( self, stream, command_char='@' ):
      -        self.command= command_char
      -        self.parsePat= re.compile( r'({!s}.|\n)'.format(self.command) )
      -        self.token_iter= (t for t in self.parsePat.split( stream.read() ) if len(t) != 0)
      -        self.lineNumber= 0
      -    def __next__( self ):
      -        token= next(self.token_iter)
      +class Tokenizer(Iterator[str]):
      +    def __init__(self, stream: TextIO, command_char: str='@') -> None:
      +        self.command = command_char
      +        self.parsePat = re.compile(f'({self.command}.|\\n)')
      +        self.token_iter = (t for t in self.parsePat.split(stream.read()) if len(t) != 0)
      +        self.lineNumber = 0
      +
      +    def __next__(self) -> str:
      +        token = next(self.token_iter)
               self.lineNumber += token.count('\n')
               return token
      -    def __iter__( self ):
      +
      +    def __iter__(self) -> Iterator[str]:
               return self
       
      -

      Tokenizer class - breaks input into tokens (132). Used by: Base Class Definitions (1)

      +

      Tokenizer class - breaks input into tokens (133). Used by: Base Class Definitions (1)

    -

    The Option Parser Class

    +

    The Option Parser Class

    For some commands (@d and @o) we have options as well as the chunk name or file name. This roughly parallels the way Tcl or the shell works.

    The two examples are

    @@ -5248,62 +5651,75 @@

    The Option Parser Class

    To handle this, we have a separate lexical scanner and parser for these two commands.

    -

    Imports (133) +=

    +

    Imports (134) +=

     import shlex
     
    -

    Imports (133). Used by: pyweb.py (153)

    +

    Imports (134). Used by: pyweb.py (155)

    Here's how we can define an option.

     OptionParser(
    -    OptionDef( "-start", nargs=1, default=None ),
    -    OptionDef( "-end", nargs=1, default="" ),
    -    OptionDef( "-indent", nargs=0 ), # A default
    -    OptionDef( "-noindent", nargs=0 ),
    -    OptionDef( "argument", nargs='*' ),
    +    OptionDef("-start", nargs=1, default=None),
    +    OptionDef("-end", nargs=1, default=""),
    +    OptionDef("-indent", nargs=0), # A default
    +    OptionDef("-noindent", nargs=0),
    +    OptionDef("argument", nargs='*'),
         )
     

    The idea is to parallel argparse.add_argument() syntax.

    -

    Option Parser class - locates optional values on commands (134) =

    +

    Option Parser class - locates optional values on commands (135) =

    +
    +class ParseError(Exception): pass
    +
    + +
    +

    Option Parser class - locates optional values on commands (135). Used by: Base Class Definitions (1)

    +
    +

    Option Parser class - locates optional values on commands (136) +=

     class OptionDef:
    -    def __init__( self, name, **kw ):
    -        self.name= name
    -        self.__dict__.update( kw )
    +    def __init__(self, name: str, **kw: Any) -> None:
    +        self.name = name
    +        self.__dict__.update(kw)
     
    -

    Option Parser class - locates optional values on commands (134). Used by: Base Class Definitions (1)

    +

    Option Parser class - locates optional values on commands (136). Used by: Base Class Definitions (1)

    The parser breaks the text into words using shelex rules. It then steps through the words, accumulating the options and the final argument value.

    -

    Option Parser class - locates optional values on commands (135) +=

    +

    Option Parser class - locates optional values on commands (137) +=

     class OptionParser:
    -    def __init__( self, *arg_defs ):
    -        self.args= dict( (arg.name,arg) for arg in arg_defs )
    -        self.trailers= [k for k in self.args.keys() if not k.startswith('-')]
    -    def parse( self, text ):
    +    def __init__(self, *arg_defs: Any) -> None:
    +        self.args = dict((arg.name, arg) for arg in arg_defs)
    +        self.trailers = [k for k in self.args.keys() if not k.startswith('-')]
    +
    +    def parse(self, text: str) -> dict[str, list[str]]:
             try:
    -            word_iter= iter(shlex.split(text))
    +            word_iter = iter(shlex.split(text))
             except ValueError as e:
    -            raise Error( "Error parsing options in {!r}".format(text) )
    -        options = dict( s for s in self._group( word_iter ) )
    +            raise Error(f"Error parsing options in {text!r}")
    +        options = dict(self._group(word_iter))
             return options
    -    def _group( self, word_iter ):
    -        option, value, final= None, [], []
    +
    +    def _group(self, word_iter: Iterator[str]) -> Iterator[tuple[str, list[str]]]:
    +        option: str | None
    +        value: list[str]
    +        final: list[str]
    +        option, value, final = None, [], []
             for word in word_iter:
                 if word == '--':
                     if option:
                         yield option, value
                     try:
    -                    final= [next(word_iter)]
    +                    final = [next(word_iter)]
                     except StopIteration:
    -                    final= [] # Special case of '--' at the end.
    +                    final = []  # Special case of '--' at the end.
                     break
                 elif word.startswith('-'):
                     if word in self.args:
    @@ -5311,26 +5727,26 @@ 

    The Option Parser Class

    yield option, value option, value = word, [] else: - raise ParseError( "Unknown option {0}".format(word) ) + raise ParseError(f"Unknown option {word!r}") else: if option: if self.args[option].nargs == len(value): yield option, value - final= [word] + final = [word] break else: - value.append( word ) + value.append(word) else: - final= [word] + final = [word] break # In principle, we step through the trailers based on nargs counts. for word in word_iter: - final.append( word ) - yield self.trailers[0], " ".join(final) + final.append(word) + yield self.trailers[0], final
    -

    Option Parser class - locates optional values on commands (135). Used by: Base Class Definitions (1)

    +

    Option Parser class - locates optional values on commands (137). Used by: Base Class Definitions (1)

    In principle, we step through the trailers based on nargs counts. Since we only ever have the one trailer, we skate by.

    @@ -5338,18 +5754,18 @@

    The Option Parser Class

    First, we have to use an OrderedDict instead of a dict.

    Then we'd have a loop something like this. (Untested, incomplete, just hand-waving.)

    -trailers= self.trailers[:] # Stateful shallow copy
    +trailers = self.trailers[:] # Stateful shallow copy
     for word in word_iter:
    -    if len(final) == trailers[-1].nargs: # nargs=='*' vs. nargs=int??
    +    if len(final) == trailers[-1].nargs:  # nargs=='*' vs. nargs=int??
             yield trailers[0], " ".join(final)
    -        final= 0
    +        final = 0
             trailers.pop(0)
     yield trailers[0], " ".join(final)
     
    -

    Action Class Hierarchy

    +

    Action Class Hierarchy

    This application performs three major actions: loading the document web, weaving and tangling. Generally, the use case is to perform a load, weave and tangle. However, a less common use case @@ -5360,45 +5776,45 @@

    Action Class Hierarchy

    the tangle pass, doing the weave action.

    This two pass action might be embedded in the following type of Python program.

    -import pyweb, os, runpy, sys
    -pyweb.tangle( "source.w" )
    -with open("source.log", "w") as target:
    -    sys.stdout= target
    -    runpy.run_path( 'source.py' )
    -    sys.stdout= sys.__stdout__
    -pyweb.weave( "source.w" )
    -
    -

    The first step runs pyWeb, excluding the final weaving pass. The second +import pyweb, os, runpy, sys, pathlib, contextlib +log = pathlib.Path("source.log") +pyweb.tangle("source.w") +with log.open("w") as target: + with contextlib.redirect_stdout(target): + runpy.run_path('source.py') +pyweb.weave("source.w") + +

    The first step runs py-web-tool , excluding the final weaving pass. The second step runs the tangled program, source.py, and produces test results in -some log file, source.log. The third step runs pyWeb excluding the +some log file, source.log. The third step runs py-web-tool excluding the tangle pass. This produces a final document that includes the source.log test results.

    To accomplish this, we provide a class hierarchy that defines the various -actions of the pyWeb application. This class hierarchy defines an extensible set of +actions of the py-web-tool application. This class hierarchy defines an extensible set of fundamental actions. This gives us the flexibility to create a simple sequence of actions and execute any combination of these. It eliminates the need for a forest of if-statements to determine precisely what will be done.

    Each action has the potential to update the state of the overall application. A partner with this command hierarchy is the Application class that defines the application options, inputs and results.

    -

    Action class hierarchy - used to describe basic actions of the application (136) =

    +

    Action class hierarchy - used to describe actions of the application (138) =

    -→Action superclass has common features of all actions (137)
    -→ActionSequence subclass that holds a sequence of other actions (140)
    -→WeaveAction subclass initiates the weave action (144)
    -→TangleAction subclass initiates the tangle action (147)
    -→LoadAction subclass loads the document web (150)
    +→Action superclass has common features of all actions (139)
    +→ActionSequence subclass that holds a sequence of other actions (142)
    +→WeaveAction subclass initiates the weave action (146)
    +→TangleAction subclass initiates the tangle action (149)
    +→LoadAction subclass loads the document web (152)
     
    -

    Action class hierarchy - used to describe basic actions of the application (136). Used by: Base Class Definitions (1)

    +

    Action class hierarchy - used to describe actions of the application (138). Used by: Base Class Definitions (1)

    -

    Action Class

    -

    The Action class embodies the basic operations of pyWeb. +

    Action Class

    +

    The Action class embodies the basic operations of py-web-tool . The intent of this hierarchy is to both provide an easily expanded method of adding new actions, but an easily specified list of actions for a particular -run of pyWeb.

    +run of py-web-tool .

    The overall process of the application is defined by an instance of Action. This instance may be the WeaveAction instance, the TangleAction instance or a ActionSequence instance.

    @@ -5408,8 +5824,8 @@

    Action Class

    that is a macro and does both tangling and weaving, an instance that excludes tangling, and an instance that excludes weaving. These correspond to the command-line options.

    -anOp= SomeAction( parameters )
    -anOp.options= argparse.Namespace
    +anOp = SomeAction(parameters)
    +anOp.options = argparse.Namespace
     anOp.web = Current web
     anOp()
     
    @@ -5431,55 +5847,59 @@

    Action Class

    !start:
    The time at which the action started.
    -

    Action superclass has common features of all actions (137) =

    +

    Action superclass has common features of all actions (139) =

     class Action:
         """An action performed by pyWeb."""
    -    def __init__( self, name ):
    -        self.name= name
    -        self.web= None
    -        self.options= None
    -        self.start= None
    -        self.logger= logging.getLogger( self.__class__.__qualname__ )
    -    def __str__( self ):
    -        return "{!s} [{!s}]".format( self.name, self.web )
    -    →Action call method actually does the real work (138)
    -    →Action final summary of what was done (139)
    +    options : argparse.Namespace
    +    web : "Web"
    +    def __init__(self, name: str) -> None:
    +        self.name = name
    +        self.start: float | None = None
    +        self.logger = logging.getLogger(self.__class__.__qualname__)
    +
    +    def __str__(self) -> str:
    +        return f"{self.name!s} [{self.web!s}]"
    +
    +    →Action call method actually does the real work (140)
    +
    +    →Action final summary of what was done (141)
     
    -

    Action superclass has common features of all actions (137). Used by: Action class hierarchy... (136)

    +

    Action superclass has common features of all actions (139). Used by: Action class hierarchy... (138)

    The __call__() method does the real work of the action. For the superclass, it merely logs a message. This is overridden by a subclass.

    -

    Action call method actually does the real work (138) =

    +

    Action call method actually does the real work (140) =

    -def __call__( self ):
    -    self.logger.info( "Starting {!s}".format(self.name) )
    -    self.start= time.process_time()
    +def __call__(self) -> None:
    +    self.logger.info("Starting %s", self.name)
    +    self.start = time.process_time()
     
    -

    Action call method actually does the real work (138). Used by: Action superclass... (137)

    +

    Action call method actually does the real work (140). Used by: Action superclass... (139)

    The summary() method returns some basic processing statistics for this action.

    -

    Action final summary of what was done (139) =

    +

    Action final summary of what was done (141) =

    -def duration( self ):
    +def duration(self) -> float:
         """Return duration of the action."""
         return (self.start and time.process_time()-self.start) or 0
    -def summary( self ):
    -    return "{!s} in {:0.2f} sec.".format( self.name, self.duration() )
    +
    +def summary(self) -> str:
    +    return f"{self.name!s} in {self.duration():0.3f} sec."
     
    -

    Action final summary of what was done (139). Used by: Action superclass... (137)

    +

    Action final summary of what was done (141). Used by: Action superclass... (139)

    -

    ActionSequence Class

    +

    ActionSequence Class

    A ActionSequence defines a composite action; it is a sequence of other actions. When the macro is performed, it delegates to the sub-actions.

    @@ -5489,65 +5909,70 @@

    ActionSequence Class

    action.

    This class overrides the perform() method of the superclass. It also adds an append() method that is used to construct the sequence of actions.

    -

    ActionSequence subclass that holds a sequence of other actions (140) =

    +

    ActionSequence subclass that holds a sequence of other actions (142) =

    -class ActionSequence( Action ):
    +class ActionSequence(Action):
         """An action composed of a sequence of other actions."""
    -    def __init__( self, name, opSequence=None ):
    -        super().__init__( name )
    -        if opSequence: self.opSequence= opSequence
    -        else: self.opSequence= []
    -    def __str__( self ):
    -        return "; ".join( [ str(x) for x in self.opSequence ] )
    -    →ActionSequence call method delegates the sequence of ations (141)
    -    →ActionSequence append adds a new action to the sequence (142)
    -    →ActionSequence summary summarizes each step (143)
    +    def __init__(self, name: str, opSequence: list[Action] | None = None) -> None:
    +        super().__init__(name)
    +        if opSequence: self.opSequence = opSequence
    +        else: self.opSequence = []
    +
    +    def __str__(self) -> str:
    +        return "; ".join([str(x) for x in self.opSequence])
    +
    +    →ActionSequence call method delegates the sequence of ations (143)
    +
    +    →ActionSequence append adds a new action to the sequence (144)
    +
    +    →ActionSequence summary summarizes each step (145)
     
    -

    ActionSequence subclass that holds a sequence of other actions (140). Used by: Action class hierarchy... (136)

    +

    ActionSequence subclass that holds a sequence of other actions (142). Used by: Action class hierarchy... (138)

    Since the macro __call__() method delegates to other Actions, it is possible to short-cut argument processing by using the Python *args construct to accept all arguments and pass them to each sub-action.

    -

    ActionSequence call method delegates the sequence of ations (141) =

    +

    ActionSequence call method delegates the sequence of ations (143) =

    -def __call__( self ):
    +def __call__(self) -> None:
    +    super().__call__()
         for o in self.opSequence:
    -        o.web= self.web
    -        o.options= self.options
    +        o.web = self.web
    +        o.options = self.options
             o()
     
    -

    ActionSequence call method delegates the sequence of ations (141). Used by: ActionSequence subclass... (140)

    +

    ActionSequence call method delegates the sequence of ations (143). Used by: ActionSequence subclass... (142)

    Since this class is essentially a wrapper around the built-in sequence type, we delegate sequence related actions directly to the underlying sequence.

    -

    ActionSequence append adds a new action to the sequence (142) =

    +

    ActionSequence append adds a new action to the sequence (144) =

    -def append( self, anAction ):
    -    self.opSequence.append( anAction )
    +def append(self, anAction: Action) -> None:
    +    self.opSequence.append(anAction)
     
    -

    ActionSequence append adds a new action to the sequence (142). Used by: ActionSequence subclass... (140)

    +

    ActionSequence append adds a new action to the sequence (144). Used by: ActionSequence subclass... (142)

    The summary() method returns some basic processing statistics for each step of this action.

    -

    ActionSequence summary summarizes each step (143) =

    +

    ActionSequence summary summarizes each step (145) =

    -def summary( self ):
    -    return ", ".join( [ o.summary() for o in self.opSequence ] )
    +def summary(self) -> str:
    +    return ", ".join([o.summary() for o in self.opSequence])
     
    -

    ActionSequence summary summarizes each step (143). Used by: ActionSequence subclass... (140)

    +

    ActionSequence summary summarizes each step (145). Used by: ActionSequence subclass... (142)

    -

    WeaveAction Class

    +

    WeaveAction Class

    The WeaveAction defines the action of weaving. This action logs a message, and invokes the weave() method of the Web instance. This method also includes the basic decision on which weaver to use. If a Weaver was @@ -5556,66 +5981,68 @@

    WeaveAction Class

    This class overrides the __call__() method of the superclass.

    If the options include theWeaver, that Weaver instance will be used. Otherwise, the web.language() method function is used to guess what weaver to use.

    -

    WeaveAction subclass initiates the weave action (144) =

    +

    WeaveAction subclass initiates the weave action (146) =

    -class WeaveAction( Action ):
    +class WeaveAction(Action):
         """Weave the final document."""
    -    def __init__( self ):
    -        super().__init__( "Weave" )
    -    def __str__( self ):
    -        return "{!s} [{!s}, {!s}]".format( self.name, self.web, self.theWeaver )
    +    def __init__(self) -> None:
    +        super().__init__("Weave")
    +
    +    def __str__(self) -> str:
    +        return f"{self.name!s} [{self.web!s}, {self.options.theWeaver!s}]"
    +
    +    →WeaveAction call method to pick the language (147)
     
    -    →WeaveAction call method to pick the language (145)
    -    →WeaveAction summary of language choice (146)
    +    →WeaveAction summary of language choice (148)
     
    -

    WeaveAction subclass initiates the weave action (144). Used by: Action class hierarchy... (136)

    +

    WeaveAction subclass initiates the weave action (146). Used by: Action class hierarchy... (138)

    The language is picked just prior to weaving. It is either (1) the language specified on the command line, or, (2) if no language was specified, a language is selected based on the first few characters of the input.

    Weaving can only raise an exception when there is a reference to a chunk that is never defined.

    -

    WeaveAction call method to pick the language (145) =

    +

    WeaveAction call method to pick the language (147) =

    -def __call__( self ):
    +def __call__(self) -> None:
         super().__call__()
         if not self.options.theWeaver:
             # Examine first few chars of first chunk of web to determine language
    -        self.options.theWeaver= self.web.language()
    -        self.logger.info( "Using {0}".format(self.options.theWeaver.__class__.__name__) )
    -    self.options.theWeaver.reference_style= self.options.reference_style
    +        self.options.theWeaver = self.web.language()
    +        self.logger.info("Using %s", self.options.theWeaver.__class__.__name__)
    +    self.options.theWeaver.reference_style = self.options.reference_style
    +    self.options.theWeaver.output = self.options.output
         try:
    -        self.web.weave( self.options.theWeaver )
    -        self.logger.info( "Finished Normally" )
    +        self.web.weave(self.options.theWeaver)
    +        self.logger.info("Finished Normally")
         except Error as e:
    -        self.logger.error(
    -            "Problems weaving document from {!s} (weave file is faulty).".format(
    -            self.web.webFileName) )
    +        self.logger.error("Problems weaving document from %r (weave file is faulty).", self.web.web_path)
             #raise
     
    -

    WeaveAction call method to pick the language (145). Used by: WeaveAction subclass... (144)

    +

    WeaveAction call method to pick the language (147). Used by: WeaveAction subclass... (146)

    The summary() method returns some basic processing statistics for the weave action.

    -

    WeaveAction summary of language choice (146) =

    +

    WeaveAction summary of language choice (148) =

    -def summary( self ):
    +def summary(self) -> str:
         if self.options.theWeaver and self.options.theWeaver.linesWritten > 0:
    -        return "{!s} {:d} lines in {:0.2f} sec.".format( self.name,
    -        self.options.theWeaver.linesWritten, self.duration() )
    -    return "did not {!s}".format( self.name, )
    +        return (
    +            f"{self.name!s} {self.options.theWeaver.linesWritten:d} lines in {self.duration():0.3f} sec."
    +        )
    +    return f"did not {self.name!s}"
     
    -

    WeaveAction summary of language choice (146). Used by: WeaveAction subclass... (144)

    +

    WeaveAction summary of language choice (148). Used by: WeaveAction subclass... (146)

    -

    TangleAction Class

    +

    TangleAction Class

    The TangleAction defines the action of tangling. This operation logs a message, and invokes the weave() method of the Web instance. This method also includes the basic decision on which weaver to use. If a Weaver was @@ -5623,76 +6050,80 @@

    TangleAction Class

    are examined and a weaver is selected.

    This class overrides the __call__() method of the superclass.

    The options must include theTangler, with the Tangler instance to be used.

    -

    TangleAction subclass initiates the tangle action (147) =

    +

    TangleAction subclass initiates the tangle action (149) =

    -class TangleAction( Action ):
    +class TangleAction(Action):
         """Tangle source files."""
    -    def __init__( self ):
    -        super().__init__( "Tangle" )
    -    →TangleAction call method does tangling of the output files (148)
    -    →TangleAction summary method provides total lines tangled (149)
    +    def __init__(self) -> None:
    +        super().__init__("Tangle")
    +
    +    →TangleAction call method does tangling of the output files (150)
    +
    +    →TangleAction summary method provides total lines tangled (151)
     
    -

    TangleAction subclass initiates the tangle action (147). Used by: Action class hierarchy... (136)

    +

    TangleAction subclass initiates the tangle action (149). Used by: Action class hierarchy... (138)

    Tangling can only raise an exception when a cross reference request (@f, @m or @u) occurs in a program code chunk. Program code chunks are defined with any of @d or @o and use @{ @} brackets.

    -

    TangleAction call method does tangling of the output files (148) =

    +

    TangleAction call method does tangling of the output files (150) =

    -def __call__( self ):
    +def __call__(self) -> None:
         super().__call__()
    -    self.options.theTangler.include_line_numbers= self.options.tangler_line_numbers
    +    self.options.theTangler.include_line_numbers = self.options.tangler_line_numbers
    +    self.options.theTangler.output = self.options.output
         try:
    -        self.web.tangle( self.options.theTangler )
    +        self.web.tangle(self.options.theTangler)
         except Error as e:
    -        self.logger.error(
    -            "Problems tangling outputs from {!r} (tangle files are faulty).".format(
    -            self.web.webFileName) )
    +        self.logger.error("Problems tangling outputs from %r (tangle files are faulty).", self.web.web_path)
             #raise
     
    -

    TangleAction call method does tangling of the output files (148). Used by: TangleAction subclass... (147)

    +

    TangleAction call method does tangling of the output files (150). Used by: TangleAction subclass... (149)

    The summary() method returns some basic processing statistics for the tangle action.

    -

    TangleAction summary method provides total lines tangled (149) =

    +

    TangleAction summary method provides total lines tangled (151) =

    -def summary( self ):
    +def summary(self) -> str:
         if self.options.theTangler and self.options.theTangler.linesWritten > 0:
    -        return "{!s} {:d} lines in {:0.2f} sec.".format( self.name,
    -        self.options.theTangler.totalLines, self.duration() )
    -    return "did not {!r}".format( self.name, )
    +        return (
    +            f"{self.name!s} {self.options.theTangler.totalLines:d} lines in {self.duration():0.3f} sec."
    +        )
    +    return f"did not {self.name!r}"
     
    -

    TangleAction summary method provides total lines tangled (149). Used by: TangleAction subclass... (147)

    +

    TangleAction summary method provides total lines tangled (151). Used by: TangleAction subclass... (149)

    -

    LoadAction Class

    +

    LoadAction Class

    The LoadAction defines the action of loading the web structure. This action uses the application's webReader to actually do the load.

    An instance is created during parsing of the input parameters. An instance of this class is part of any of the weave, tangle and "do everything" action.

    This class overrides the __call__() method of the superclass.

    The options must include webReader, with the WebReader instance to be used.

    -

    LoadAction subclass loads the document web (150) =

    +

    LoadAction subclass loads the document web (152) =

    -class LoadAction( Action ):
    +class LoadAction(Action):
         """Load the source web."""
    -    def __init__( self ):
    -        super().__init__( "Load" )
    -    def __str__( self ):
    -        return "Load [{!s}, {!s}]".format( self.webReader, self.web )
    -    →LoadAction call method loads the input files (151)
    -    →LoadAction summary provides lines read (152)
    +    def __init__(self) -> None:
    +        super().__init__("Load")
    +    def __str__(self) -> str:
    +        return f"Load [{self.webReader!s}, {self.web!s}]"
    +
    +    →LoadAction call method loads the input files (153)
    +
    +    →LoadAction summary provides lines read (154)
     
    -

    LoadAction subclass loads the document web (150). Used by: Action class hierarchy... (136)

    +

    LoadAction subclass loads the document web (152). Used by: Action class hierarchy... (138)

    Trying to load the web involves two steps, either of which can raise exceptions due to incorrect inputs.

    @@ -5709,25 +6140,24 @@

    LoadAction Class

  • The Web class createUsedBy() method can raise an exception when a chunk reference cannot be resolved to a named chunk.
  • -

    LoadAction call method loads the input files (151) =

    +

    LoadAction call method loads the input files (153) =

    -def __call__( self ):
    +def __call__(self) -> None:
         super().__call__()
    -    self.webReader= self.options.webReader
    -    self.webReader.command= self.options.command
    -    self.webReader.permitList= self.options.permitList
    -    self.web.webFileName= self.options.webFileName
    -    error= "Problems with source file {!r}, no output produced.".format(
    -            self.options.webFileName)
    +    self.webReader = self.options.webReader
    +    self.webReader.command = self.options.command
    +    self.webReader.permitList = self.options.permitList
    +    self.web.web_path = self.options.source_path
    +    error = f"Problems with source file {self.options.source_path!r}, no output produced."
         try:
    -        self.webReader.load( self.web, self.options.webFileName )
    +        self.webReader.load(self.web, self.options.source_path)
             if self.webReader.errors != 0:
    -            self.logger.error( error )
    -            raise Error( "Syntax Errors in the Web" )
    +            self.logger.error(error)
    +            raise Error("Syntax Errors in the Web")
             self.web.createUsedBy()
             if self.webReader.errors != 0:
    -            self.logger.error( error )
    -            raise Error( "Internal Reference Errors in the Web" )
    +            self.logger.error(error)
    +            raise Error("Internal Reference Errors in the Web")
         except Error as e:
             self.logger.error(error)
             raise # Older design.
    @@ -5737,38 +6167,38 @@ 

    LoadAction Class

    -

    LoadAction call method loads the input files (151). Used by: LoadAction subclass... (150)

    +

    LoadAction call method loads the input files (153). Used by: LoadAction subclass... (152)

    The summary() method returns some basic processing statistics for the load action.

    -

    LoadAction summary provides lines read (152) =

    +

    LoadAction summary provides lines read (154) =

    -def summary( self ):
    -    return "{!s} {:d} lines from {:d} files in {:0.2f} sec.".format(
    -        self.name, self.webReader.totalLines,
    -        self.webReader.totalFiles, self.duration() )
    +def summary(self) -> str:
    +    return (
    +        f"{self.name!s} {self.webReader.totalLines:d} lines from {self.webReader.totalFiles:d} files in {self.duration():0.3f} sec."
    +    )
     
    -

    LoadAction summary provides lines read (152). Used by: LoadAction subclass... (150)

    +

    LoadAction summary provides lines read (154). Used by: LoadAction subclass... (152)

    -

    pyWeb Module File

    +

    pyWeb Module File

    The pyWeb application file is shown below:

    -

    pyweb.py (153) =

    +

    pyweb.py (155) =

    -→Overheads (155), →(156), →(157)
    -→Imports (11), →(47), →(96), →(124), →(131), →(133), →(154), →(158), →(164)
    -→Base Class Definitions (1)
    -→Application Class (159), →(160)
    -→Logging Setup (165), →(166)
    -→Interface Functions (167)
    +→Overheads (157), →(158), →(159)
    +→Imports (3), →(12), →(48), →(58), →(98), →(124), →(129), →(132), →(134), →(156), →(160), →(166)
    +→Base Class Definitions (1)
    +→Application Class (161), →(162)
    +→Logging Setup (167), →(168)
    +→Interface Functions (169)
     
    -

    pyweb.py (153).

    +

    pyweb.py (155).

    The Overheads are described below, they include things like:

    -

    Python Library Imports

    +

    Python Library Imports

    Numerous Python library modules are used by this application.

    A few are listed here because they're used widely. Others are listed closer to where they're referenced.

    @@ -5798,7 +6228,7 @@

    Python Library Imports

  • The datetime module is used to format times, phasing out use of time.
  • The types module is used to get at SimpleNamespace for configuration.
  • -

    Imports (154) +=

    +

    Imports (156) +=

     import os
     import time
    @@ -5807,87 +6237,67 @@ 

    Python Library Imports

    -

    Imports (154). Used by: pyweb.py (153)

    +

    Imports (156). Used by: pyweb.py (155)

    Note that os.path, time, datetime and platform` are provided in the expression context.

    -

    Overheads

    +

    Overheads

    The shell escape is provided so that the user can define this file as executable, and launch it directly from their shell. The shell reads the first line of a file; when it finds the '#!' shell escape, the remainder of the line is taken as the path to the binary program that should be run. The shell runs this binary, providing the file as standard input.

    -

    Overheads (155) =

    +

    Overheads (157) =

     #!/usr/bin/env python
     
    -

    Overheads (155). Used by: pyweb.py (153)

    +

    Overheads (157). Used by: pyweb.py (155)

    A Python __doc__ string provides a standard vehicle for documenting the module or the application program. The usual style is to provide a one-sentence summary on the first line. This is followed by more detailed usage information.

    -

    Overheads (156) +=

    +

    Overheads (158) +=

    -"""pyWeb Literate Programming - tangle and weave tool.
    +"""py-web-tool Literate Programming.
     
    -Yet another simple literate programming tool derived from nuweb,
    +Yet another simple literate programming tool derived from **nuweb**,
     implemented entirely in Python.
    -This produces any markup for any programming language.
    -
    -Usage:
    -    pyweb.py [-dvs] [-c x] [-w format] file.w
    -
    -Options:
    -    -v           verbose output (the default)
    -    -s           silent output
    -    -d           debugging output
    -    -c x         change the command character from '@' to x
    -    -w format    Use the given weaver for the final document.
    -                 Choices are rst, html, latex and htmlshort.
    -                 Additionally, a `module.class` name can be used.
    -    -xw          Exclude weaving
    -    -xt          Exclude tangling
    -    -pi          Permit include-command errors
    -    -rt          Transitive references
    -    -rs          Simple references (default)
    -    -n           Include line number comments in the tangled source; requires
    -                 comment start and stop on the @o commands.
    -
    -    file.w       The input file, with @o, @d, @i, @[, @{, @|, @<, @f, @m, @u commands.
    +With a suitable configuration, this weaves documents with any markup language,
    +and tangles source files for any programming language.
     """
     
    -

    Overheads (156). Used by: pyweb.py (153)

    +

    Overheads (158). Used by: pyweb.py (155)

    The keyword cruft is a standard way of placing version control information into a Python module so it is preserved. See PEP (Python Enhancement Proposal) #8 for information on recommended styles.

    We also sneak in a "DO NOT EDIT" warning that belongs in all generated application source files.

    -

    Overheads (157) +=

    +

    Overheads (159) +=

    -__version__ = """3.0"""
    +__version__ = """3.1"""
     
     ### DO NOT EDIT THIS FILE!
    -### It was created by /Users/slott/Documents/Projects/PyWebTool-3/pyweb/pyweb.py, __version__='3.0'.
    -### From source pyweb.w modified Sat Jun 16 08:10:37 2018.
    -### In working directory '/Users/slott/Documents/Projects/PyWebTool-3/pyweb'.
    +### It was created by /Users/slott/Documents/Projects/py-web-tool/bootstrap/pyweb.py, __version__='3.0'.
    +### From source pyweb.w modified Mon Jun 13 08:52:05 2022.
    +### In working directory '/Users/slott/Documents/Projects/py-web-tool/src'.
     
    -

    Overheads (157). Used by: pyweb.py (153)

    +

    Overheads (159). Used by: pyweb.py (155)

    -

    The Application Class

    +

    The Application Class

    The Application class is provided so that the Action instances have an overall application to update. This allows the WeaveAction to provide the selected Weaver instance to the application. It also provides a @@ -5908,39 +6318,40 @@

    The Application Class

     import pyweb, argparse
     
    -p= argparse.ArgumentParser()
    +p = argparse.ArgumentParser()
     argument definition
     config = p.parse_args()
     
    -a= pyweb.Application()
    +a = pyweb.Application()
     Configure the Application based on options
    -a.process( config )
    +a.process(config)
     

    The main() function creates an Application instance and calls the parseArgs() and process() methods to provide the expected default behavior for this module when it is used as the main program.

    The configuration can be either a types.SimpleNamespace or an argparse.Namespace instance.

    -

    Imports (158) +=

    +

    Imports (160) +=

     import argparse
     
    -

    Imports (158). Used by: pyweb.py (153)

    +

    Imports (160). Used by: pyweb.py (155)

    -

    Application Class (159) =

    +

    Application Class (161) =

     class Application:
    -    def __init__( self ):
    -        self.logger= logging.getLogger( self.__class__.__qualname__ )
    -        →Application default options (161)
    -    →Application parse command line (162)
    -    →Application class process all files (163)
    +    def __init__(self) -> None:
    +        self.logger = logging.getLogger(self.__class__.__qualname__)
    +        →Application default options (163)
    +
    +    →Application parse command line (164)
    +    →Application class process all files (165)
     
    -

    Application Class (159). Used by: pyweb.py (153)

    +

    Application Class (161). Used by: pyweb.py (155)

    The first part of parsing the command line is setting default values that apply when parameters are omitted. @@ -6016,7 +6427,7 @@

    The Application Class

    Rather than automate this, and potentially expose elements of the class hierarchy that aren't really meant to be used, we provide a manually-developed list.

    -

    Application Class (160) +=

    +

    Application Class (162) +=

     # Global list of available weaver classes.
     weavers = {
    @@ -6028,96 +6439,102 @@ 

    The Application Class

    -

    Application Class (160). Used by: pyweb.py (153)

    +

    Application Class (162). Used by: pyweb.py (155)

    The defaults used for application configuration. The expand() method expands on these simple text values to create more useful objects.

    -

    Application default options (161) =

    -
    -self.defaults= argparse.Namespace(
    -    verbosity= logging.INFO,
    -    command= '@',
    -    weaver= 'rst',
    -    skip= '', # Don't skip any steps
    -    permit= '', # Don't tolerate missing includes
    -    reference= 's', # Simple references
    -    tangler_line_numbers= False,
    +

    Application default options (163) =

    +
    +self.defaults = argparse.Namespace(
    +    verbosity=logging.INFO,
    +    command='@',
    +    weaver='rst',
    +    skip='',  # Don't skip any steps
    +    permit='',  # Don't tolerate missing includes
    +    reference='s',  # Simple references
    +    tangler_line_numbers=False,
    +    output=Path.cwd(),
         )
    -self.expand( self.defaults )
    +# self.expand(self.defaults)
     
     # Primitive Actions
    -self.loadOp= LoadAction()
    -self.weaveOp= WeaveAction()
    -self.tangleOp= TangleAction()
    +self.loadOp = LoadAction()
    +self.weaveOp = WeaveAction()
    +self.tangleOp = TangleAction()
     
     # Composite Actions
    -self.doWeave= ActionSequence( "load and weave", [self.loadOp, self.weaveOp] )
    -self.doTangle= ActionSequence( "load and tangle", [self.loadOp, self.tangleOp] )
    -self.theAction= ActionSequence( "load, tangle and weave", [self.loadOp, self.tangleOp, self.weaveOp] )
    +self.doWeave = ActionSequence("load and weave", [self.loadOp, self.weaveOp])
    +self.doTangle = ActionSequence("load and tangle", [self.loadOp, self.tangleOp])
    +self.theAction = ActionSequence("load, tangle and weave", [self.loadOp, self.tangleOp, self.weaveOp])
     
    -

    Application default options (161). Used by: Application Class... (159)

    +

    Application default options (163). Used by: Application Class... (161)

    The algorithm for parsing the command line parameters uses the built in argparse module. We have to build a parser, define the options, and the parse the command-line arguments, updating the default namespace.

    We further expand on the arguments. This transforms simple strings into object instances.

    -

    Application parse command line (162) =

    +

    Application parse command line (164) =

    -def parseArgs( self ):
    +def parseArgs(self, argv: list[str]) -> argparse.Namespace:
         p = argparse.ArgumentParser()
    -    p.add_argument( "-v", "--verbose", dest="verbosity", action="store_const", const=logging.INFO )
    -    p.add_argument( "-s", "--silent", dest="verbosity", action="store_const", const=logging.WARN )
    -    p.add_argument( "-d", "--debug", dest="verbosity", action="store_const", const=logging.DEBUG )
    -    p.add_argument( "-c", "--command", dest="command", action="store" )
    -    p.add_argument( "-w", "--weaver", dest="weaver", action="store" )
    -    p.add_argument( "-x", "--except", dest="skip", action="store", choices=('w','t') )
    -    p.add_argument( "-p", "--permit", dest="permit", action="store" )
    -    p.add_argument( "-r", "--reference", dest="reference", action="store", choices=('t', 's') )
    -    p.add_argument( "-n", "--linenumbers", dest="tangler_line_numbers", action="store_true" )
    -    p.add_argument( "files", nargs='+' )
    -    config= p.parse_args( namespace=self.defaults )
    -    self.expand( config )
    +    p.add_argument("-v", "--verbose", dest="verbosity", action="store_const", const=logging.INFO)
    +    p.add_argument("-s", "--silent", dest="verbosity", action="store_const", const=logging.WARN)
    +    p.add_argument("-d", "--debug", dest="verbosity", action="store_const", const=logging.DEBUG)
    +    p.add_argument("-c", "--command", dest="command", action="store")
    +    p.add_argument("-w", "--weaver", dest="weaver", action="store")
    +    p.add_argument("-x", "--except", dest="skip", action="store", choices=('w', 't'))
    +    p.add_argument("-p", "--permit", dest="permit", action="store")
    +    p.add_argument("-r", "--reference", dest="reference", action="store", choices=('t', 's'))
    +    p.add_argument("-n", "--linenumbers", dest="tangler_line_numbers", action="store_true")
    +    p.add_argument("-o", "--output", dest="output", action="store", type=Path)
    +    p.add_argument("-V", "--Version", action='version', version=f"py-web-tool pyweb.py {__version__}")
    +    p.add_argument("files", nargs='+', type=Path)
    +    config = p.parse_args(argv, namespace=self.defaults)
    +    self.expand(config)
         return config
     
    -def expand( self, config ):
    +def expand(self, config: argparse.Namespace) -> argparse.Namespace:
         """Translate the argument values from simple text to useful objects.
         Weaver. Tangler. WebReader.
         """
    -    if config.reference == 't':
    -        config.reference_style = TransitiveReference()
    -    elif config.reference == 's':
    -        config.reference_style = SimpleReference()
    -    else:
    -        raise Error( "Improper configuration" )
    -
    +    match config.reference:
    +        case 't':
    +            config.reference_style = TransitiveReference()
    +        case 's':
    +            config.reference_style = SimpleReference()
    +        case _:
    +            raise Error("Improper configuration")
    +
    +    # Weaver
         try:
    -        weaver_class= weavers[config.weaver.lower()]
    +        weaver_class = weavers[config.weaver.lower()]
         except KeyError:
             module_name, _, class_name = config.weaver.partition('.')
             weaver_module = __import__(module_name)
             weaver_class = weaver_module.__dict__[class_name]
             if not issubclass(weaver_class, Weaver):
    -            raise TypeError( "{0!r} not a subclass of Weaver".format(weaver_class) )
    -    config.theWeaver= weaver_class()
    +            raise TypeError(f"{weaver_class!r} not a subclass of Weaver")
    +    config.theWeaver = weaver_class()
     
    -    config.theTangler= TanglerMake()
    +    # Tangler
    +    config.theTangler = TanglerMake()
     
         if config.permit:
             # save permitted errors, usual case is ``-pi`` to permit ``@i`` include errors
    -        config.permitList= [ '{!s}{!s}'.format( config.command, c ) for c in config.permit ]
    +        config.permitList = [f'{config.command!s}{c!s}' for c in config.permit]
         else:
    -        config.permitList= []
    +        config.permitList = []
     
    -    config.webReader= WebReader()
    +    config.webReader = WebReader()
     
         return config
     
    -

    Application parse command line (162). Used by: Application Class... (159)

    +

    Application parse command line (164). Used by: Application Class... (161)

    The process() function uses the current Application settings to process each file as follows:

    @@ -6125,7 +6542,7 @@

    The Application Class

  • Create a new WebReader for the Application, providing the parameters required to process the input file.
  • Create a Web instance, w -and set the Web's sourceFileName from the WebReader's fileName.
  • +and set the Web's sourceFileName from the WebReader's filePath.
  • Perform the given command, typically a ActionSequence, which does some combination of load, tangle the output files and weave the final document in the target language; if @@ -6137,122 +6554,124 @@

    The Application Class

    the output files, and the exception is reraised. The re-raising is done so that all exceptions are handled by the outermost main program.

    -

    Application class process all files (163) =

    +

    Application class process all files (165) =

    -def process( self, config ):
    -    root= logging.getLogger()
    -    root.setLevel( config.verbosity )
    -    self.logger.debug( "Setting root log level to {!r}".format(
    -        logging.getLevelName(root.getEffectiveLevel()) ) )
    +def process(self, config: argparse.Namespace) -> None:
    +    root = logging.getLogger()
    +    root.setLevel(config.verbosity)
    +    self.logger.debug("Setting root log level to %r", logging.getLevelName(root.getEffectiveLevel()))
     
         if config.command:
    -        self.logger.debug( "Command character {!r}".format(config.command) )
    +        self.logger.debug("Command character %r", config.command)
     
         if config.skip:
    -        if config.skip.lower().startswith('w'): # not weaving == tangling
    -            self.theAction= self.doTangle
    -        elif config.skip.lower().startswith('t'): # not tangling == weaving
    -            self.theAction= self.doWeave
    +        if config.skip.lower().startswith('w'):  # not weaving == tangling
    +            self.theAction = self.doTangle
    +        elif config.skip.lower().startswith('t'):  # not tangling == weaving
    +            self.theAction = self.doWeave
             else:
    -            raise Exception( "Unknown -x option {!r}".format(config.skip) )
    +            raise Exception(f"Unknown -x option {config.skip!r}")
     
    -    self.logger.info( "Weaver {!s}".format(config.theWeaver) )
    +    self.logger.info("Weaver %s", config.theWeaver)
     
         for f in config.files:
    -        w= Web() # New, empty web to load and process.
    -        self.logger.info( "{!s} {!r}".format(self.theAction.name, f) )
    -        config.webFileName= f
    -        self.theAction.web= w
    -        self.theAction.options= config
    +        w = Web() # New, empty web to load and process.
    +        self.logger.info("%s %r", self.theAction.name, f)
    +        config.source_path = f
    +        self.theAction.web = w
    +        self.theAction.options = config
             self.theAction()
    -        self.logger.info( self.theAction.summary() )
    +        self.logger.info(self.theAction.summary())
     
    -

    Application class process all files (163). Used by: Application Class... (159)

    +

    Application class process all files (165). Used by: Application Class... (161)

  • -

    Logging Setup

    +

    Logging Setup

    We'll create a logging context manager. This allows us to wrap the main() function in an explicit with statement that assures that logging is configured and cleaned up politely.

    -

    Imports (164) +=

    +

    Imports (166) +=

     import logging
     import logging.config
     
    -

    Imports (164). Used by: pyweb.py (153)

    +

    Imports (166). Used by: pyweb.py (155)

    This has two configuration approaches. If a positional argument is given, that dictionary is used for logging.config.dictConfig. Otherwise, keyword arguments are provided to logging.basicConfig.

    A subclass might properly load a dictionary encoded in YAML and use that with logging.config.dictConfig.

    -

    Logging Setup (165) =

    +

    Logging Setup (167) =

     class Logger:
    -    def __init__( self, dict_config=None, **kw_config ):
    -        self.dict_config= dict_config
    -        self.kw_config= kw_config
    -    def __enter__( self ):
    +    def __init__(self, dict_config: dict[str, Any] | None = None, **kw_config: Any) -> None:
    +        self.dict_config = dict_config
    +        self.kw_config = kw_config
    +
    +    def __enter__(self) -> "Logger":
             if self.dict_config:
    -            logging.config.dictConfig( self.dict_config )
    +            logging.config.dictConfig(self.dict_config)
             else:
    -            logging.basicConfig( **self.kw_config )
    +            logging.basicConfig(**self.kw_config)
             return self
    -    def __exit__( self, *args ):
    +
    +    def __exit__(self, *args: Any) -> Literal[False]:
             logging.shutdown()
             return False
     
    -

    Logging Setup (165). Used by: pyweb.py (153)

    +

    Logging Setup (167). Used by: pyweb.py (155)

    Here's a sample logging setup. This creates a simple console handler and a formatter that matches the basicConfig formatter.

    It defines the root logger plus two overrides for class loggers that might be used to gather additional information.

    -

    Logging Setup (166) +=

    +

    Logging Setup (168) +=

    -log_config= dict(
    -    version= 1,
    -    disable_existing_loggers= False, # Allow pre-existing loggers to work.
    -    handlers= {
    +log_config = {
    +    'version': 1,
    +    'disable_existing_loggers': False, # Allow pre-existing loggers to work.
    +    'style': '{',
    +    'handlers': {
             'console': {
                 'class': 'logging.StreamHandler',
                 'stream': 'ext://sys.stderr',
                 'formatter': 'basic',
             },
         },
    -    formatters = {
    +    'formatters': {
             'basic': {
                 'format': "{levelname}:{name}:{message}",
                 'style': "{",
             }
         },
     
    -    root= { 'handlers': ['console'], 'level': logging.INFO, },
    +    'root': {'handlers': ['console'], 'level': logging.INFO,},
     
         #For specific debugging support...
    -    loggers= {
    -    #    'RST': { 'level': logging.DEBUG },
    -    #    'TanglerMake': { 'level': logging.DEBUG },
    -    #    'WebReader': { 'level': logging.DEBUG },
    +    'loggers': {
    +    #    'RST': {'level': logging.DEBUG},
    +    #    'TanglerMake': {'level': logging.DEBUG},
    +    #    'WebReader': {'level': logging.DEBUG},
         },
    -)
    +}
     
    -

    Logging Setup (166). Used by: pyweb.py (153)

    +

    Logging Setup (168). Used by: pyweb.py (155)

    This seems a bit verbose; a separate configuration file might be better.

    Also, we might want a decorator to define loggers consistently for each class.

    -

    The Main Function

    +

    The Main Function

    The top-level interface is the main() function. This function creates an Application instance.

    The Application object parses the command-line arguments. @@ -6260,20 +6679,20 @@

    The Main Function

    This two-step process allows for some dependency injection to customize argument processing.

    We might also want to parse a logging configuration file, as well as a weaver template configuration file.

    -

    Interface Functions (167) =

    +

    Interface Functions (169) =

    -def main():
    -    a= Application()
    -    config= a.parseArgs()
    +def main(argv: list[str] = sys.argv[1:]) -> None:
    +    a = Application()
    +    config = a.parseArgs(argv)
         a.process(config)
     
     if __name__ == "__main__":
    -    with Logger( log_config ):
    -        main( )
    +    with Logger(log_config):
    +        main()
     
    -

    Interface Functions (167). Used by: pyweb.py (153)

    +

    Interface Functions (169). Used by: pyweb.py (155)

    This can be extended by doing something like the following.

      @@ -6284,46 +6703,41 @@

      The Main Function

     import pyweb
    -class MyWeaver( HTML ):
    +class MyWeaver(HTML):
        Any template changes
     
     pyweb.weavers['myweaver']= MyWeaver()
     pyweb.main()
     
    -

    This will create a variant on pyWeb that will handle a different +

    This will create a variant on py-web-tool that will handle a different weaver via the command-line option -w myweaver.

    - +
    -

    Unit Tests

    -

    The test directory includes pyweb_test.w, which will create a +

    Unit Tests

    +

    The tests directory includes pyweb_test.w, which will create a complete test suite.

    -

    This source will weaves a pyweb_test.html file. See file:test/pyweb_test.html

    +

    This source will weaves a pyweb_test.html file. See tests/pyweb_test.html.

    This source will tangle several test modules: test.py, test_tangler.py, test_weaver.py, -test_loader.py and test_unit.py. Running the test.py module will include and -execute all 78 tests.

    +test_loader.py, test_unit.py, and test_scripts.py.

    +

    Use pytest to discover and run all 80+ test cases.

    Here's a script that works out well for running this without disturbing the development environment. The PYTHONPATH setting is essential to support importing pyweb.

    -cd test
    -python ../pyweb.py pyweb_test.w
    -PYTHONPATH=.. python test.py
    +python pyweb.py -o tests tests/pyweb_test.w
    +PYTHONPATH=$(PWD) pytest
     

    Note that the last line really does set an environment variable and run -a program on a single line.

    - +the pytest tool on a single line.

    +
    -
    -

    Additional Files

    +
    +

    Handy Scripts and Other Files

    Two aditional scripts, tangle.py and weave.py, are provided as examples -which an be customized.

    -

    The README and setup.py files are also an important part of the -distribution as are a .nojekyll and index.html that are part of -publishing from GitHub.

    -

    The .CSS file and .conf file for RST production are also provided here.

    +which can be customized and extended.

    -

    tangle.py Script

    +

    tangle.py Script

    This script shows a simple version of Tangling. This has a permitted error for '@i' commands to allow an include file (for example test results) to be omitted from the tangle operation.

    @@ -6337,92 +6751,101 @@

    tangle.py< Set the web, set the options, execute the callable action, and write a summary. -

    tangle.py (168) =

    +

    tangle.py (170) =

     #!/usr/bin/env python3
     """Sample tangle.py script."""
    -import pyweb
    -import logging
     import argparse
    +import logging
    +from pathlib import Path
    +import pyweb
    +
    +def main(source: Path) -> None:
    +    with pyweb.Logger(pyweb.log_config):
    +        logger = logging.getLogger(__file__)
    +
    +        options = argparse.Namespace(
    +            source_path=source,
    +            output=source.parent,
    +            verbosity=logging.INFO,
    +            command='@',
    +            permitList=['@i'],
    +            tangler_line_numbers=False,
    +            reference_style=pyweb.SimpleReference(),
    +            theTangler=pyweb.TanglerMake(),
    +            webReader=pyweb.WebReader(),
    +        )
     
    -with pyweb.Logger( pyweb.log_config ):
    -    logger= logging.getLogger(__file__)
    -
    -    options = argparse.Namespace(
    -            webFileName= "pyweb.w",
    -            verbosity= logging.INFO,
    -            command= '@',
    -            permitList= ['@i'],
    -            tangler_line_numbers= False,
    -            reference_style = pyweb.SimpleReference(),
    -            theTangler= pyweb.TanglerMake(),
    -            webReader= pyweb.WebReader(),
    -            )
    -
    -    w= pyweb.Web()
    -
    -    for action in LoadAction(), TangleAction():
    -            action.web= w
    -            action.options= options
    +        w = pyweb.Web()
    +
    +        for action in pyweb.LoadAction(), pyweb.TangleAction():
    +            action.web = w
    +            action.options = options
                 action()
    -            logger.info( action.summary() )
    +            logger.info(action.summary())
    +
    +if __name__ == "__main__":
    +    main(Path("examples/test_rst.w"))
     
    -

    tangle.py (168).

    +

    tangle.py (170).

    -

    weave.py Script

    +

    weave.py Script

    This script shows a simple version of Weaving. This shows how to define a customized set of templates for a different markup language.

    A customized weaver generally has three parts.

    -

    weave.py (169) =

    +

    weave.py (171) =

    -→weave.py overheads for correct operation of a script (170)
    -→weave.py custom weaver definition to customize the Weaver being used (171)
    -→weaver.py processing: load and weave the document (172)
    +→weave.py overheads for correct operation of a script (172)
    +
    +→weave.py custom weaver definition to customize the Weaver being used (173)
    +
    +→weaver.py processing: load and weave the document (174)
     
    -

    weave.py (169).

    +

    weave.py (171).

    -

    weave.py overheads for correct operation of a script (170) =

    +

    weave.py overheads for correct operation of a script (172) =

     #!/usr/bin/env python3
     """Sample weave.py script."""
    -import pyweb
    -import logging
     import argparse
    +import logging
     import string
    +from pathlib import Path
    +import pyweb
     
    -

    weave.py overheads for correct operation of a script (170). Used by: weave.py (169)

    +

    weave.py overheads for correct operation of a script (172). Used by: weave.py (171)

    -

    weave.py custom weaver definition to customize the Weaver being used (171) =

    +

    weave.py custom weaver definition to customize the Weaver being used (173) =

    -class MyHTML( pyweb.HTML ):
    +class MyHTML(pyweb.HTML):
         """HTML formatting templates."""
    -    extension= ".html"
    +    extension = ".html"
     
    -    cb_template= string.Template("""<a name="pyweb${seq}"></a>
    +    cb_template = string.Template("""<a name="pyweb${seq}"></a>
         <!--line number ${lineNumber}-->
         <p><em>${fullName}</em> (${seq})&nbsp;${concat}</p>
    -    <code><pre>\n""")
    +    <pre><code>\n""")
     
    -    ce_template= string.Template("""
    -    </pre></code>
    +    ce_template = string.Template("""
    +    </code></pre>
         <p>&loz; <em>${fullName}</em> (${seq}).
         ${references}
         </p>\n""")
     
    -    fb_template= string.Template("""<a name="pyweb${seq}"></a>
    +    fb_template = string.Template("""<a name="pyweb${seq}"></a>
         <!--line number ${lineNumber}-->
         <p>``${fullName}`` (${seq})&nbsp;${concat}</p>
    -    <code><pre>\n""") # Prevent indent
    +    <pre><code>\n""") # Prevent indent
     
    -    fe_template= string.Template( """</pre></code>
    +    fe_template = string.Template( """</code></pre>
         <p>&loz; ``${fullName}`` (${seq}).
         ${references}
         </p>\n""")
    @@ -6431,550 +6854,127 @@ 

    weave.py
    -

    weave.py custom weaver definition to customize the Weaver being used (171). Used by: weave.py (169)

    +

    weave.py custom weaver definition to customize the Weaver being used (173). Used by: weave.py (171)

    -

    weaver.py processing: load and weave the document (172) =

    +

    weaver.py processing: load and weave the document (174) =

    -with pyweb.Logger( pyweb.log_config ):
    -    logger= logging.getLogger(__file__)
    +def main(source: Path) -> None:
    +    with pyweb.Logger(pyweb.log_config):
    +        logger = logging.getLogger(__file__)
     
    -    options = argparse.Namespace(
    -            webFileName= "pyweb.w",
    -            verbosity= logging.INFO,
    -            command= '@',
    -            theWeaver= MyHTML(),
    -            permitList= [],
    -            tangler_line_numbers= False,
    -            reference_style = pyweb.SimpleReference(),
    -            theTangler= pyweb.TanglerMake(),
    -            webReader= pyweb.WebReader(),
    -            )
    +        options = argparse.Namespace(
    +            source_path=source,
    +            output=source.parent,
    +            verbosity=logging.INFO,
    +            command='@',
    +            permitList=[],
    +            tangler_line_numbers=False,
    +            reference_style=pyweb.SimpleReference(),
    +            theWeaver=MyHTML(),
    +            webReader=pyweb.WebReader(),
    +        )
     
    -    w= pyweb.Web()
    +        w = pyweb.Web()
     
    -    for action in LoadAction(), WeaveAction():
    -            action.web= w
    -            action.options= options
    +        for action in pyweb.LoadAction(), pyweb.WeaveAction():
    +            action.web = w
    +            action.options = options
                 action()
    -            logger.info( action.summary() )
    -
    - -
    -

    weaver.py processing: load and weave the document (172). Used by: weave.py (169)

    -
    -

    -
    -

    The setup.py and MANIFEST.in files

    -

    In order to support a pleasant installation, the setup.py file is helpful.

    -

    setup.py (173) =

    -
    -#!/usr/bin/env python3
    -"""Setup for pyWeb."""
    -
    -from distutils.core import setup
    -
    -setup(name='pyweb',
    -      version='3.0',
    -      description='pyWeb 3.0: Yet Another Literate Programming Tool',
    -      author='S. Lott',
    -      author_email='s_lott@yahoo.com',
    -      url='http://slott-softwarearchitect.blogspot.com/',
    -      py_modules=['pyweb'],
    -      classifiers=[
    -      'Intended Audience :: Developers',
    -      'Topic :: Documentation',
    -      'Topic :: Software Development :: Documentation',
    -      'Topic :: Text Processing :: Markup',
    -      ]
    -   )
    -
    - -
    -

    setup.py (173).

    -
    -

    In order build a source distribution kit the python3 setup.py sdist requires a -MANIFEST. We can either list all files or provide a MANIFEST.in -that specifies additional rules. -We use a simple inclusion to augment the default manifest rules.

    -

    MANIFEST.in (174) =

    -
    -include *.w *.css *.html *.conf *.rst
    -include test/*.w test/*.css test/*.html test/*.conf test/*.py
    -include jedit/*.xml
    -
    - -
    -

    MANIFEST.in (174).

    -
    -
    -
    -

    The README file

    -

    Here's the README file.

    -

    README (175) =

    -
    -pyWeb 3.0: In Python, Yet Another Literate Programming Tool
    -
    -Literate programming is an attempt to reconcile the opposing needs
    -of clear presentation to people with the technical issues of
    -creating code that will work with our current set of tools.
    -
    -Presentation to people requires extensive and sophisticated typesetting
    -techniques.  Further, the "narrative arc" of a presentation may not
    -follow the source code as layed out for the compiler.
    -
    -pyWeb is a literate programming tool based on Knuth's Web to combine the actions
    -of weaving a document with tangling source files.
    -It is independent of any particular document markup or source language.
    -Is uses a simple set of markup tags to define chunks of code and
    -documentation.
    -
    -The ``pyweb.w`` file is the source for the various pyweb module and script files.
    -The various source code files are created by applying a
    -tangle operation to the ``.w`` file.  The final documentation is created by
    -applying a weave operation to the ``.w`` file.
    -
    -Installation
    --------------
    -
    -::
    -
    -    python3 setup.py install
    -
    -This will install the pyweb module.
    -
    -Document production
    ---------------------
    -
    -The supplied documentation uses RST markup and requires docutils.
    -
    -::
    -
    -    python3 -m pyweb pyweb.w
    -    rst2html.py pyweb.rst pyweb.html
    -
    -Authoring
    ----------
    -
    -The pyweb document describes the simple markup used to define code chunks
    -and assemble those code chunks into a coherent document as well as working code.
    -
    -If you're a JEdit user, the ``jedit`` directory can be used
    -to configure syntax highlighting that includes PyWeb and RST.
    -
    -Operation
    ----------
    -
    -You can then run pyweb with
    -
    -::
    -
    -    python3 -m pyweb pyweb.w
    -
    -This will create the various output files from the source .w file.
    -
    --   ``pyweb.html`` is the final woven document.
    -
    --   ``pyweb.py``, ``tangle.py``, ``weave.py``, ``README``, ``setup.py`` and ``MANIFEST.in``
    -    ``.nojekyll`` and ``index.html`` are tangled output files.
    -
    -Testing
    --------
    -
    -The test directory includes ``pyweb_test.w``, which will create a
    -complete test suite.
    -
    -This weaves a ``pyweb_test.html`` file.
    -
    -This tangles several test modules:  ``test.py``, ``test_tangler.py``, ``test_weaver.py``,
    -``test_loader.py`` and ``test_unit.py``.  Running the ``test.py`` module will include and
    -execute all tests.
    -
    -::
    -
    -    cd test
    -    python3 -m pyweb pyweb_test.w
    -    PYTHONPATH=.. python3 test.py
    -    rst2html.py pyweb_test.rst pyweb_test.html
    -
    - -
    -

    README (175).

    -
    -
    -
    -

    The HTML Support Files

    -

    To get the RST to look good, there are some additional files.

    -

    docutils.conf defines the CSS files to use. -The default CSS file (stylesheet-path) may need to be customized for your -installation of docutils.

    -

    docutils.conf (176) =

    -
    -# docutils.conf
    -
    -[html4css1 writer]
    -stylesheet-path: /Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages/docutils/writers/html4css1/html4css1.css,
    -    page-layout.css
    -syntax-highlight: long
    -
    - -
    -

    docutils.conf (176).

    -
    -

    page-layout.css This tweaks one CSS to be sure that -the resulting HTML pages are easier to read.

    -

    page-layout.css (177) =

    -
    -/* Page layout tweaks */
    -div.document { width: 7in; }
    -.small { font-size: smaller; }
    -.code
    -{
    -    color: #101080;
    -    display: block;
    -    border-color: black;
    -    border-width: thin;
    -    border-style: solid;
    -    background-color: #E0FFFF;
    -    /*#99FFFF*/
    -    padding: 0 0 0 1%;
    -    margin: 0 6% 0 6%;
    -    text-align: left;
    -    font-size: smaller;
    -}
    -
    - -
    -

    page-layout.css (177).

    -
    -

    Yes, this creates a (nearly) empty file for use by GitHub. There's a small -bug in NamedChunk.tangle() that prevents handling zero-length text.

    -

    .nojekyll (178) =

    -
    -

    System Message: ERROR/3 (pyweb.rst, line 8444)

    -

    Content block expected for the "parsed-literal" directive; none found.

    -
    -..  parsed-literal::
    -    :class: code
    -
    +            logger.info(action.summary())
     
    -
    -
    -
    - -
    -

    .nojekyll (178).

    -
    -

    Finally, an index.html to redirect GitHub to the pyweb.html file.

    -

    index.html (179) =

    -
    -<?xml version="1.0" encoding="UTF-8"?>
    -<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
    -<html xmlns="http://www.w3.org/1999/xhtml">
    -<head><title>Redirect</title>
    -<meta http-equiv="refresh" content="0;url=pyweb.html" />
    -</head>
    -<body>Sorry, you should have been redirected <a href="pyweb.html">pyweb.html</a>.</body>
    -</html>
    +if __name__ == "__main__":
    +    main(Path("examples/test_rst.w"))
     
    -

    index.html (179).

    +

    weaver.py processing: load and weave the document (174). Used by: weave.py (171)

    - +
    -
    -

    JEdit Configuration

    -

    Here's the pyweb.xml file that you'll need to configure -JEdit so that it properly highlights your PyWeb commands.

    -

    We'll define the overall properties plus two sets of rules.

    -

    jedit/pyweb.xml (180) =

    -
    -<?xml version="1.0"?>
    -<!DOCTYPE MODE SYSTEM "xmode.dtd">
    -
    -<MODE>
    -    →props for JEdit mode (181)
    -    →rules for JEdit PyWeb and RST (182)
    -    →rules for JEdit PyWeb XML-Like Constructs (183)
    -</MODE>
    -
    - -
    -

    jedit/pyweb.xml (180).

    -
    -

    Here are some properties to define RST constructs to JEdit

    -

    props for JEdit mode (181) =

    -
    -<PROPS>
    -    <PROPERTY NAME="lineComment" VALUE=".. "/>
    -    <!-- indent after literal blocks and directives -->
    -    <PROPERTY NAME="indentNextLines" VALUE=".*::$"/>
    -    <!--
    -    <PROPERTY NAME="commentStart" VALUE="@{" />
    -    <PROPERTY NAME="commentEnd" VALUE="@}" />
    -    -->
    -</PROPS>
    -
    - -
    -

    props for JEdit mode (181). Used by: jedit/pyweb.xml (180)

    -
    -

    Here are some rules to define PyWeb and RST constructs to JEdit.

    -

    rules for JEdit PyWeb and RST (182) =

    -
    -<RULES IGNORE_CASE="FALSE" HIGHLIGHT_DIGITS="FALSE">
    -
    -    <!-- targets -->
    -    <EOL_SPAN AT_LINE_START="TRUE" TYPE="KEYWORD3">__</EOL_SPAN>
    -    <EOL_SPAN AT_LINE_START="TRUE" TYPE="KEYWORD3">.. _</EOL_SPAN>
    -
    -    <!-- section titles -->
    -    <SEQ_REGEXP HASH_CHAR="===" TYPE="LABEL">={3,}</SEQ_REGEXP>
    -    <SEQ_REGEXP HASH_CHAR="---" TYPE="LABEL">-{3,}</SEQ_REGEXP>
    -    <SEQ_REGEXP HASH_CHAR="~~~" TYPE="LABEL">~{3,}</SEQ_REGEXP>
    -    <SEQ_REGEXP HASH_CHAR="###" TYPE="LABEL">#{3,}</SEQ_REGEXP>
    -    <SEQ_REGEXP HASH_CHAR='"""' TYPE="LABEL">"{3,}</SEQ_REGEXP>
    -    <SEQ_REGEXP HASH_CHAR="^^^" TYPE="LABEL">\^{3,}</SEQ_REGEXP>
    -    <SEQ_REGEXP HASH_CHAR="+++" TYPE="LABEL">\+{3,}</SEQ_REGEXP>
    -    <SEQ_REGEXP HASH_CHAR="***" TYPE="LABEL">\*{3,}</SEQ_REGEXP>
    -
    -    <!-- replacement -->
    -    <SEQ_REGEXP
    -        HASH_CHAR=".."
    -        AT_LINE_START="TRUE"
    -        TYPE="LITERAL3"
    -    >\.\.\s\|[^|]+\|</SEQ_REGEXP>
    -
    -    <!-- substitution -->
    -    <SEQ_REGEXP
    -        HASH_CHAR="|"
    -        AT_LINE_START="FALSE"
    -        TYPE="LITERAL4"
    -    >\|[^|]+\|</SEQ_REGEXP>
    -
    -    <!-- directives: .. name:: -->
    -    <SEQ_REGEXP
    -        HASH_CHAR=".."
    -        AT_LINE_START="TRUE"
    -        TYPE="LITERAL2"
    -    >\.\.\s[A-z][A-z0-9-_]+::</SEQ_REGEXP>
    -
    -    <!-- strong emphasis: **...** -->
    -    <SEQ_REGEXP
    -        HASH_CHAR="**"
    -        AT_LINE_START="FALSE"
    -        TYPE="KEYWORD2"
    -    >\*\*[^*]+\*\*</SEQ_REGEXP>
    -
    -    <!-- emphasis: *...* -->
    -    <SEQ_REGEXP
    -        HASH_CHAR="*"
    -        AT_LINE_START="FALSE"
    -        TYPE="KEYWORD4"
    -    >\*[^\s*][^*]*\*</SEQ_REGEXP>
    -
    -    <!-- comments -->
    -    <EOL_SPAN AT_LINE_START="TRUE" TYPE="COMMENT1">.. </EOL_SPAN>
    -
    -    <!-- links: `...`_ or `...`__ -->
    -    <SEQ_REGEXP
    -        HASH_CHAR="`"
    -        TYPE="LABEL"
    -    >`[A-z0-9]+[^`]+`_{1,2}</SEQ_REGEXP>
    -
    -    <!-- footnote reference: [0]_ -->
    -    <SEQ_REGEXP
    -        HASH_CHAR="["
    -        TYPE="LABEL"
    -    >\[[0-9]+\]_</SEQ_REGEXP>
    -
    -    <!-- footnote reference: [#]_ or [#foo]_ -->
    -    <SEQ_REGEXP
    -        HASH_CHAR="[#"
    -        TYPE="LABEL"
    -    >\[#[A-z0-9_]*\]_</SEQ_REGEXP>
    -
    -    <!-- footnote reference: [*]_ -->
    -    <SEQ TYPE="LABEL">[*]_</SEQ>
    -
    -    <!-- citation reference: [foo]_ -->
    -    <SEQ_REGEXP
    -        HASH_CHAR="["
    -        TYPE="LABEL"
    -    >\[[A-z][A-z0-9_-]*\]_</SEQ_REGEXP>
    -
    -    <!-- inline literal: ``...``-->
    -    <!--<SEQ_REGEXP
    -        HASH_CHAR="``"
    -        TYPE="LITERAL1"
    -    >``[^`]+``</SEQ_REGEXP>-->
    -    <SPAN TYPE="LITERAL1" ESCAPE="\">
    -        <BEGIN>``</BEGIN>
    -        <END>``</END>
    -    </SPAN>
    -
    -    <!-- interpreted text: `...` -->
    -    <!--
    -    <SEQ_REGEXP
    -        HASH_CHAR="`"
    -        TYPE="KEYWORD1"
    -    >`[^`]+`</SEQ_REGEXP>
    -
    -    -->
    -    <EOL_SPAN TYPE="COMMENT1">@d</EOL_SPAN>
    -    <EOL_SPAN TYPE="COMMENT1">@o</EOL_SPAN>
    -
    -    <SPAN TYPE="COMMENT1" DELEGATE="CODE">
    -        <BEGIN>@{</BEGIN>
    -        <END>@}</END>
    -    </SPAN>
    -
    -    <SPAN TYPE="KEYWORD1">
    -        <BEGIN>`</BEGIN>
    -        <END>`</END>
    -    </SPAN>
    -
    -    <SEQ_REGEXP HASH_CHAR="```" TYPE="LABEL">`{3,}</SEQ_REGEXP>
    -
    -    <!-- :field list: -->
    -    <SEQ_REGEXP
    -        HASH_CHAR=":"
    -        TYPE="KEYWORD1"
    -    >:[A-z][A-z0-9  =\s\t_]*:</SEQ_REGEXP>
    -
    -    <!-- table -->
    -    <SEQ_REGEXP
    -        HASH_CHAR="+-"
    -        TYPE="LABEL"
    -    >\+-[+-]+</SEQ_REGEXP>
    -    <SEQ_REGEXP
    -        HASH_CHAR="+?"
    -        TYPE="LABEL"
    -    >\+=[+=]+</SEQ_REGEXP>
    -
    -</RULES>
    -
    - -
    -

    rules for JEdit PyWeb and RST (182). Used by: jedit/pyweb.xml (180)

    -
    -

    Here are some additional rules to define PyWeb constructs to JEdit -that look like XML.

    -

    rules for JEdit PyWeb XML-Like Constructs (183) =

    -
    -<RULES SET="CODE" DEFAULT="KEYWORD1">
    -    <SPAN TYPE="MARKUP">
    -        <BEGIN>@&lt;</BEGIN>
    -        <END>@&gt;</END>
    -    </SPAN>
    -</RULES>
    -
    - -
    -

    rules for JEdit PyWeb XML-Like Constructs (183). Used by: jedit/pyweb.xml (180)

    -
    -

    Additionally, you'll want to update the JEdit catalog.

    -
    -<?xml version="1.0"?>
    -<!DOCTYPE MODES SYSTEM "catalog.dtd">
    -<MODES>
    -
    -<!-- Add lines like the following, one for each edit mode you add: -->
    -<MODE NAME="pyweb" FILE="pyweb.xml" FILE_NAME_GLOB="*.w"/>
    -
    -</MODES>
    -
    - - -
    -

    To Do

    -
      -
    1. Fix name definition order. There's no good reason why a full name should -be first and elided names defined later.

      -
    2. -
    3. Silence the logging during testing.

      -
    4. -
    5. Add a JSON-based configuration file to configure templates.

      -
        -
      • See the weave.py example. -This removes any need for a weaver command-line option; its defined within the source. -Also, setting the command character can be done in this configuration, too.

        -
      • -
      • An alternative is to get markup templates from a "header" section in the .w file.

        -

        To support reuse over multiple projects, a header could be included with @i. -The downside is that we have a lot of variable = value syntax that makes it -more like a properties file than a .w syntax file. It seems needless to invent -a lot of new syntax just for configuration.

        -
      • +

        To Do

        +
          +
        1. Rename the module from pyweb to pylpweb to avoid name squatting issues. +Rename the project from py-web-tool to py-lpweb.
        2. +
        3. Switch to jinja templates.
            +
          • See the weave.py example. +Defining templates in the source removes any need for a command-line option. A silly optimization. +Setting the "command character" to something other than @ can be done in the configuration, too.
          • +
          • With Jinjda templates can be provided via +a Jinja configuration (there are many choices.) By stepping away from the string.Template, +we can incorporate list-processing {%for%}...{%endfor%} construct that +pushes some processing into the template.
        4. -
        5. JSON-based logging configuration file would be helpful. -Should be separate from template configuration.

          -
        6. -
        7. We might want to decompose the impl.w file: it's huge.

          -
        8. -
        9. We might want to interleave code and test into a document that presents both -side-by-side. They get routed to different output files.

          -
        10. -
        11. Add a @h "header goes here" command to allow weaving any pyWeb required addons to -a LaTeX header, HTML header or RST header. -These are extra ..  include::, \\usepackage{fancyvrb} or maybe an HTML CSS reference -that come from pyWeb and need to be folded into otherwise boilerplate documents.

          -
        12. -
        13. Update the -indent option to accept a numeric argument with the +

        14. Separate TOML-based logging configuration file would be helpful. +Must be separate from template configuration.
        15. +
        16. Rethink the presentation. Are ◊ and → REALLY necessary? +Can we use ◊ and → now that Unicode is more universal? +And why '\N{LOZENGE}'? There's a nice '\N{END OF PROOF}' symbol we could use. +Remove the unused header, docBegin(), and docEnd().
        17. +
        18. Tangling can include non-woven content. More usefully, Weaving can exclude some chunks. +The use case is a book chapter with test cases that are not woven into the text. +Add an option to define tangle-only chunks that are NOT woven into the final document.
        19. +
        20. Update the -indent option on @d chunks to accept a numeric argument with the specific indentation value. This becomes a kind of "noindent" with a given -value. The -noindent would then be the same as -indent 0.

          -
        21. -
        22. Offer a basic XHTML template that uses CDATA sections instead of quoting. -Does require the standard quoting for the CDATA end tag.

          -
        23. -
        24. The createUsedBy() method can be done incrementally by +value. The -noindent would then be the same as -indent 0. +Currently, -indent and -noindent are true/false flags.

        25. +
        26. We might want to decompose the impl.w file: it's huge.
        27. +
        28. We might want to interleave code and test into a document that presents both +side-by-side. We can route to multiple files. +It's a little awkward to create tangled files in multiple directories; +We'd have to use ../tests/whatever.py, assuming we were always using -o src.
        29. +
        30. Fix name definition order. There's no good reason why a full name must +be first and elided names defined later.
        31. +
        32. Offer a basic XHTML template that uses CDATA sections instead of quoting. +Does require the standard quoting for the CDATA end tag.
        33. +
        34. The createUsedBy() method can be done incrementally by accumulating a list of forward references to chunks; as each new chunk is added, any references to the chunk are removed from the forward references list, and a call is made to the Web's setUsage method. References backward to already existing chunks -are easily resolved with a simple lookup.

          -
        35. -
        36. Note that the overall Web is a bit like a NamedChunk that contains Chunks. +are easily resolved with a simple lookup.

        37. +
        38. Note that the overall Web is a bit like a NamedChunk that contains Chunks. This similarity could be factored out. While this will create a more proper Composition pattern implementation, it -leads to the question of why nest @d or @o chunks in the first place?

          -
        39. +leads to the question of why nest @d or @o chunks in the first place?
        -
        -

        Other Thoughts

        -

        There are two possible projects that might prove useful.

        -
          -
        • Jinja2 for better templates.
        • -
        • pyYAML for slightly cleaner encoding of logging configuration -or other configuration.
        • -
        -

        There are advantages and disadvantages to depending on other projects. -The disadvantage is a (very low, but still present) barrier to adoption. -The advantage of adding these two projects might be some simplification.

        - -
        +
    -

    Change Log

    +

    Change Log

    +

    Changes for 3.1

    +
      +
    • Change to Python 3.10 as the supported version.
    • +
    • Add type hints, f-strings, pathlib, abc.ABC.
    • +
    • Replace some complex elif blocks with match statements.
    • +
    • Use pytest as a test runner.
    • +
    • Add a Makefile, pyproject.toml, requirements.txt and requirements-dev.txt.
    • +
    • Implement -o dir option to write output to a directory of choice, simplifying tox setup.
    • +
    • Add bootstrap directory with a snapshot of a previous working release to simplify development.
    • +
    • Add Test cases for weave.py and tangle.py
    • +
    • Replace hand-build mock classes with unittest.mock.Mock objects
    • +
    • Separate the projec into src, tests, examples. Cleanup Makefile, pyproject.toml, etc.
    • +
    • Silence the ERROR-level logging during testing.
    • +
    • Clean up the examples
    • +

    Changes for 3.0

    • Move to GitHub
    • @@ -7071,747 +7071,723 @@

      Change Log

    -

    Indices

    +

    Indices

    -

    Files

    +

    Files

    - - - + - + - - - - - - - - - - - - - - - - - +
    .nojekyll:→(178)
    MANIFEST.in:→(174)
    pyweb.py:→(155)
    README:→(175)
    tangle.py:→(170)
    docutils.conf:→(176)
    index.html:→(179)
    jedit/pyweb.xml:
     →(180)
    page-layout.css:
     →(177)
    pyweb.py:→(153)
    setup.py:→(173)
    tangle.py:→(168)
    weave.py:→(169)
    weave.py:→(171)
    -

    Macros

    +

    Macros

    - + - - + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + + - + +→(59) - + - + - + - + - + - + - + +→(80) - + - + - + - + - + - + - + +→(11) - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - - + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - - - - + - - - - - - - - - - - - - - + + - + - + - + - + +→(174)
    Action call method actually does the real work:
     →(138)
     →(140)
    Action class hierarchy - used to describe basic actions of the application:
     →(136)
    Action class hierarchy - used to describe actions of the application:
     →(138)
    Action final summary of what was done:
     →(139)
     →(141)
    Action superclass has common features of all actions:
     →(137)
     →(139)
    ActionSequence append adds a new action to the sequence:
     →(142)
     →(144)
    ActionSequence call method delegates the sequence of ations:
     →(141)
     →(143)
    ActionSequence subclass that holds a sequence of other actions:
     →(140)
     →(142)
    ActionSequence summary summarizes each step:
     →(143)
     →(145)
    Application Class:
     →(159) →(160)
     →(161) →(162)
    Application class process all files:
     →(163)
     →(165)
    Application default options:
     →(161)
     →(163)
    Application parse command line:
     →(162)
     →(164)
    Base Class Definitions:
     →(1)
     →(1)
    Chunk add to the web:
     →(55)
     →(56)
    Chunk append a command:
     →(53)
     →(54)
    Chunk append text:
     →(54)
     →(55)
    Chunk class:→(52)
    Chunk base class for anonymous chunks of the file:
     →(53)
    Chunk class hierarchy - used to describe input chunks:
     →(51)
     →(52)
    Chunk examination:
     starts with, matches pattern: -→(57)
    Chunk generate references from this Chunk:
     →(58)
     →(60)
    Chunk indent adjustments:
     →(62)
     →(64)
    Chunk references to this Chunk:
     →(59)
     →(61)
    Chunk superclass make Content definition:
     →(56)
     →(57)
    Chunk tangle this Chunk into a code file:
     →(61)
     →(63)
    Chunk weave this Chunk into the documentation:
     →(60)
     →(62)
    CodeCommand class to contain a program source code block:
     →(81)
     →(83)
    Command analysis features:
     starts-with and Regular Expression search: -→(78)
    Command class hierarchy - used to describe individual commands:
     →(76)
     →(78)
    Command superclass:
     →(77)
     →(79)
    Command tangle and weave functions:
     →(79)
     →(81)
    Emitter class hierarchy - used to control output files:
     →(2)
     →(2)
    Emitter core open, close and write:
     →(4)
     →(5)
    Emitter doClose, to be overridden by subclasses:
     →(6)
     →(7)
    Emitter doOpen, to be overridden by subclasses:
     →(5)
     →(6)
    Emitter indent control:
     set, clear and reset: -→(10)
    Emitter superclass:
     →(3)
     →(4)
    Emitter write a block of code:
     →(7) →(8) →(9)
     →(8) →(9) →(10)
    Error class - defines the errors raised:
     →(94)
     →(96)
    FileXrefCommand class for an output file cross-reference:
     →(83)
     →(85)
    HTML code chunk begin:
     →(33)
     →(34)
    HTML code chunk end:
     →(34)
     →(35)
    HTML output file begin:
     →(35)
     →(36)
    HTML output file end:
     →(36)
     →(37)
    HTML reference to a chunk:
     →(39)
     →(40)
    HTML references summary at the end of a chunk:
     →(37)
     →(38)
    HTML short references summary at the end of a chunk:
     →(42)
     →(43)
    HTML simple cross reference markup:
     →(40)
     →(41)
    HTML subclass of Weaver:
     →(31) →(32)
     →(32) →(33)
    HTML write a line of code:
     →(38)
     →(39)
    HTML write user id cross reference line:
     →(41)
     →(42)
    Imports:→(11) →(47) →(96) →(124) →(131) →(133) →(154) →(158) →(164)
    Imports:→(3) →(12) →(48) →(58) →(98) →(124) →(129) →(132) →(134) →(156) →(160) →(166)
    Interface Functions:
     →(167)
     →(169)
    LaTeX code chunk begin:
     →(24)
     →(25)
    LaTeX code chunk end:
     →(25)
     →(26)
    LaTeX file output begin:
     →(26)
     →(27)
    LaTeX file output end:
     →(27)
     →(28)
    LaTeX reference to a chunk:
     →(30)
     →(31)
    LaTeX references summary at the end of a chunk:
     →(28)
     →(29)
    LaTeX subclass of Weaver:
     →(23)
     →(24)
    LaTeX write a line of code:
     →(29)
     →(30)
    LoadAction call method loads the input files:
     →(151)
     →(153)
    LoadAction subclass loads the document web:
     →(150)
     →(152)
    LoadAction summary provides lines read:
     →(152)
     →(154)
    Logging Setup:→(165) →(166)
    Logging Setup:→(167) →(168)
    MacroXrefCommand class for a named chunk cross-reference:
     →(84)
     →(86)
    NamedChunk add to the web:
     →(65)
     →(67)
    NamedChunk class:
     →(63) →(68)
    NamedChunk class for defined names:
     →(65) →(70)
    NamedChunk tangle into the source file:
     →(67)
     →(69)
    NamedChunk user identifiers set and get:
     →(64)
     →(66)
    NamedChunk weave into the documentation:
     →(66)
     →(68)
    NamedDocumentChunk class:
     →(73)
     →(75)
    NamedDocumentChunk tangle:
     →(75)
     →(77)
    NamedDocumentChunk weave:
     →(74)
     →(76)
    Option Parser class - locates optional values on commands:
     →(134) →(135)
     →(135) →(136) →(137)
    OutputChunk add to the web:
     →(70)
     →(72)
    OutputChunk class:
     →(69)
     →(71)
    OutputChunk tangle:
     →(72)
     →(74)
    OutputChunk weave:
     →(71)
     →(73)
    Overheads:→(155) →(156) →(157)
    Overheads:→(157) →(158) →(159)
    RST subclass of Weaver:
     →(22)
     →(23)
    Reference class hierarchy - strategies for references to a chunk:
     →(91) →(92) →(93)
     →(93) →(94) →(95)
    ReferenceCommand class for chunk references:
     →(86)
     →(88)
    ReferenceCommand refers to a chunk:
     →(88)
     →(90)
    ReferenceCommand resolve a referenced chunk name:
     →(87)
     →(89)
    ReferenceCommand tangle a referenced chunk:
     →(90)
     →(92)
    ReferenceCommand weave a reference to a chunk:
     →(89)
     →(91)
    TangleAction call method does tangling of the output files:
     →(148)
     →(150)
    TangleAction subclass initiates the tangle action:
     →(147)
     →(149)
    TangleAction summary method provides total lines tangled:
     →(149)
     →(151)
    Tangler code chunk begin:
     →(45)
     →(46)
    Tangler code chunk end:
     →(46)
     →(47)
    Tangler doOpen, and doClose overrides:
     →(44)
     →(45)
    Tangler subclass of Emitter to create source files with no markup:
     →(43)
     →(44)
    TanglerMake doClose override, comparing temporary to original:
     →(50)
     →(51)
    TanglerMake doOpen override, using a temporary file:
     →(49)
     →(50)
    TanglerMake subclass which is make-sensitive:
     →(48)
     →(49)
    TextCommand class to contain a document text block:
     →(80)
     →(82)
    Tokenizer class - breaks input into tokens:
     →(132)
     →(133)
    UserIdXrefCommand class for a user identifier cross-reference:
     →(85)
     →(87)
    WeaveAction call method to pick the language:
     →(145)
     →(147)
    WeaveAction subclass initiates the weave action:
     →(144)
     →(146)
    WeaveAction summary of language choice:
     →(146)
     →(148)
    Weaver code chunk begin-end:
     →(17)
     →(18)
    Weaver cross reference output methods:
     →(20) →(21)
     →(21) →(22)
    Weaver doOpen, doClose and addIndent overrides:
     →(13)
     →(14)
    Weaver document chunk begin-end:
     →(15)
     →(16)
    Weaver file chunk begin-end:
     →(18)
     →(19)
    Weaver quoted characters:
     →(14)
     →(15)
    Weaver reference command output:
     →(19)
     →(20)
    Weaver reference summary, used by code chunk and file chunk:
     →(16)
     →(17)
    Weaver subclass of Emitter to create documentation:
     →(12)
     →(13)
    Web Chunk check reference counts are all one:
     →(105)
     →(107)
    Web Chunk cross reference methods:
     →(104) →(106) →(107) →(108)
     →(106) →(108) →(109) →(110)
    Web Chunk name resolution methods:
     →(102) →(103)
     →(104) →(105)
    Web add a named macro chunk:
     →(100)
     →(102)
    Web add an anonymous chunk:
     →(99)
     →(101)
    Web add an output file definition chunk:
     →(101)
     →(103)
    Web add full chunk names, ignoring abbreviated names:
     →(98)
     →(100)
    Web class - describes the overall "web" of chunks:
     →(95)
     →(97)
    Web construction methods used by Chunks and WebReader:
     →(97)
     →(99)
    Web determination of the language from the first chunk:
     →(111)
     →(113)
    Web tangle the output files:
     →(112)
     →(114)
    Web weave the output document:
     →(113)
     →(115)
    WebReader class - parses the input file, building the Web structure:
     →(114)
     →(116)
    WebReader command literals:
     →(130)
     →(131)
    WebReader handle a command string:
     →(115) →(127)
     →(117) →(127)
    WebReader load the web:
     →(129)
     →(130)
    WebReader location in the input stream:
     →(128)
     →(128)
    XrefCommand superclass for all cross-reference commands:
     →(82)
     →(84)
    add a reference command to the current chunk:
     →(123)
     →(123)
    add an expression command to the current chunk:
     →(125)
     →(125)
    assign user identifiers to the current chunk:
     →(122)
     →(122)
    collect all user identifiers from a given map into ux:
     →(109)
     →(111)
    double at-sign replacement, append this character to previous TextCommand:
     →(126)
     →(126)
    find user identifier usage and update ux from the given map:
     →(110)
     →(112)
    finish a chunk, start a new Chunk adding it to the web:
     →(120)
    import another file:
     →(119)
     →(121)
    major commands segment the input into separate Chunks:
     →(116)
    minor commands add Commands to the current Chunk:
     →(121)
    props for JEdit mode:
     →(181)
    rules for JEdit PyWeb XML-Like Constructs:
     →(183)
    rules for JEdit PyWeb and RST:
     →(182)
    include another file:
     →(120)
    start a NamedChunk or NamedDocumentChunk, adding it to the web:
     →(118)
     →(119)
    start an OutputChunk, adding it to the web:
     →(117)
     →(118)
    weave.py custom weaver definition to customize the Weaver being used:
     →(171)
     →(173)
    weave.py overheads for correct operation of a script:
     →(170)
     →(172)
    weaver.py processing:
     load and weave the document: -→(172)
    -

    User Identifiers

    +

    User Identifiers

    - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + + + - + - + - + - + - + - + - + + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + + + - + + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - +
    Action:[137] 140 144 147 150
    Action:[139] 142 144 146 149 152
    ActionSequence:[140] 161
    ActionSequence:[142] 163
    Application:[159] 167
    Application:[161] 169
    Chunk:[52] 58 63 90 95 104 119 120 123 129
    Chunk:16 17 18 19 46 47 [53] 59 60 65 79 88 92 93 94 95 97 101 102 103 105 106 110 116 120 121 123 130
    CodeCommand:63 [81]
    CodeCommand:65 [83]
    Command:53 [77] 80 82 86 163
    Command:53 54 55 57 65 75 [79] 82 84 88 165
    Emitter:[3] 12 43
    Emitter:[4] 5 13 44
    Error:58 61 67 75 82 90 [94] 100 102 103 113 118 119 125 135 145 148 151 162
    Error:60 63 69 77 84 92 [96] 102 104 105 115 119 120 125 137 147 150 153 164
    FileXrefCommand:
     [83] 121
     [85] 117
    HTML:31 [32] 111 160 171
    HTML:32 [33] 113 162 173
    LaTeX:[23] 111 160
    LaTeX:[24] 113 162
    LoadAction:[150] 161 168 172
    LoadAction:[152] 163 170 174
    MacroXrefCommand:
     [84] 121
     [86] 117
    NamedChunk:[63] 68 69 73 118
    NamedChunk:59 [65] 70 71 75 119
    NamedDocumentChunk:
     [73] 118
     [75] 119
    OutputChunk:[69] 117
    OutputChunk:[71] 118
    Path:[3] 4 5 53 97 114 116 120 130 163 164 170 172 174
    ReferenceCommand:
     [86] 123
     [88] 123
    TangleAction:[147] 161 168
    TangleAction:[149] 163 170
    Tangler:3 [43] 48 162
    Tangler:4 [44] 49 63 64 69 70 74 77 81 82 83 84 92 114 164
    TanglerMake:[48] 162 166 168 172
    TanglerMake:[49] 164 168 170
    TextCommand:54 56 67 73 [80] 81
    TextCommand:55 57 69 75 [82] 83
    Tokenizer:129 [132]
    Tokenizer:116 130 [133]
    UserIdXrefCommand:
     [85] 121
     [87] 117
    WeaveAction:[146] 163 174
    WeaveAction:[144] 161 172
    Weaver:[13] 23 24 32 61 62 68 73 76 81 82 83 84 85 86 87 91 113 115 164 165
    Weaver:[12] 22 23 31 162 163
    Web:46 53 56 60 62 63 64 67 68 69 70 72 73 74 76 77 81 82 83 84 85 86 87 89 90 91 92 [97] 116 130 139 153 165 170 174
    Web:45 55 65 70 [95] 151 163 168 172 175
    WebReader:[116] 120 130 164 168 170 174
    WebReader:[114] 119 162 166 168 172
    XrefCommand:[84] 85 86 87
    XrefCommand:[82] 83 84 85
    __enter__:[5] 167
    __version__:125 [157]
    __exit__:[5] 167
    _gatherUserId:[108]
    __version__:125 [159] 164
    _updateUserId:[108]
    _gatherUserId:[110]
    add:55 [99]
    _updateUserId:[110]
    addDefName:[98] 100 123
    add:56 [101]
    addIndent:10 [13] 62 66
    addDefName:[100] 102 123
    addNamed:65 [100]
    addIndent:11 [14] 64 68
    addOutput:70 [101]
    addNamed:67 [102]
    append:10 13 53 54 93 99 100 101 104 110 121 123 135 [142]
    addOutput:72 [103]
    appendText:[54] 123 125 126 129
    append:11 14 54 55 95 101 102 103 106 112 117 123 137 [144]
    argparse:[158] 161 162 168 170 172
    appendText:[55] 123 125 126 130
    builtins:[124] 125
    argparse:139 [160] 163 164 165 170 172 174
    chunkXref:84 [107]
    builtins:[124] 125
    close:[4] 13 44 50
    chunkXref:86 [109]
    clrIndent:[10] 62 66 68
    close:[5] 14 45 51
    codeBegin:17 [45] 66 67
    clrIndent:[11] 64 68 70
    codeBlock:[7] 66 81
    codeBegin:18 [46] 68 69
    codeEnd:17 [46] 66 67
    codeBlock:[8] 68 83
    codeFinish:4 9 [13]
    codeEnd:18 [47] 68 69
    createUsedBy:[104] 151
    codeFinish:5 10 [14]
    datetime:125 [154]
    createUsedBy:[106] 153
    doClose:4 6 13 44 [50]
    datetime:125 [156]
    doOpen:4 5 13 44 [49]
    doClose:5 7 14 45 [51]
    docBegin:[15] 60
    doOpen:5 6 14 45 [50]
    docEnd:[15] 60
    docBegin:[16] 62
    duration:[139] 146 149 152
    docEnd:[16] 62
    expand:74 123 161 [162]
    duration:[141] 148 151 154
    expect:117 118 123 125 [127]
    expand:76 123 163 [164]
    fileBegin:18 [35] 71
    expect:118 119 123 125 [127]
    fileEnd:18 [36] 71
    fileBegin:19 [36] 73
    fileXref:83 [107]
    fileEnd:19 [37] 73
    filecmp:[47] 50
    fileXref:85 [109]
    formatXref:[82] 83 84
    filecmp:[48] 51
    fullNameFor:66 71 87 98 [102] 103 104
    formatXref:[84] 85 86
    genReferences:[58] 104
    fullNameFor:68 73 89 100 [104] 105 106
    getUserIDRefs:57 [64] 109
    genReferences:[60] 106
    getchunk:87 [103] 104 113
    getUserIDRefs:59 [66] 111
    handleCommand:[115] 129
    getchunk:89 [105] 106 115
    language:[111] 145 156 175
    handleCommand:[117] 130
    lineNumber:17 18 33 35 45 54 56 [57] 63 67 73 77 80 82 86 119 121 123 125 126 128 129 132 171
    language:[113] 147 158
    load:119 [129] 151 161 163
    lineNumber:18 19 34 36 46 55 57 [59] 65 69 75 79 82 84 88 117 120 123 125 126 128 130 133 173
    location:115 122 125 127 [128]
    load:120 [130] 153 163 165
    logging:3 77 91 95 114 137 159 161 162 163 [164] 165 166 168 170 172
    location:117 122 125 127 [128]
    logging.config:[164] 165
    logging:4 53 79 93 97 116 139 161 163 164 165 [166] 167 168 170 172 174
    main:[167]
    logging.config:[166] 167
    makeContent:54 [56] 63 73
    main:[169] 170 174
    makeContent:55 [57] 65 75
    multi_reference:
     105 [106]
     107 [108]
    no_definition:107 [108]
    no_definition:105 [106]
    no_reference:107 [108]
    no_reference:105 [106]
    open:[5] 14 45 114 115 125 130
    open:[4] 13 44 112 113 125 129
    os:50 51 125 [156]
    os:44 49 50 113 125 [154]
    parse:118 119 [130] 137
    parse:117 118 [129] 135
    parseArgs:[164] 169
    parseArgs:[162] 167
    perform:[153]
    perform:[151]
    platform:[124] 125
    platform:[124] 125
    process:125 [165] 169
    process:125 [163] 167
    quote:[9] 83
    quote:[8] 81
    quoted_chars:9 15 30 [39]
    quoted_chars:8 14 29 [38]
    re:112 [132] 133
    re:110 [131] 132 175
    readdIndent:4 [11] 14
    readdIndent:3 [10] 13
    ref:29 60 [81] 90 101 102 103
    ref:28 58 [79] 88 99 100 101
    referenceSep:[20] 115
    referenceSep:[19] 113
    referenceTo:20 21 [40] 68
    referenceTo:19 20 [39] 66
    references:17 18 19 20 26 33 35 37 [43] 53 60 61 107 122 163 173
    references:16 17 18 19 25 32 34 36 [42] 52 58 105 122 156 161 171
    resolve:69 [89] 90 91 92 105
    resolve:67 [87] 88 89 90 103
    searchForRE:59 [80] 82 112
    searchForRE:57 [78] 80 110
    setIndent:[11] 70
    setUserIDRefs:[64] 122
    setUserIDRefs:59 [66] 122
    shlex:[133] 135
    shlex:[134] 137
    startswith:57 [78] 80 102 111 129 135 163
    startswith:59 [80] 82 104 113 130 137 165
    string:[11] 16 17 18 19 20 21 24 25 28 30 33 34 35 36 37 39 40 41 42 170 171
    string:[12] 17 18 19 20 21 22 25 26 29 31 34 35 36 37 38 40 41 42 43 55 172 173
    summary:139 143 146 149 [152] 163 168 172
    summary:141 145 148 151 [154] 165 170 174
    sys:[124] 125 166
    sys:[124] 125 168 169
    tangle:45 61 67 69 72 73 75 79 80 81 82 90 [112] 148 156 161 168 175
    tangle:46 63 69 71 74 75 77 81 82 83 84 92 [114] 150 163 170
    tempfile:[47] 49
    tempfile:[48] 50
    time:138 139 [154]
    time:125 140 141 [156]
    types:12 125 [154]
    types:13 125 [156]
    usedBy:[88]
    usedBy:[90]
    userNamesXref:85 [108]
    userNamesXref:87 [110]
    weakref:[96] 99 100 101
    weakref:53 [98] 101 102 103
    weave:60 66 71 74 79 80 81 83 84 85 89 [113] 145 156 161 170 175
    weave:62 68 73 76 81 82 83 85 86 87 91 [115] 147 163 172
    weaveChunk:89 [113]
    weaveChunk:91 [115]
    weaveReferenceTo:
     60 66 [74] 113
     62 68 [76] 115
    weaveShortReferenceTo:
     60 66 [74] 113
     62 68 [76] 115
    webAdd:55 65 [70] 117 118 119 120 129
    webAdd:56 67 [72] 118 119 120 121 130
    write:[4] 7 9 17 18 20 21 45 80 113
    write:4 [5] 8 10 18 19 21 22 46 82 115
    xrefDefLine:21 [41] 85
    xrefDefLine:22 [42] 87
    xrefFoot:20 [40] 82 85
    xrefFoot:21 [41] 84 87
    xrefHead:20 [40] 82 85
    xrefHead:21 [41] 84 87
    xrefLine:20 [40] 82
    xrefLine:21 [41] 84

    -Created by /Users/slott/Documents/Projects/PyWebTool-3/pyweb/pyweb.py at Sat Jun 16 08:11:27 2018.
    -

    Source pyweb.w modified Sat Jun 16 08:10:37 2018.

    +Created by /Users/slott/Documents/Projects/py-web-tool/bootstrap/pyweb.py at Mon Jun 13 13:20:24 2022. +

    Source pyweb.w modified Mon Jun 13 08:52:05 2022.

    pyweb.__version__ '3.0'.

    -

    Working directory '/Users/slott/Documents/Projects/PyWebTool-3/pyweb'.

    +

    Working directory '/Users/slott/Documents/Projects/py-web-tool/src'.

    diff --git a/src/pyweb.py b/src/pyweb.py new file mode 100644 index 0000000..2b8780b --- /dev/null +++ b/src/pyweb.py @@ -0,0 +1,2188 @@ +#!/usr/bin/env python +"""py-web-tool Literate Programming. + +Yet another simple literate programming tool derived from **nuweb**, +implemented entirely in Python. +With a suitable configuration, this weaves documents with any markup language, +and tangles source files for any programming language. +""" +__version__ = """3.1""" + +### DO NOT EDIT THIS FILE! +### It was created by /Users/slott/Documents/Projects/py-web-tool/bootstrap/pyweb.py, __version__='3.0'. +### From source pyweb.w modified Mon Jun 13 08:52:05 2022. +### In working directory '/Users/slott/Documents/Projects/py-web-tool/src'. + +from pathlib import Path +import abc + +import string +from textwrap import dedent, indent, shorten + +import tempfile +import filecmp + +from typing import Pattern, Match, Optional, Any, Literal +import weakref + + +import builtins +import sys +import platform + +from typing import TextIO + +import re +from collections.abc import Iterator, Iterable + + +import shlex + + +import os +import time +import datetime +import types + +import argparse + + +import logging +import logging.config + + + + + +class Error(Exception): pass + + + + +class Command(abc.ABC): + """A Command is the lowest level of granularity in the input stream.""" + chunk : "Chunk" + text : str + def __init__(self, fromLine: int = 0) -> None: + self.lineNumber = fromLine+1 # tokenizer is zero-based + self.logger = logging.getLogger(self.__class__.__qualname__) + + def __str__(self) -> str: + return f"at {self.lineNumber!r}" + + def __eq__(self, other: Any) -> bool: + match other: + case Command(): + return self.lineNumber == other.lineNumber and self.text == other.text + case _: + return NotImplemented + + + def startswith(self, prefix: str) -> bool: + return False + def searchForRE(self, rePat: Pattern[str]) -> Match[str] | None: + return None + def indent(self) -> int: + return 0 + + + + def ref(self, aWeb: "Web") -> str | None: + return None + + @abc.abstractmethod + def weave(self, aWeb: "Web", aWeaver: "Weaver") -> None: + ... + + @abc.abstractmethod + def tangle(self, aWeb: "Web", aTangler: "Tangler") -> None: + ... + + + + + + +class TextCommand(Command): + """A piece of document source text.""" + def __init__(self, text: str, fromLine: int = 0) -> None: + super().__init__(fromLine) + self.text = text + def __str__(self) -> str: + return f"at {self.lineNumber!r}: {self.text[:32]!r}..." + def startswith(self, prefix: str) -> bool: + return self.text.startswith(prefix) + def searchForRE(self, rePat: Pattern[str]) -> Match[str] | None: + return rePat.search(self.text) + def indent(self) -> int: + if self.text.endswith('\n'): + return 0 + try: + last_line = self.text.splitlines()[-1] + return len(last_line) + except IndexError: + return 0 + def weave(self, aWeb: "Web", aWeaver: "Weaver") -> None: + aWeaver.write(self.text) + def tangle(self, aWeb: "Web", aTangler: "Tangler") -> None: + aTangler.write(self.text) + + + + +class CodeCommand(TextCommand): + """A piece of program source code.""" + def weave(self, aWeb: "Web", aWeaver: "Weaver") -> None: + aWeaver.codeBlock(aWeaver.quote(self.text)) + def tangle(self, aWeb: "Web", aTangler: "Tangler") -> None: + aTangler.codeBlock(self.text) + + + + +class XrefCommand(Command): + """Any of the Xref-goes-here commands in the input.""" + def __str__(self) -> str: + return f"at {self.lineNumber!r}: cross reference" + + def formatXref(self, xref: dict[str, list[int]], aWeaver: "Weaver") -> None: + aWeaver.xrefHead() + for n in sorted(xref): + aWeaver.xrefLine(n, xref[n]) + aWeaver.xrefFoot() + + def tangle(self, aWeb: "Web", aTangler: "Tangler") -> None: + raise Error('Illegal tangling of a cross reference command.') + + + + +class FileXrefCommand(XrefCommand): + """A FileXref command.""" + def weave(self, aWeb: "Web", aWeaver: "Weaver") -> None: + """Weave a File Xref from @o commands.""" + self.formatXref(aWeb.fileXref(), aWeaver) + + + + +class MacroXrefCommand(XrefCommand): + """A MacroXref command.""" + def weave(self, aWeb: "Web", aWeaver: "Weaver") -> None: + """Weave the Macro Xref from @d commands.""" + self.formatXref(aWeb.chunkXref(), aWeaver) + + + + +class UserIdXrefCommand(XrefCommand): + """A UserIdXref command.""" + def weave(self, aWeb: "Web", aWeaver: "Weaver") -> None: + """Weave a user identifier Xref from @d commands.""" + ux = aWeb.userNamesXref() + if len(ux) != 0: + aWeaver.xrefHead() + for u in sorted(ux): + defn, refList = ux[u] + aWeaver.xrefDefLine(u, defn, refList) + aWeaver.xrefFoot() + else: + aWeaver.xrefEmpty() + + + + +class ReferenceCommand(Command): + """A reference to a named chunk, via @.""" + def __init__(self, refTo: str, fromLine: int = 0) -> None: + super().__init__(fromLine) + self.refTo = refTo + self.fullname = None + self.sequenceList = None + self.chunkList: list[Chunk] = [] + + def __str__(self) -> str: + return "at {self.lineNumber!r}: reference to chunk {self.refTo!r}" + + + def resolve(self, aWeb: "Web") -> None: + """Expand our chunk name and list of parts""" + self.fullName = aWeb.fullNameFor(self.refTo) + self.chunkList = aWeb.getchunk(self.refTo) + + + + def ref(self, aWeb: "Web") -> str: + """Find and return the full name for this reference.""" + self.resolve(aWeb) + return self.fullName + + + + def weave(self, aWeb: "Web", aWeaver: "Weaver") -> None: + """Create the nicely formatted reference to a chunk of code.""" + self.resolve(aWeb) + aWeb.weaveChunk(self.fullName, aWeaver) + + + + def tangle(self, aWeb: "Web", aTangler: "Tangler") -> None: + """Create source code.""" + self.resolve(aWeb) + + self.logger.debug("Indent %r + %r", aTangler.context, self.chunk.previous_command.indent()) + self.chunk.reference_indent(aWeb, aTangler, self.chunk.previous_command.indent()) + + self.logger.debug("Tangling %r with chunks %r", self.fullName, self.chunkList) + if len(self.chunkList) != 0: + for p in self.chunkList: + p.tangle(aWeb, aTangler) + else: + raise Error(f"Attempt to tangle an undefined Chunk, {self.fullName!s}.") + + self.chunk.reference_dedent(aWeb, aTangler) + + + + + + + + +class Chunk: + """Anonymous piece of input file: will be output through the weaver only.""" + web: weakref.ReferenceType["Web"] + previous_command: "Command" + initial: bool + filePath: Path + + def __init__(self) -> None: + self.logger = logging.getLogger(self.__class__.__qualname__) + self.commands: list["Command"] = [] # The list of children of this chunk + self.user_id_list: list[str] = [] + self.name: str = "" + self.fullName: str = "" + self.seq: int = 0 + self.referencedBy: list[Chunk] = [] # Chunks which reference this chunk. Ideally just one. + self.references_list: list[str] = [] # Names that this chunk references + self.refCount = 0 + + def __str__(self) -> str: + return "\n".join(map(str, self.commands)) + def __repr__(self) -> str: + return f"{self.__class__.__name__!s}({self.name!r})" + def __eq__(self, other: Any) -> bool: + match other: + case Chunk(): + return self.name == other.name and self.commands == other.commands + case _: + return NotImplemented + + + def append(self, command: Command) -> None: + """Add another Command to this chunk.""" + self.commands.append(command) + command.chunk = self + + + + def appendText(self, text: str, lineNumber: int = 0) -> None: + """Append a string to the most recent TextCommand.""" + match self.commands: + case [*Command, TextCommand()]: + self.commands[-1].text += text + case _: + self.commands.append(self.makeContent(text, lineNumber)) + + + + def webAdd(self, web: "Web") -> None: + """Add self to a Web as anonymous chunk.""" + web.add(self) + + + + + def genReferences(self, aWeb: "Web") -> Iterator[str]: + """Generate references from this Chunk.""" + try: + for t in self.commands: + ref = t.ref(aWeb) + if ref is not None: + yield ref + except Error as e: + raise + + + + def makeContent(self, text: str, lineNumber: int = 0) -> Command: + return TextCommand(text, lineNumber) + + + + def startswith(self, prefix: str) -> bool: + """Examine the first command's starting text.""" + return len(self.commands) >= 1 and self.commands[0].startswith(prefix) + + def searchForRE(self, rePat: Pattern[str]) -> Optional["Chunk"]: + """Visit each command, applying the pattern.""" + for c in self.commands: + if c.searchForRE(rePat): + return self + return None + + @property + def lineNumber(self) -> int | None: + """Return the first command's line number or None.""" + return self.commands[0].lineNumber if len(self.commands) >= 1 else None + + def setUserIDRefs(self, text: str) -> None: + """Used by NamedChunk subclass.""" + pass + + def getUserIDRefs(self) -> list[str]: + """Used by NamedChunk subclass.""" + return [] + + + + def references(self, theWeaver: "Weaver") -> list[tuple[str, int]]: + """Extract name, sequence from Chunks into a list.""" + return [ + (c.name, c.seq) + for c in theWeaver.reference_style.chunkReferencedBy(self) + ] + + + + def weave(self, aWeb: "Web", aWeaver: "Weaver") -> None: + """Create the nicely formatted document from an anonymous chunk.""" + aWeaver.docBegin(self) + for cmd in self.commands: + cmd.weave(aWeb, aWeaver) + aWeaver.docEnd(self) + def weaveReferenceTo(self, aWeb: "Web", aWeaver: "Weaver") -> None: + """Create a reference to this chunk -- except for anonymous chunks.""" + raise Exception( "Cannot reference an anonymous chunk.""") + def weaveShortReferenceTo(self, aWeb: "Web", aWeaver: "Weaver") -> None: + """Create a short reference to this chunk -- except for anonymous chunks.""" + raise Exception( "Cannot reference an anonymous chunk.""") + + + + def tangle(self, aWeb: "Web", aTangler: "Tangler") -> None: + """Create source code -- except anonymous chunks should not be tangled""" + raise Error('Cannot tangle an anonymous chunk', self) + + + + def reference_indent(self, aWeb: "Web", aTangler: "Tangler", amount: int) -> None: + aTangler.addIndent(amount) # Or possibly set indent to local zero. + + def reference_dedent(self, aWeb: "Web", aTangler: "Tangler") -> None: + aTangler.clrIndent() + + + + + +class NamedChunk(Chunk): + """Named piece of input file: will be output as both tangler and weaver.""" + def __init__(self, name: str) -> None: + super().__init__() + self.name = name + self.user_id_list = [] + self.refCount = 0 + + def __str__(self) -> str: + return f"{self.name!r}: {self!s}" + + def makeContent(self, text: str, lineNumber: int = 0) -> Command: + return CodeCommand(text, lineNumber) + + + def setUserIDRefs(self, text: str) -> None: + """Save user ID's associated with this chunk.""" + self.user_id_list = text.split() + def getUserIDRefs(self) -> list[str]: + return self.user_id_list + + + + def webAdd(self, web: "Web") -> None: + """Add self to a Web as named chunk, update xrefs.""" + web.addNamed(self) + + + + def weave(self, aWeb: "Web", aWeaver: "Weaver") -> None: + """Create the nicely formatted document from a chunk of code.""" + self.fullName = aWeb.fullNameFor(self.name) + aWeaver.addIndent() + aWeaver.codeBegin(self) + for cmd in self.commands: + cmd.weave(aWeb, aWeaver) + aWeaver.clrIndent( ) + aWeaver.codeEnd(self) + def weaveReferenceTo(self, aWeb: "Web", aWeaver: "Weaver") -> None: + """Create a reference to this chunk.""" + self.fullName = aWeb.fullNameFor(self.name) + txt = aWeaver.referenceTo(self.fullName, self.seq) + aWeaver.codeBlock(txt) + def weaveShortReferenceTo(self, aWeb: "Web", aWeaver: "Weaver") -> None: + """Create a shortened reference to this chunk.""" + txt = aWeaver.referenceTo(None, self.seq) + aWeaver.codeBlock(txt) + + + + def tangle(self, aWeb: "Web", aTangler: "Tangler") -> None: + """Create source code. + Use aWeb to resolve @. + Format as correctly indented source text + """ + self.previous_command = TextCommand("", self.commands[0].lineNumber) + aTangler.codeBegin(self) + for t in self.commands: + try: + t.tangle(aWeb, aTangler) + except Error as e: + raise + self.previous_command = t + aTangler.codeEnd(self) + + + + +class NamedChunk_Noindent(NamedChunk): + """Named piece of input file: will be output as both tangler and weaver.""" + def reference_indent(self, aWeb: "Web", aTangler: "Tangler", amount: int) -> None: + aTangler.setIndent(0) + + def reference_dedent(self, aWeb: "Web", aTangler: "Tangler") -> None: + aTangler.clrIndent() + + + +class OutputChunk(NamedChunk): + """Named piece of input file, defines an output tangle.""" + def __init__(self, name: str, comment_start: str = "", comment_end: str = "") -> None: + super().__init__(name) + self.comment_start = comment_start + self.comment_end = comment_end + + def webAdd(self, web: "Web") -> None: + """Add self to a Web as output chunk, update xrefs.""" + web.addOutput(self) + + + + def weave(self, aWeb: "Web", aWeaver: "Weaver") -> None: + """Create the nicely formatted document from a chunk of code.""" + self.fullName = aWeb.fullNameFor(self.name) + aWeaver.fileBegin(self) + for cmd in self.commands: + cmd.weave(aWeb, aWeaver) + aWeaver.fileEnd(self) + + + + def tangle(self, aWeb: "Web", aTangler: "Tangler") -> None: + aTangler.comment_start = self.comment_start + aTangler.comment_end = self.comment_end + super().tangle(aWeb, aTangler) + + + + + +class NamedDocumentChunk(NamedChunk): + """Named piece of input file with document source, defines an output tangle.""" + + def makeContent(self, text: str, lineNumber: int = 0) -> Command: + return TextCommand(text, lineNumber) + + + def weave(self, aWeb: "Web", aWeaver: "Weaver") -> None: + """Ignore this when producing the document.""" + pass + def weaveReferenceTo(self, aWeb: "Web", aWeaver: "Weaver") -> None: + """On a reference to this chunk, expand the body in place.""" + for cmd in self.commands: + cmd.weave(aWeb, aWeaver) + def weaveShortReferenceTo(self, aWeb: "Web", aWeaver: "Weaver") -> None: + """On a reference to this chunk, expand the body in place.""" + self.weaveReferenceTo(aWeb, aWeaver) + + + + def tangle(self, aWeb: "Web", aTangler: "Tangler") -> None: + """Raise an exception on an attempt to tangle.""" + raise Error("Cannot tangle a chunk defined with @[.""") + + + + + + + +class Web: + """The overall Web of chunks.""" + def __init__(self, file_path: Path | None = None) -> None: + self.web_path = file_path + self.chunkSeq: list[Chunk] = [] + self.output: dict[str, list[Chunk]] = {} # Map filename to Chunk + self.named: dict[str, list[Chunk]] = {} # Map chunkname to Chunk + self.sequence = 0 + self.errors = 0 + self.logger = logging.getLogger(self.__class__.__qualname__) + + def __str__(self) -> str: + return f"Web {self.web_path!r}" + + + + def addDefName(self, name: str) -> str | None: + """Reference to or definition of a chunk name.""" + nm = self.fullNameFor(name) + if nm is None: return None + if nm[-3:] == '...': + self.logger.debug("Abbreviated reference %r", name) + return None # first occurance is a forward reference using an abbreviation + if nm not in self.named: + self.named[nm] = [] + self.logger.debug("Adding empty chunk %r", name) + return nm + + + + def add(self, chunk: Chunk) -> None: + """Add an anonymous chunk.""" + self.chunkSeq.append(chunk) + chunk.web = weakref.ref(self) + + + + def addNamed(self, chunk: Chunk) -> None: + """Add a named chunk to a sequence with a given name.""" + self.chunkSeq.append(chunk) + chunk.web = weakref.ref(self) + nm = self.addDefName(chunk.name) + if nm: + # We found the full name for this chunk + self.sequence += 1 + chunk.seq = self.sequence + chunk.fullName = nm + self.named[nm].append(chunk) + chunk.initial = len(self.named[nm]) == 1 + self.logger.debug("Extending chunk %r from %r", nm, chunk.name) + else: + raise Error(f"No full name for {chunk.name!r}", chunk) + + + + def addOutput(self, chunk: Chunk) -> None: + """Add an output chunk to a sequence with a given name.""" + self.chunkSeq.append(chunk) + chunk.web = weakref.ref(self) + if chunk.name not in self.output: + self.output[chunk.name] = [] + self.logger.debug("Adding chunk %r", chunk.name) + self.sequence += 1 + chunk.seq = self.sequence + chunk.fullName = chunk.name + self.output[chunk.name].append(chunk) + chunk.initial = len(self.output[chunk.name]) == 1 + + + + + def fullNameFor(self, name: str) -> str: + """Resolve "..." names into the full name.""" + if name in self.named: + return name + elif name.endswith('...'): + best = [n + for n in self.named + if n.startswith(name[:-3]) + ] + match best: + case []: + return name + case [singleton]: + return singleton + case _: + raise Error(f"Ambiguous abbreviation {name!r}, matches {sorted(best)!r}") + else: + return name + + + def getchunk(self, name: str) -> list[Chunk]: + """Locate a named sequence of chunks.""" + nm = self.fullNameFor(name) + if nm in self.named: + return self.named[nm] + raise Error(f"Cannot resolve {name!r} in {self.named.keys()!r}") + + + + def createUsedBy(self) -> None: + """Update every piece of a Chunk to show how the chunk is referenced. + Each piece can then report where it's used in the web. + """ + for aChunk in self.chunkSeq: + #usage = (self.fullNameFor(aChunk.name), aChunk.seq) + for aRefName in aChunk.genReferences(self): + for c in self.getchunk(aRefName): + c.referencedBy.append(aChunk) + c.refCount += 1 + + + for nm in self.no_reference(): + self.logger.warning("No reference to %r", nm) + for nm in self.multi_reference(): + self.logger.warning("Multiple references to %r", nm) + for nm in self.no_definition(): + self.logger.error("No definition for %r", nm) + self.errors += 1 + + + + def no_reference(self) -> list[str]: + return [nm for nm, cl in self.named.items() if len(cl)>0 and cl[0].refCount == 0] + def multi_reference(self) -> list[str]: + return [nm for nm, cl in self.named.items() if len(cl)>0 and cl[0].refCount > 1] + def no_definition(self) -> list[str]: + return [nm for nm, cl in self.named.items() if len(cl) == 0] + + + def fileXref(self) -> dict[str, list[int]]: + fx = {} + for f, cList in self.output.items(): + fx[f] = [c.seq for c in cList] + return fx + def chunkXref(self) -> dict[str, list[int]]: + mx = {} + for n, cList in self.named.items(): + mx[n] = [c.seq for c in cList] + return mx + + + def userNamesXref(self) -> dict[str, tuple[int, list[int]]]: + ux: dict[str, tuple[int, list[int]]] = {} + self._gatherUserId(self.named, ux) + self._gatherUserId(self.output, ux) + self._updateUserId(self.named, ux) + self._updateUserId(self.output, ux) + return ux + + def _gatherUserId(self, chunkMap: dict[str, list[Chunk]], ux: dict[str, tuple[int, list[int]]]) -> None: + + for n,cList in chunkMap.items(): + for c in cList: + for id in c.getUserIDRefs(): + ux[id] = (c.seq, []) + + + def _updateUserId(self, chunkMap: dict[str, list[Chunk]], ux: dict[str, tuple[int, list[int]]]) -> None: + + # examine source for occurrences of all names in ux.keys() + for id in ux.keys(): + self.logger.debug("References to %r", id) + idpat = re.compile(f'\\W{id}\\W') + for n,cList in chunkMap.items(): + for c in cList: + if c.seq != ux[id][0] and c.searchForRE(idpat): + ux[id][1].append(c.seq) + + + + + def language(self, preferredWeaverClass: type["Weaver"] | None = None) -> "Weaver": + """Construct a weaver appropriate to the document's language""" + if preferredWeaverClass: + return preferredWeaverClass() + self.logger.debug("Picking a weaver based on first chunk %r", str(self.chunkSeq[0])[:4]) + if self.chunkSeq[0].startswith('<'): + return HTML() + if self.chunkSeq[0].startswith('%') or self.chunkSeq[0].startswith('\\'): + return LaTeX() + return RST() + + + + def tangle(self, aTangler: "Tangler") -> None: + for f, c in self.output.items(): + with aTangler.open(Path(f)): + for p in c: + p.tangle(self, aTangler) + + + + def weave(self, aWeaver: "Weaver") -> None: + self.logger.debug("Weaving file from '%s'", self.web_path) + if not self.web_path: + raise Error("No filename supplied for weaving.") + with aWeaver.open(self.web_path): + for c in self.chunkSeq: + c.weave(self, aWeaver) + + def weaveChunk(self, name: str, aWeaver: "Weaver") -> None: + self.logger.debug("Weaving chunk %r", name) + chunkList = self.getchunk(name) + if not chunkList: + raise Error(f"No Definition for {name!r}") + chunkList[0].weaveReferenceTo(self, aWeaver) + for p in chunkList[1:]: + aWeaver.write(aWeaver.referenceSep()) + p.weaveShortReferenceTo(self, aWeaver) + + + + + + +class Tokenizer(Iterator[str]): + def __init__(self, stream: TextIO, command_char: str='@') -> None: + self.command = command_char + self.parsePat = re.compile(f'({self.command}.|\\n)') + self.token_iter = (t for t in self.parsePat.split(stream.read()) if len(t) != 0) + self.lineNumber = 0 + + def __next__(self) -> str: + token = next(self.token_iter) + self.lineNumber += token.count('\n') + return token + + def __iter__(self) -> Iterator[str]: + return self + + + + +class ParseError(Exception): pass + +class OptionDef: + def __init__(self, name: str, **kw: Any) -> None: + self.name = name + self.__dict__.update(kw) + +class OptionParser: + def __init__(self, *arg_defs: Any) -> None: + self.args = dict((arg.name, arg) for arg in arg_defs) + self.trailers = [k for k in self.args.keys() if not k.startswith('-')] + + def parse(self, text: str) -> dict[str, list[str]]: + try: + word_iter = iter(shlex.split(text)) + except ValueError as e: + raise Error(f"Error parsing options in {text!r}") + options = dict(self._group(word_iter)) + return options + + def _group(self, word_iter: Iterator[str]) -> Iterator[tuple[str, list[str]]]: + option: str | None + value: list[str] + final: list[str] + option, value, final = None, [], [] + for word in word_iter: + if word == '--': + if option: + yield option, value + try: + final = [next(word_iter)] + except StopIteration: + final = [] # Special case of '--' at the end. + break + elif word.startswith('-'): + if word in self.args: + if option: + yield option, value + option, value = word, [] + else: + raise ParseError(f"Unknown option {word!r}") + else: + if option: + if self.args[option].nargs == len(value): + yield option, value + final = [word] + break + else: + value.append(word) + else: + final = [word] + break + # In principle, we step through the trailers based on nargs counts. + for word in word_iter: + final.append(word) + yield self.trailers[0], final + + + +class WebReader: + """Parse an input file, creating Chunks and Commands.""" + + output_option_parser = OptionParser( + OptionDef("-start", nargs=1, default=None), + OptionDef("-end", nargs=1, default=""), + OptionDef("argument", nargs='*'), + ) + + definition_option_parser = OptionParser( + OptionDef("-indent", nargs=0), + OptionDef("-noindent", nargs=0), + OptionDef("argument", nargs='*'), + ) + + # Configuration + command: str + permitList: list[str] + base_path : Path + + # State of the reader + _source: TextIO + filePath: Path + theWeb: "Web" + tokenizer: Tokenizer + aChunk: Chunk + + def __init__(self, parent: Optional["WebReader"] = None) -> None: + self.logger = logging.getLogger(self.__class__.__qualname__) + + # Configuration of this reader. + self.parent = parent + if self.parent: + self.command = self.parent.command + self.permitList = self.parent.permitList + else: # Defaults until overridden + self.command = '@' + self.permitList = [] + + # Summary + self.totalLines = 0 + self.totalFiles = 0 + self.errors = 0 + + + # Structural ("major") commands + self.cmdo = self.command+'o' + self.cmdd = self.command+'d' + self.cmdlcurl = self.command+'{' + self.cmdrcurl = self.command+'}' + self.cmdlbrak = self.command+'[' + self.cmdrbrak = self.command+']' + self.cmdi = self.command+'i' + + # Inline ("minor") commands + self.cmdlangl = self.command+'<' + self.cmdrangl = self.command+'>' + self.cmdpipe = self.command+'|' + self.cmdlexpr = self.command+'(' + self.cmdrexpr = self.command+')' + self.cmdcmd = self.command+self.command + + # Content "minor" commands + self.cmdf = self.command+'f' + self.cmdm = self.command+'m' + self.cmdu = self.command+'u' + + + def __str__(self) -> str: + return self.__class__.__name__ + + + def location(self) -> tuple[str, int]: + return (str(self.filePath), self.tokenizer.lineNumber+1) + + + + + def load(self, web: "Web", filepath: Path, source: TextIO | None = None) -> "WebReader": + self.theWeb = web + self.filePath = filepath + self.base_path = self.filePath.parent + + # Only set the a web's filename once using the first file. + # **TODO:** this should be a setter property of the web. + if self.theWeb.web_path is None: + self.theWeb.web_path = self.filePath + + if source: + self._source = source + self.parse_source() + else: + with self.filePath.open() as self._source: + self.parse_source() + return self + + def parse_source(self) -> None: + self.tokenizer = Tokenizer(self._source, self.command) + self.totalFiles += 1 + + self.aChunk = Chunk() # Initial anonymous chunk of text. + self.aChunk.webAdd(self.theWeb) + + for token in self.tokenizer: + if len(token) >= 2 and token.startswith(self.command): + if self.handleCommand(token): + continue + else: + self.logger.error('Unknown @-command in input: %r', token) + self.aChunk.appendText(token, self.tokenizer.lineNumber) + elif token: + # Accumulate a non-empty block of text in the current chunk. + self.aChunk.appendText(token, self.tokenizer.lineNumber) + else: + # Whitespace + pass + + + + + def handleCommand(self, token: str) -> bool: + self.logger.debug("Reading %r", token) + + match token[:2]: + case self.cmdo: + + args = next(self.tokenizer) + self.expect((self.cmdlcurl,)) + options = self.output_option_parser.parse(args) + self.aChunk = OutputChunk( + name=' '.join(options['argument']), + comment_start=''.join(options.get('start', "# ")), + comment_end=''.join(options.get('end', "")), + ) + self.aChunk.filePath = self.filePath + self.aChunk.webAdd(self.theWeb) + # capture an OutputChunk up to @} + + case self.cmdd: + + args = next(self.tokenizer) + brack = self.expect((self.cmdlcurl,self.cmdlbrak)) + options = self.output_option_parser.parse(args) + name = ' '.join(options['argument']) + + if brack == self.cmdlbrak: + self.aChunk = NamedDocumentChunk(name) + elif brack == self.cmdlcurl: + if '-noindent' in options: + self.aChunk = NamedChunk_Noindent(name) + else: + self.aChunk = NamedChunk(name) + elif brack == None: + pass # Error noted by expect() + else: + raise Error("Design Error") + + self.aChunk.filePath = self.filePath + self.aChunk.webAdd(self.theWeb) + # capture a NamedChunk up to @} or @] + + case self.cmdi: + + incPath = Path(next(self.tokenizer).strip()) + try: + include = WebReader(parent=self) + if not incPath.is_absolute(): + incPath = self.base_path / incPath + self.logger.info("Including '%s'", incPath) + include.load(self.theWeb, incPath) + self.totalLines += include.tokenizer.lineNumber + self.totalFiles += include.totalFiles + if include.errors: + self.errors += include.errors + self.logger.error("Errors in included file '%s', output is incomplete.", incPath) + except Error as e: + self.logger.error("Problems with included file '%s', output is incomplete.", incPath) + self.errors += 1 + except IOError as e: + self.logger.error("Problems finding included file '%s', output is incomplete.", incPath) + # Discretionary -- sometimes we want to continue + if self.cmdi in self.permitList: pass + else: raise # Seems heavy-handed, but, the file wasn't found! + self.aChunk = Chunk() + self.aChunk.webAdd(self.theWeb) + + case self.cmdrcurl | self.cmdrbrak: + + self.aChunk = Chunk() + self.aChunk.webAdd(self.theWeb) + + case self.cmdpipe: + + try: + self.aChunk.setUserIDRefs(next(self.tokenizer).strip()) + except AttributeError: + # Out of place @| user identifier command + self.logger.error("Unexpected references near %r: %r", self.location(), token) + self.errors += 1 + + case self.cmdf: + self.aChunk.append(FileXrefCommand(self.tokenizer.lineNumber)) + case self.cmdm: + self.aChunk.append(MacroXrefCommand(self.tokenizer.lineNumber)) + case self.cmdu: + self.aChunk.append(UserIdXrefCommand(self.tokenizer.lineNumber)) + case self.cmdlangl: + + # get the name, introduce into the named Chunk dictionary + expand = next(self.tokenizer).strip() + closing = self.expect((self.cmdrangl,)) + self.theWeb.addDefName(expand) + self.aChunk.append(ReferenceCommand(expand, self.tokenizer.lineNumber)) + self.aChunk.appendText("", self.tokenizer.lineNumber) # to collect following text + self.logger.debug("Reading %r %r", expand, closing) + + case self.cmdlexpr: + + # get the Python expression, create the expression result + expression = next(self.tokenizer) + self.expect((self.cmdrexpr,)) + try: + # Build Context + # **TODO:** Parts of this are static. + dangerous = { + 'breakpoint', 'compile', 'eval', 'exec', 'execfile', 'globals', 'help', 'input', + 'memoryview', 'open', 'print', 'super', '__import__' + } + safe = types.SimpleNamespace(**dict( + (name, obj) + for name,obj in builtins.__dict__.items() + if name not in dangerous + )) + globals = dict( + __builtins__=safe, + os=types.SimpleNamespace(path=os.path, getcwd=os.getcwd, name=os.name), + time=time, + datetime=datetime, + platform=platform, + theLocation=str(self.location()), + theWebReader=self, + theFile=self.theWeb.web_path, + thisApplication=sys.argv[0], + __version__=__version__, # Legacy compatibility. Deprecated. + version=__version__, + ) + # Evaluate + result = str(eval(expression, globals)) + except Exception as exc: + self.logger.error('Failure to process %r: result is %r', expression, exc) + self.errors += 1 + result = f"@({expression!r}: Error {exc!r}@)" + self.aChunk.appendText(result, self.tokenizer.lineNumber) + + case self.cmdcmd: + + self.aChunk.appendText(self.command, self.tokenizer.lineNumber) + + case self.cmdlcurl | self.cmdlbrak: + # These should have been consumed as part of @o and @d parsing + self.logger.error("Extra %r (possibly missing chunk name) near %r", token, self.location()) + self.errors += 1 + case _: + return False # did not recogize the command + return True # did recognize the command + + + def expect(self, tokens: Iterable[str]) -> str | None: + try: + t = next(self.tokenizer) + while t == '\n': + t = next(self.tokenizer) + except StopIteration: + self.logger.error("At %r: end of input, %r not found", self.location(), tokens) + self.errors += 1 + return None + if t not in tokens: + self.logger.error("At %r: expected %r, found %r", self.location(), tokens, t) + self.errors += 1 + return None + return t + + + + + + +class Emitter: + """Emit an output file; handling indentation context.""" + + code_indent = 0 #: Used by a Tangler + filePath : Path #: Path within the base directory (on the name is used) + output : Path #: Base directory to write + theFile: TextIO #: Open file being written + + def __init__(self) -> None: + self.logger = logging.getLogger(self.__class__.__qualname__) + self.log_indent = logging.getLogger("indent." + self.__class__.__qualname__) + # Working State + self.lastIndent = 0 + self.fragment = False + self.context: list[int] = [] + self.readdIndent(self.code_indent) # Create context and initial lastIndent values + # Summary + self.linesWritten = 0 + self.totalFiles = 0 + self.totalLines = 0 + + def __str__(self) -> str: + return self.__class__.__name__ + + + def open(self, aPath: Path) -> "Emitter": + """Open a file.""" + if not hasattr(self, 'output'): + self.output = Path.cwd() + self.filePath = self.output / aPath.name + self.logger.debug(f"Writing to {self.output} / {aPath.name} == {self.filePath}") + self.linesWritten = 0 + self.doOpen() + return self + + + def doOpen(self) -> None: + self.logger.debug("Creating %r", self.filePath) + + + + def close(self) -> None: + self.codeFinish() # Trailing newline for tangler only. + self.doClose() + self.totalFiles += 1 + self.totalLines += self.linesWritten + + + def doClose(self) -> None: + self.logger.debug("Wrote %d lines to %r", self.linesWritten, self.filePath) + + + + def write(self, text: str) -> None: + if text is None: return + self.theFile.write(text) + self.linesWritten += text.count('\n') + + # Context Manager Interface -- used by ``open()`` method + def __enter__(self) -> "Emitter": + return self + + def __exit__(self, *exc: Any) -> Literal[False]: + self.close() + return False + + + + def codeBlock(self, text: str) -> None: + """Indented write of a block of code. + Buffers the spaces from the last line provided to act as the indent for the next line. + """ + indent = self.context[-1] + lines = text.split('\n') + if len(lines) == 1: + # Fragment with no newline. + self.logger.debug("Fragment: %d, %r", self.lastIndent, lines[0]) + self.write(f"{self.lastIndent*' '!s}{lines[0]!s}") + self.lastIndent = 0 + self.fragment = True + else: + # Multiple lines with one or more newlines. + first, rest = lines[:1], lines[1:] + self.logger.debug("First Line: %d, %r", self.lastIndent, first[0]) + self.write(f"{self.lastIndent*' '!s}{first[0]!s}\n") + for l in rest[:-1]: + self.logger.debug("Next Line: %d, %r", indent, l) + self.write(f"{indent*' '!s}{l!s}\n") + if rest[-1]: + # Last line is non-empty. + self.logger.debug("Last (Partial) Line: %d, %r", indent, rest[-1]) + self.write(f"{indent*' '!s}{rest[-1]!s}") + self.lastIndent = 0 + self.fragment = True + else: + # Last line was empty, a trailing newline. + self.logger.debug("Last (Empty) Line: indent is %d", len(rest[-1]) + indent) + # Buffer the next indent + self.lastIndent = len(rest[-1]) + indent + self.fragment = False + + + quoted_chars: list[tuple[str, str]] = [ + # Must be empty for tangling. + ] + + def quote(self, aLine: str) -> str: + """Each individual line of code; often overridden by weavers to quote the code.""" + clean = aLine + for from_, to_ in self.quoted_chars: + clean = clean.replace(from_, to_) + return clean + + + def codeFinish(self) -> None: + if self.fragment: + self.write('\n') + + + + def addIndent(self, increment: int) -> None: + self.lastIndent = self.context[-1]+increment + self.context.append(self.lastIndent) + self.log_indent.debug("addIndent %d: %r", increment, self.context) + + def setIndent(self, indent: int) -> None: + self.context.append(indent) + self.lastIndent = self.context[-1] + self.log_indent.debug("setIndent %d: %r", indent, self.context) + + def clrIndent(self) -> None: + if len(self.context) > 1: + self.context.pop() + self.lastIndent = self.context[-1] + self.log_indent.debug("clrIndent %r", self.context) + + def readdIndent(self, indent: int = 0) -> None: + """Resets the indentation context.""" + self.lastIndent = indent + self.context = [self.lastIndent] + self.log_indent.debug("readdIndent %d: %r", indent, self.context) + + + + + + +class Weaver(Emitter): + """Format various types of XRef's and code blocks when weaving. + + For RST format we splice in the following two lines + :: + + .. include:: + .. include:: + """ + extension = ".rst" + code_indent = 4 + # Not actually used. + header = dedent(""" + .. include:: + .. include:: + """) + + reference_style : "Reference" + + def __init__(self) -> None: + super().__init__() + + + def doOpen(self) -> None: + """Create the final woven document.""" + self.filePath = self.filePath.with_suffix(self.extension) + self.logger.info("Weaving '%s'", self.filePath) + self.theFile = self.filePath.open("w") + self.readdIndent(self.code_indent) + + def doClose(self) -> None: + self.theFile.close() + self.logger.info("Wrote %d lines to %r", self.linesWritten, self.filePath) + + def addIndent(self, increment: int = 0) -> None: + """increment not used when weaving""" + self.context.append(self.context[-1]) + self.log_indent.debug("addIndent %d: %r", self.lastIndent, self.context) + + def codeFinish(self) -> None: + pass # Not needed when weaving + + + + # Template Expansions. + + + # Prevent some RST markup from being recognized (and processed) in code. + quoted_chars: list[tuple[str, str]] = [ + ('\\', r'\\'), # Must be first. + ('`', r'\`'), + ('_', r'\_'), + ('*', r'\*'), + ('|', r'\|'), + ] + + + def docBegin(self, aChunk: Chunk) -> None: + pass + + def docEnd(self, aChunk: Chunk) -> None: + pass + + + + ref_template = string.Template( + "${refList}" + ) + ref_separator = "; " + ref_item_template = string.Template( + "$fullName (`${seq}`_)" + ) + + def references(self, aChunk: Chunk) -> str: + references = aChunk.references(self) + if len(references) != 0: + refList = [ + self.ref_item_template.substitute(seq=s, fullName=n) + for n, s in references + ] + return self.ref_template.substitute(refList=self.ref_separator.join(refList)) + else: + return "" + + + + cb_template = string.Template( + dedent(""" + .. _`${seq}`: + .. rubric:: ${fullName} (${seq}) ${concat} + .. parsed-literal:: + :class: code + + """) + ) + + ce_template = string.Template( + dedent(""" + .. + + .. class:: small + + |loz| *${fullName} (${seq})*. Used by: ${references} + """) + ) + + def codeBegin(self, aChunk: Chunk) -> None: + txt = self.cb_template.substitute( + seq=aChunk.seq, + lineNumber=aChunk.lineNumber, + fullName=aChunk.fullName, + concat="=" if aChunk.initial else "+=", + ) + self.write(txt) + + def codeEnd(self, aChunk: Chunk) -> None: + txt = self.ce_template.substitute( + seq = aChunk.seq, + lineNumber = aChunk.lineNumber, + fullName = aChunk.fullName, + references = self.references(aChunk), + ) + self.write(txt) + + + + fb_template = string.Template( + dedent(""" + .. _`${seq}`: + .. rubric:: ${fullName} (${seq}) ${concat} + .. parsed-literal:: + :class: code + + """) + ) + + fe_template = string.Template( + dedent(""" + .. + + .. class:: small + + |loz| *${fullName} (${seq})*. + """) + ) + + def fileBegin(self, aChunk: Chunk) -> None: + txt = self.fb_template.substitute( + seq=aChunk.seq, + lineNumber=aChunk.lineNumber, + fullName=aChunk.fullName, + concat="=" if aChunk.initial else "+=", + ) + self.write(txt) + + def fileEnd(self, aChunk: Chunk) -> None: + assert len(self.references(aChunk)) == 0 + txt = self.fe_template.substitute( + seq=aChunk.seq, + lineNumber=aChunk.lineNumber, + fullName=aChunk.fullName, + references=[]) + self.write(txt) + + + + refto_name_template = string.Template( + r"|srarr|\ ${fullName} (`${seq}`_)" + ) + refto_seq_template = string.Template( + r"|srarr|\ (`${seq}`_)" + ) + refto_seq_separator = ", " + + def referenceTo(self, aName: str | None, seq: int) -> str: + """Weave a reference to a chunk. + Provide name to get a full reference. + name=None to get a short reference. + """ + if aName: + return self.refto_name_template.substitute(fullName=aName, seq=seq) + else: + return self.refto_seq_template.substitute(seq=seq) + + def referenceSep(self) -> str: + """Separator between references.""" + return self.refto_seq_separator + + + + xref_head_template = string.Template( + dedent(""" + """) + ) + xref_foot_template = string.Template( + dedent(""" + """) + ) + xref_item_template = string.Template( + dedent(""" :${fullName}: + ${refList} + """) + ) + xref_empty_template = string.Template( + dedent(""" (None) + """) + ) + + def xrefHead(self) -> None: + txt = self.xref_head_template.substitute() + self.write(txt) + + def xrefFoot(self) -> None: + txt = self.xref_foot_template.substitute() + self.write(txt) + + def xrefLine(self, name: str, refList: list[int]) -> None: + refList_txt = [self.referenceTo(None, r) for r in refList] + txt = self.xref_item_template.substitute(fullName=name, refList = " ".join(refList_txt)) # RST Separator + self.write(txt) + + def xrefEmpty(self) -> None: + self.write(self.xref_empty_template.substitute()) + + name_def_template = string.Template( + '[`${seq}`_]' + ) + name_ref_template = string.Template( + '`${seq}`_' + ) + + def xrefDefLine(self, name: str, defn: int, refList: list[int]) -> None: + """Special template for the definition, default reference for all others.""" + templates = {defn: self.name_def_template} + refTxt = [ + templates.get(r, self.name_ref_template).substitute(seq=r) + for r in sorted(refList + [defn]) + ] + # Generic space separator + txt = self.xref_item_template.substitute(fullName=name, refList=" ".join(refTxt)) + self.write(txt) + + + + + +class RST(Weaver): + pass + + +class LaTeX(Weaver): + """LaTeX formatting for XRef's and code blocks when weaving. + Requires ``\\usepackage{fancyvrb}`` + """ + extension = ".tex" + code_indent = 0 + # Not actually used + header = dedent(""" + \\usepackage{fancyvrb} + """) + + + cb_template = string.Template( + dedent(""" \\label{pyweb${seq}} + \\begin{flushleft} + \\textit{Code example ${fullName} (${seq})} + \\begin{Verbatim}[commandchars=\\\\\\{\\},codes={\\catcode`$$=3\\catcode`^=7},frame=single] + """) + ) + + + + ce_template = string.Template( + dedent(""" + \\end{Verbatim} + ${references} + \\end{flushleft} + """) + ) + + + + fb_template = cb_template + + + + fe_template = ce_template + + + + ref_item_template = string.Template( + indent( + dedent(""" + \\item Code example ${fullName} (${seq}) (Sect. \\ref{pyweb${seq}}, p. \\pageref{pyweb${seq}}) + """), + ' ' + ) + ) + + ref_template = string.Template( + indent( + dedent(""" + \\footnotesize + Used by: + \\begin{list}{}{} + ${refList} + \\end{list} + \\normalsize"""), + ' ' + ) + ) + + + + quoted_chars: list[tuple[str, str]] = [ + ("\\end{Verbatim}", "\\end\\,{Verbatim}"), # Allow \end{Verbatim} in a Verbatim context + ("\\{", "\\\\,{"), # Prevent unexpected commands in Verbatim + ("$", "\\$"), # Prevent unexpected math in Verbatim + ] + + + + refto_name_template = string.Template( + """$$\\triangleright$$ Code Example ${fullName} (${seq})""" + ) + + refto_seq_template = string.Template( + """(${seq})""" + ) + + + + + +class HTML(Weaver): + """HTML formatting for XRef's and code blocks when weaving.""" + extension = ".html" + code_indent = 0 + header = "" + + + cb_template = string.Template( + indent( + dedent(""" + + +

    ${fullName} (${seq}) ${concat}

    +
    
    +                """),
    +            '    '
    +        )
    +    )
    +    
    +
    +        
    +    ce_template = string.Template(
    +        indent(
    +            dedent("""
    +                
    +

    ${fullName} (${seq}). + ${references} +

    + """), + ' ' + ) + ) + + + + fb_template = string.Template( + indent( + dedent(""" + +

    ``${fullName}`` (${seq}) ${concat}

    +
    
    +            """), # No leading \\n.
    +            '    '
    +        )
    +    )
    +    
    +
    +        
    +    fe_template = string.Template(
    +        indent(
    +            dedent("""            
    +

    ◊ ``${fullName}`` (${seq}). + ${references} +

    + """), + ' ' + ) + ) + + + + ref_item_template = string.Template( + '${fullName} (${seq})' + ) + + ref_template = string.Template( + ' Used by ${refList}.' + ) + + + + quoted_chars: list[tuple[str, str]] = [ + ("&", "&"), # Must be first + ("<", "<"), + (">", ">"), + ('"', """), + ] + + + + refto_name_template = string.Template( + '${fullName} (${seq})' + ) + + refto_seq_template = string.Template( + '(${seq})' + ) + + + + xref_head_template = string.Template( + dedent("""
    + """) + ) + xref_foot_template = string.Template( + dedent("""
    + """) + ) + xref_item_template = string.Template( + dedent("""
    ${fullName}
    ${refList}
    + """) + ) + + + name_def_template = string.Template( + '•${seq}' + ) + + name_ref_template = string.Template( + '${seq}' + ) + + + + + + +class HTMLShort(HTML): + """HTML formatting for XRef's and code blocks when weaving with short references.""" + + ref_item_template = string.Template( + '(${seq})' + ) + + + + + + +class Tangler(Emitter): + """Tangle output files.""" + def __init__(self) -> None: + super().__init__() + self.comment_start: str = "#" + self.comment_end: str = "" + self.include_line_numbers = False + + + def checkPath(self) -> None: + self.filePath.parent.mkdir(parents=True, exist_ok=True) + + def doOpen(self) -> None: + """Tangle out of the output files.""" + self.checkPath() + self.theFile = self.filePath.open("w") + self.logger.info("Tangling '%s'", self.filePath) + + def doClose(self) -> None: + self.theFile.close() + self.logger.info("Wrote %d lines to %r", self.linesWritten, self.filePath) + + + + def codeBegin(self, aChunk: Chunk) -> None: + self.log_indent.debug(" None: + self.log_indent.debug(">%r", aChunk.fullName) + + + + + +class TanglerMake(Tangler): + """Tangle output files, leaving files untouched if there are no changes.""" + tempname : str + def __init__(self, *args: Any) -> None: + super().__init__(*args) + + + def doOpen(self) -> None: + fd, self.tempname = tempfile.mkstemp(dir=os.curdir) + self.theFile = os.fdopen(fd, "w") + self.logger.info("Tangling '%s'", self.filePath) + + + + + def doClose(self) -> None: + self.theFile.close() + try: + same = filecmp.cmp(self.tempname, self.filePath) + except OSError as e: + same = False # Doesn't exist. (Could check for errno.ENOENT) + if same: + self.logger.info("Unchanged '%s'", self.filePath) + os.remove(self.tempname) + else: + # Windows requires the original file name be removed first. + try: + self.filePath.unlink() + except OSError as e: + pass # Doesn't exist. (Could check for errno.ENOENT) + self.checkPath() + self.filePath.hardlink_to(self.tempname) + os.remove(self.tempname) + self.logger.info("Wrote %d lines to %s", self.linesWritten, self.filePath) + + + + + + + +class Reference(abc.ABC): + def __init__(self) -> None: + self.logger = logging.getLogger(self.__class__.__qualname__) + + @abc.abstractmethod + def chunkReferencedBy(self, aChunk: Chunk) -> list[Chunk]: + """Return a list of Chunks.""" + ... + +class SimpleReference(Reference): + def chunkReferencedBy(self, aChunk: Chunk) -> list[Chunk]: + refBy = aChunk.referencedBy + return refBy + +class TransitiveReference(Reference): + def chunkReferencedBy(self, aChunk: Chunk) -> list[Chunk]: + refBy = aChunk.referencedBy + self.logger.debug("References: %r(%d) %r", aChunk.name, aChunk.seq, refBy) + return self.allParentsOf(refBy) + def allParentsOf(self, chunkList: list[Chunk], depth: int = 0) -> list[Chunk]: + """Transitive closure of parents via recursive ascent. + """ + final = [] + for c in chunkList: + final.append(c) + final.extend(self.allParentsOf(c.referencedBy, depth+1)) + self.logger.debug(f"References: {'--':>{2*depth}s} {final!s}") + return final + + + + +class Action: + """An action performed by pyWeb.""" + options : argparse.Namespace + web : "Web" + def __init__(self, name: str) -> None: + self.name = name + self.start: float | None = None + self.logger = logging.getLogger(self.__class__.__qualname__) + + def __str__(self) -> str: + return f"{self.name!s} [{self.web!s}]" + + + def __call__(self) -> None: + self.logger.info("Starting %s", self.name) + self.start = time.process_time() + + + + + def duration(self) -> float: + """Return duration of the action.""" + return (self.start and time.process_time()-self.start) or 0 + + def summary(self) -> str: + return f"{self.name!s} in {self.duration():0.3f} sec." + + + + + +class ActionSequence(Action): + """An action composed of a sequence of other actions.""" + def __init__(self, name: str, opSequence: list[Action] | None = None) -> None: + super().__init__(name) + if opSequence: self.opSequence = opSequence + else: self.opSequence = [] + + def __str__(self) -> str: + return "; ".join([str(x) for x in self.opSequence]) + + + def __call__(self) -> None: + super().__call__() + for o in self.opSequence: + o.web = self.web + o.options = self.options + o() + + + + + def append(self, anAction: Action) -> None: + self.opSequence.append(anAction) + + + + + def summary(self) -> str: + return ", ".join([o.summary() for o in self.opSequence]) + + + + + +class WeaveAction(Action): + """Weave the final document.""" + def __init__(self) -> None: + super().__init__("Weave") + + def __str__(self) -> str: + return f"{self.name!s} [{self.web!s}, {self.options.theWeaver!s}]" + + + def __call__(self) -> None: + super().__call__() + if not self.options.theWeaver: + # Examine first few chars of first chunk of web to determine language + self.options.theWeaver = self.web.language() + self.logger.info("Using %s", self.options.theWeaver.__class__.__name__) + self.options.theWeaver.reference_style = self.options.reference_style + self.options.theWeaver.output = self.options.output + try: + self.web.weave(self.options.theWeaver) + self.logger.info("Finished Normally") + except Error as e: + self.logger.error("Problems weaving document from %r (weave file is faulty).", self.web.web_path) + #raise + + + + + def summary(self) -> str: + if self.options.theWeaver and self.options.theWeaver.linesWritten > 0: + return ( + f"{self.name!s} {self.options.theWeaver.linesWritten:d} lines in {self.duration():0.3f} sec." + ) + return f"did not {self.name!s}" + + + + + +class TangleAction(Action): + """Tangle source files.""" + def __init__(self) -> None: + super().__init__("Tangle") + + + def __call__(self) -> None: + super().__call__() + self.options.theTangler.include_line_numbers = self.options.tangler_line_numbers + self.options.theTangler.output = self.options.output + try: + self.web.tangle(self.options.theTangler) + except Error as e: + self.logger.error("Problems tangling outputs from %r (tangle files are faulty).", self.web.web_path) + #raise + + + + + def summary(self) -> str: + if self.options.theTangler and self.options.theTangler.linesWritten > 0: + return ( + f"{self.name!s} {self.options.theTangler.totalLines:d} lines in {self.duration():0.3f} sec." + ) + return f"did not {self.name!r}" + + + + + +class LoadAction(Action): + """Load the source web.""" + def __init__(self) -> None: + super().__init__("Load") + def __str__(self) -> str: + return f"Load [{self.webReader!s}, {self.web!s}]" + + + def __call__(self) -> None: + super().__call__() + self.webReader = self.options.webReader + self.webReader.command = self.options.command + self.webReader.permitList = self.options.permitList + self.web.web_path = self.options.source_path + error = f"Problems with source file {self.options.source_path!r}, no output produced." + try: + self.webReader.load(self.web, self.options.source_path) + if self.webReader.errors != 0: + self.logger.error(error) + raise Error("Syntax Errors in the Web") + self.web.createUsedBy() + if self.webReader.errors != 0: + self.logger.error(error) + raise Error("Internal Reference Errors in the Web") + except Error as e: + self.logger.error(error) + raise # Older design. + except IOError as e: + self.logger.error(error) + raise + + + + + def summary(self) -> str: + return ( + f"{self.name!s} {self.webReader.totalLines:d} lines from {self.webReader.totalFiles:d} files in {self.duration():0.3f} sec." + ) + + + + + + + +class Application: + def __init__(self) -> None: + self.logger = logging.getLogger(self.__class__.__qualname__) + + self.defaults = argparse.Namespace( + verbosity=logging.INFO, + command='@', + weaver='rst', + skip='', # Don't skip any steps + permit='', # Don't tolerate missing includes + reference='s', # Simple references + tangler_line_numbers=False, + output=Path.cwd(), + ) + # self.expand(self.defaults) + + # Primitive Actions + self.loadOp = LoadAction() + self.weaveOp = WeaveAction() + self.tangleOp = TangleAction() + + # Composite Actions + self.doWeave = ActionSequence("load and weave", [self.loadOp, self.weaveOp]) + self.doTangle = ActionSequence("load and tangle", [self.loadOp, self.tangleOp]) + self.theAction = ActionSequence("load, tangle and weave", [self.loadOp, self.tangleOp, self.weaveOp]) + + + + def parseArgs(self, argv: list[str]) -> argparse.Namespace: + p = argparse.ArgumentParser() + p.add_argument("-v", "--verbose", dest="verbosity", action="store_const", const=logging.INFO) + p.add_argument("-s", "--silent", dest="verbosity", action="store_const", const=logging.WARN) + p.add_argument("-d", "--debug", dest="verbosity", action="store_const", const=logging.DEBUG) + p.add_argument("-c", "--command", dest="command", action="store") + p.add_argument("-w", "--weaver", dest="weaver", action="store") + p.add_argument("-x", "--except", dest="skip", action="store", choices=('w', 't')) + p.add_argument("-p", "--permit", dest="permit", action="store") + p.add_argument("-r", "--reference", dest="reference", action="store", choices=('t', 's')) + p.add_argument("-n", "--linenumbers", dest="tangler_line_numbers", action="store_true") + p.add_argument("-o", "--output", dest="output", action="store", type=Path) + p.add_argument("-V", "--Version", action='version', version=f"py-web-tool pyweb.py {__version__}") + p.add_argument("files", nargs='+', type=Path) + config = p.parse_args(argv, namespace=self.defaults) + self.expand(config) + return config + + def expand(self, config: argparse.Namespace) -> argparse.Namespace: + """Translate the argument values from simple text to useful objects. + Weaver. Tangler. WebReader. + """ + match config.reference: + case 't': + config.reference_style = TransitiveReference() + case 's': + config.reference_style = SimpleReference() + case _: + raise Error("Improper configuration") + + # Weaver + try: + weaver_class = weavers[config.weaver.lower()] + except KeyError: + module_name, _, class_name = config.weaver.partition('.') + weaver_module = __import__(module_name) + weaver_class = weaver_module.__dict__[class_name] + if not issubclass(weaver_class, Weaver): + raise TypeError(f"{weaver_class!r} not a subclass of Weaver") + config.theWeaver = weaver_class() + + # Tangler + config.theTangler = TanglerMake() + + if config.permit: + # save permitted errors, usual case is ``-pi`` to permit ``@i`` include errors + config.permitList = [f'{config.command!s}{c!s}' for c in config.permit] + else: + config.permitList = [] + + config.webReader = WebReader() + + return config + + + + def process(self, config: argparse.Namespace) -> None: + root = logging.getLogger() + root.setLevel(config.verbosity) + self.logger.debug("Setting root log level to %r", logging.getLevelName(root.getEffectiveLevel())) + + if config.command: + self.logger.debug("Command character %r", config.command) + + if config.skip: + if config.skip.lower().startswith('w'): # not weaving == tangling + self.theAction = self.doTangle + elif config.skip.lower().startswith('t'): # not tangling == weaving + self.theAction = self.doWeave + else: + raise Exception(f"Unknown -x option {config.skip!r}") + + self.logger.info("Weaver %s", config.theWeaver) + + for f in config.files: + w = Web() # New, empty web to load and process. + self.logger.info("%s %r", self.theAction.name, f) + config.source_path = f + self.theAction.web = w + self.theAction.options = config + self.theAction() + self.logger.info(self.theAction.summary()) + + + + +# Global list of available weaver classes. +weavers = { + 'html': HTML, + 'htmlshort': HTMLShort, + 'latex': LaTeX, + 'rst': RST, +} + + +class Logger: + def __init__(self, dict_config: dict[str, Any] | None = None, **kw_config: Any) -> None: + self.dict_config = dict_config + self.kw_config = kw_config + + def __enter__(self) -> "Logger": + if self.dict_config: + logging.config.dictConfig(self.dict_config) + else: + logging.basicConfig(**self.kw_config) + return self + + def __exit__(self, *args: Any) -> Literal[False]: + logging.shutdown() + return False + +log_config = { + 'version': 1, + 'disable_existing_loggers': False, # Allow pre-existing loggers to work. + 'style': '{', + 'handlers': { + 'console': { + 'class': 'logging.StreamHandler', + 'stream': 'ext://sys.stderr', + 'formatter': 'basic', + }, + }, + 'formatters': { + 'basic': { + 'format': "{levelname}:{name}:{message}", + 'style': "{", + } + }, + + 'root': {'handlers': ['console'], 'level': logging.INFO,}, + + #For specific debugging support... + 'loggers': { + # 'RST': {'level': logging.DEBUG}, + # 'TanglerMake': {'level': logging.DEBUG}, + # 'WebReader': {'level': logging.DEBUG}, + }, +} + + +def main(argv: list[str] = sys.argv[1:]) -> None: + a = Application() + config = a.parseArgs(argv) + a.process(config) + +if __name__ == "__main__": + with Logger(log_config): + main() + diff --git a/pyweb.rst b/src/pyweb.rst similarity index 65% rename from pyweb.rst rename to src/pyweb.rst index f018f82..295ed96 100644 --- a/pyweb.rst +++ b/src/pyweb.rst @@ -1,5 +1,5 @@ ############################## -pyWeb Literate Programming 3.0 +pyWeb Literate Programming 3.1 ############################## ================================================= @@ -12,7 +12,7 @@ Yet Another Literate Programming Tool .. contents:: -.. pyweb/intro.w +.. py-web-tool/src/intro.w Introduction ============ @@ -29,11 +29,12 @@ have a common origin, then the traditional gaps between intent (expressed in the documentation) and action (expressed in the working program) are significantly reduced. -**pyWeb** is a literate programming tool that combines the actions +**py-web-tool** is a literate programming tool that combines the actions of *weaving* a document with *tangling* source files. It is independent of any source language. -It is designed to work with RST document markup. -Is uses a simple set of markup tags to define chunks of code and +While is designed to work with RST document markup, it should be amenable to any other +flavor of markup. +It uses a small set of markup tags to define chunks of code and documentation. Background @@ -85,11 +86,11 @@ like `Literate Programming `_, and the OASIS `XML Cover Pages: Literate Programming with SGML and XML `_. -The immediate predecessors to this **pyWeb** tool are +The immediate predecessors to this **py-web-tool** tool are `FunnelWeb `_, `noweb `_ and `nuweb `_. The ideas lifted from these other -tools created the foundation for **pyWeb**. +tools created the foundation for **py-web-tool**. There are several Python-oriented literate programming tools. These include @@ -97,7 +98,7 @@ These include `interscript `_, `lpy `_, `py2html `_, -`PyLit `_. +`PyLit-3 `_ The *FunnelWeb* tool is independent of any programming language and only mildly dependent on T\ :sub:`e`\ X. @@ -132,45 +133,45 @@ programming". The *py2html* tool does very sophisticated syntax coloring. -The *PyLit* tool is perhaps the very best approach to simple Literate +The *PyLit-3* tool is perhaps the very best approach to Literate programming, since it leverages an existing lightweight markup language and it's output formatting. However, it's limited in the presentation order, making it difficult to present a complex Python module out of the proper Python required presentation. -**pyWeb** ---------- +**py-web-tool** +--------------- -**pyWeb** works with any +**py-web-tool** works with any programming language. It can work with any markup language, but is currently -configured to work with RST only. This philosophy +configured to work with RST. This philosophy comes from *FunnelWeb* *noweb*, *nuweb* and *interscript*. The primary differences -between **pyWeb** and other tools are the following. +between **py-web-tool** and other tools are the following. -- **pyWeb** is object-oriented, permitting easy extension. +- **py-web-tool** is object-oriented, permitting easy extension. *noweb* extensions are separate processes that communicate through a sophisticated protocol. *nuweb* is not easily extended without rewriting and recompiling the C programs. -- **pyWeb** is built in the very portable Python programming +- **py-web-tool** is built in the very portable Python programming language. This allows it to run anywhere that Python 3.3 runs, with only the addition of docutils. This makes it a useful tool for programmers in any language. -- **pyWeb** is much simpler than *FunnelWeb*, *LEO* or *Interscript*. It has +- **py-web-tool** is much simpler than *FunnelWeb*, *LEO* or *Interscript*. It has a very limited selection of commands, but can still produce complex programs and HTML documents. -- **pyWeb** does not invent a complex markup language like *Interscript*. +- **py-web-tool** does not invent a complex markup language like *Interscript*. Because *Iterscript* has its own markup, it can generate L\ :sub:`a`\ T\ :sub:`e`\ X or HTML or other output formats from a unique input format. While powerful, it seems simpler to - avoid inventing yet another sophisticated markup language. The language **pyWeb** + avoid inventing yet another sophisticated markup language. The language **py-web-tool** uses is very simple, and the author's use their preferred markup language almost exclusively. -- **pyWeb** supports the forward literate programming philosophy, +- **py-web-tool** supports the forward literate programming philosophy, where a source document creates programming language and markup language. The alternative, deriving the document from markup embedded in program comments ("inverted literate programming"), seems less appealing. @@ -178,7 +179,7 @@ between **pyWeb** and other tools are the following. can't reflect the original author's preferred order of exposition, since that informtion generally isn't part of the source code. -- **pyWeb** also specifically rejects some features of *nuweb* +- **py-web-tool** also specifically rejects some features of *nuweb* and *FunnelWeb*. These include the macro capability with parameter substitution, and multiple references to a chunk. These two capabilities can be used to grow object-like applications from non-object programming @@ -186,18 +187,18 @@ between **pyWeb** and other tools are the following. Java, C++) are object-oriented, this macro capability is more of a problem than a help. -- Since **pyWeb** is built in the Python interpreter, a source document +- Since **py-web-tool** is built in the Python interpreter, a source document can include Python expressions that are evaluated during weave operation to produce time stamps, source file descriptions or other information in the woven or tangled output. -**pyWeb** works with any programming language; it can work with any markup language. +**py-web-tool** works with any programming language; it can work with any markup language. The initial release supports RST via simple templates. The following is extensively quoted from Briggs' *nuweb* documentation, and provides an excellent background in the advantages of the very -simple approach started by *nuweb* and adopted by **pyWeb**. +simple approach started by *nuweb* and adopted by **py-web-tool**. The need to support arbitrary programming languages has many consequences: @@ -248,30 +249,66 @@ simple approach started by *nuweb* and adopted by **pyWeb**. but it is also important in many practical situations, *e.g.*, debugging. :Speed: - Since [**pyWeb**] doesn't do too much, it runs very quickly. + Since [**py-web-tool**] doesn't do too much, it runs very quickly. It combines the functions of ``tangle`` and ``weave`` into a single program that performs both functions at once. :Chunk numbers: - Inspired by the example of **noweb**, [**pyWeb**] refers to all program code + Inspired by the example of **noweb**, [**py-web-tool**] refers to all program code chunks by a simple, ascending sequence number through the file. This becomes the HTML anchor name, also. :Multiple file output: - The programmer may specify more than one output file in a single [**pyWeb**] + The programmer may specify more than one output file in a single [**py-web-tool**] source file. This is required when constructing programs in a combination of languages (say, Fortran and C). It's also an advantage when constructing very large programs. -Use Cases ------------ +Acknowledgements +---------------- + +This application is very directly based on (derived from?) work that + preceded this, particularly the following: + +- Ross N. Williams' *FunnelWeb* http://www.ross.net/funnelweb/ + +- Norman Ramsey's *noweb* http://www.eecs.harvard.edu/~nr/noweb/ + +- Preston Briggs' *nuweb* http://sourceforge.net/projects/nuweb/ + Currently supported by Charles Martin and Marc W. Mengel + +Also, after using John Skaller's *interscript* http://interscript.sourceforge.net/ +for two large development efforts, I finally understood the feature set I really wanted. + +Jason Fruit and others contributed to the previous version. + + +.. py-web-tool/src/usage.w + +Installing +========== + +This requires Python 3.10. + +This is not (currently) hosted in PyPI. Instead of installing it with PIP, +clone the GitHub repository or download the distribution kit. + +Install pyweb "manually" using the provided ``setup.py``. -**pyWeb** supports two use cases, `Tangle Source Files`_ and `Weave Documentation`_. -These are often combined into a single request of the application that will both -weave and tangle. +:: + + python setup.py install + +This will install the ``pyweb`` module. + +Using +===== + +**py-web-tool** supports two use cases, `Tangle Source Files`_ and `Weave Documentation`_. +These are often combined to both tangle and weave an application and it's documentation. Tangle Source Files -~~~~~~~~~~~~~~~~~~~ +------------------- A user initiates this process when they have a complete ``.w`` file that contains a description of source files. These source files are described with ``@o`` commands @@ -285,14 +322,21 @@ Outside this use case, the user will debug those source files, possibly updating The use case is a failure when the source files cannot be produced, due to errors in the ``.w`` file. These must be corrected based on information in log messages. -The sequence is simply ``./pyweb.py *theFile*.w``. +A typical command to tangle (without weaving) is: + +.. parsed-literal:: + + python -m pyweb -xw *theFile*.w + +The outputs will be defined by the ``@o`` commands in the source. Weave Documentation -~~~~~~~~~~~~~~~~~~~~ +------------------- A user initiates this process when they have a ``.w`` file that contains a description of a document to produce. The document is described by the entire -``.w`` file. +``.w`` file. The default is to use ReSTructured Text (RST) markup. +The output file will have the ``.rst`` suffix. The use case is successful when the documentation file is produced. @@ -302,18 +346,24 @@ Outside this use case, the user will edit the documentation file, possibly updat The use case is a failure when the documentation file cannot be produced, due to errors in the ``.w`` file. These must be corrected based on information in log messages. -The sequence is simply ``./pyweb.py *theFile*.w``. +A typical command to weave (without tangling) is: + +.. parsed-literal:: + + python -m pyweb -xt *theFile*\ .w + +The output will be the *theFile*\ ``.rst``. -Tangle, Regression Test and Weave -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Tangle, Test, and Weave with Test Results +----------------------------------------- A user initiates this process when they have a ``.w`` file that contains a description of a document to produce. The document is described by the entire -``.w`` file. Further, their final document should include regression test output +``.w`` file. Further, their final document should include test output from the source files created by the tangle operation. The use case is successful when the documentation file is produced, including -current regression test output. +current test output. Outside this use case, the user will edit the documentation file, possibly updating the ``.w`` file. This will lead to a need to restart this use case. @@ -322,64 +372,253 @@ The use case is a failure when the documentation file cannot be produced, due to errors in the ``.w`` file. These must be corrected based on information in log messages. The use case is a failure when the documentation file does not include current -regression test output. +test output. The sequence is as follows: .. parsed-literal:: - ./pyweb.py -xw -pi *theFile*\ .w - python *theTest* >\ *aLog* - ./pyweb.py -xt *theFile*\ .w - - + python -m pyweb -xw -pi *theFile*\ .w + pytest >\ *aLog* + python -m pyweb -xt *theFile*\ .w + The first step excludes weaving and permits errors on the ``@i`` command. The ``-pi`` option is necessary in the event that the log file does not yet exist. The second step -runs the regression test, creating a log file. The third step weaves the final document, -including the regression test output. +runs the test, creating a log file. The third step weaves the final document, +including the test output. + +Running **py-web-tool** to Tangle and Weave +------------------------------------------- + +Assuming that you have marked ``pyweb.py`` as executable, +you do the following: + +.. parsed-literal:: + + python -m pyweb *theFile*\ .w + +This will tangle the ``@o`` commands in each *theFile*. +It will also weave the output, and create *theFile*.rst. + +Command Line Options +~~~~~~~~~~~~~~~~~~~~~ + +Currently, the following command line options are accepted. + + +:-v: + Verbose logging. + +:-s: + Silent operation. + +:-c\ *x*: + Change the command character from ``@`` to ``*x*``. + +:-w\ *weaver*: + Choose a particular documentation weaver template. Currently the choices + are RST and HTML. + +:-xw: + Exclude weaving. This does tangling of source program files only. + +:-xt: + Exclude tangling. This does weaving of the document file only. + +:-p\ *command*: + Permit errors in the given list of commands. The most common + version is ``-pi`` to permit errors in locating an include file. + This is done in the following scenario: pass 1 uses ``-xw -pi`` to exclude + weaving and permit include-file errors; + the tangled program is run to create test results; pass 2 uses + ``-xt`` to exclude tangling and include the test results. + +:-o\ *directory*: + The directory to which to write output files. + +Bootstrapping +-------------- + +**py-web-tool** is written using **py-web-tool**. The distribution includes the original ``.w`` +files as well as a ``.py`` module. + +The bootstrap procedure is to run a "known good" ``pyweb`` to transform +a working copy into a new version of ``pyweb``. We provide the previous release in the ``bootstrap`` +directory. + +.. parsed-literal:: + + python bootstrap/pyweb.py pyweb.w + rst2html.py pyweb.rst pyweb.html + +The resulting ``pyweb.html`` file is the updated documentation. +The ``pyweb.py`` is the updated candidate release of **py-web-tool**. + +Similarly, the tests built from a ``.w`` files. + +.. parsed-literal:: -Writing **pyWeb** ``.w`` Files -------------------------------- + python pyweb.py tests/pyweb_test.w -o tests + PYTHONPATH=.. pytest + rst2html.py tests/pyweb_test.rst tests/pyweb_test.html -The essence of literate programming is a markup language that distinguishes code +Dependencies +------------- + +**py-web-tool** requires Python 3.10 or newer. + +If you create RST output, you'll want to use ``docutils`` to translate +the RST to HTML or LaTeX or any of the other formats supported by docutils. + +Tools like ``pytest`` and ``tox`` are also used for development. + + +.. py-web-tool/src/language.w + +The **py-web-tool** ``.w`` Markup Language +========================================== + +The essence of literate programming is a markup language that includes both code from documentation. For tangling, the code is relevant. For weaving, both code and documentation are relevant. -The **pyWeb** markup defines a sequence of *Chunks*. -Each Chunk is either program source code to -be *tangled* or it is documentation to be *woven*. The bulk of -the file is typically documentation chunks that describe the program in -some human-oriented markup language like RST, HTML, or LaTeX. +The source document is a "Web" documentation that includes the code. +It's important to see the ``.w`` file as the final documentation. The code is tangled out +of the source web. - -The **pyWeb** tool parses the input, and performs the +The **py-web-tool** tool parses the ``.w`` file, and performs the tangle and weave operations. It *tangles* each individual output file -from the program source chunks. It *weaves* a final documentation file +from the program source chunks. It *weaves* the final documentation file file from the entire sequence of chunks provided, mixing the author's original documentation with some markup around the embedded program source. -**pyWeb** markup surrounds the code with tags. Everything else is documentation. -When tangling, the tagged code is assembled into the final file. -When weaving, the tags are replaced with output markup. This means that **pyWeb** -is not **totally** independent of the output markup. +Concepts +--------- + +The ``.w`` file has two tiers of markup in it. + +- At the top, it has **py-web-tool** markup to distinguish + documentation chunks from code chunks. + +- Within the documentation chunks, there can be + markup for the target publication tool chain. This might + be RST, LaTeX, HTML, or some other markup language. + +The **py-web-tool** markup decomposes the source document a sequence of *Chunks*. +Each Chunk is one of the two kinds: + +- program source code to be *tangled* and *woven*. + +- documentation to be *woven*. + +The bulk of the file is typically documentation chunks that describe the program in +some publication-oriented markup language like RST, HTML, or LaTeX. + +**py-web-tool** markup surrounds the code with "commands." Everything else is documentation. -The code chunks will have their indentation adjusted to match the context in which -they were originally defined. This assures that Python (which relies on indentation) -parses correctly. For other languages, proper indentation is expected but not required. +The code chunks have two transformations applied. -The non-code chunks are not transformed up in any way. Everything that's not -explicitly a code chunk is simply output without modification. +- When Tangling, the indentation is adjusted to match the context in which they were originally defined. + This assures that Python (which relies on indentation) + parses correctly. For other languages, proper indentation is expected but not required. -All of the **pyWeb** tags begin with ``@``. This can be changed. +- When Weaving, selected characters can be quoted so they don't break the publication tool. + For HTML, ``&``, ``<``, ``>`` are quoted properly. For LaTeX, a few escapes are used + to avoid problems with the ``fancyvrb`` environment. + +The non-code, documentation chunks are not transformed up in any way. Everything that's not +explicitly a code chunk is output without modification. + +All of the **py-web-tool** tags begin with ``@``. This is sometimes called the command prefix. +(This can be changed.) The tags were historically referred to as "commands." The *Structural* tags (historically called "major commands") partition the input and define the various chunks. The *Inline* tags are (called "minor commands") are used to control the -woven and tangled output from those chunks. There are *Content* tags which generate +woven and tangled output from the defined chunks. There are *Content* tags which generate summary cross-reference content in woven files. +Boilerplate +----------- + +There is some mandatory "boilerplate" required to make a working document. +Requirements vary by markup language. + +RST +~~~ + +The RST template uses two substitutions, ``|srarr|`` and ``|loz|``. + +These can be provided by + +:: + + .. include:: + .. include:: + +Or + +:: + + .. |srarr| unicode:: U+02192 .. RIGHTWARDS ARROW + .. |loz| unicode:: U+025CA .. LOZENGE + +Often the boilerplate document looks like this + +.. parsed-literal:: + + #################### + *Title* + #################### + + =============== + *Author* + =============== + + .. include:: + .. include:: + + .. contents:: + + *Your Document Starts Here* + + +LaTeX +~~~~~ + +The LaTeX templates use ``\\fancyvrb``. +The following is required. + +:: + + \\usepackage{fancyvrb} + +Some minimal boilerplate document looks like this: + +.. parsed-literal:: + + \documentclass{article} + \usepackage{fancyvrb} + \title{ *Title* } + \author{ *Author* } + + \begin{document} + + \maketitle + \tableofcontents + + *Your Document Starts Here* + + \end{document} + +HTML +~~~~ + +No additional setup is required for HTML. However, there's often +a fairly large amount of HTML boilerplate, depending on the CSS +requirements. Structural Tags -~~~~~~~~~~~~~~~ +--------------- There are two definitional tags; these define the various chunks in an input file. @@ -454,7 +693,7 @@ documentation. It includes a named output chunk which will write to ``myFile.py` It ends with an anonymous chunk of documentation. Inline Tags -~~~~~~~~~~~~ +--------------- There are several tags that are replaced by content in the woven output. @@ -480,7 +719,7 @@ There are several tags that are replaced by content in the woven output. These are described in `Expression Context`_. Content Tags -~~~~~~~~~~~~~ +--------------- There are three index creation tags that are replaced by content in the woven output. @@ -505,7 +744,7 @@ There are three index creation tags that are replaced by content in the woven ou Additional Features -~~~~~~~~~~~~~~~~~~~ +------------------- **Sequence Numbers**. The named chunks (from both ``@o`` and ``@d`` commands) are assigned unique sequence numbers to simplify cross references. @@ -522,8 +761,8 @@ is shown in the following example: @o myFile.py @{ - @ - print( math.pi,time.time() ) + @ + print(math.pi,time.time()) @} Some notes on the packages used. @@ -571,14 +810,14 @@ fairly complex output files. @o myFile.py @{ - import math,time + import math, time @} Some notes on the packages used. @o myFile.py @{ - print math.pi,time.time() + print(math.pi, time.time()) @} Some more HTML documentation. @@ -607,13 +846,13 @@ named chunk was defined with the following. .. parsed-literal:: @{ - import math,time + import math, time @} This puts a newline character before and after the import line. Controlling Indentation -~~~~~~~~~~~~~~~~~~~~~~~ +----------------------- We have two choices in indentation: @@ -636,7 +875,7 @@ Here's how the context-sensitive indentation works. @o myFile.py @{ - def aFunction( a, b ): + def aFunction(a, b): @ @| aFunction @} @@ -656,7 +895,7 @@ more obvious. .. parsed-literal:: ~ - ~def aFunction( a, b ): + ~def aFunction(a, b): ~ ~ """doc string""" ~ return a + b @@ -703,7 +942,7 @@ provided by the ``some bigger chunk`` context. After the first newline (*More that uses """*) will be at the left margin. Tracking Source Line Numbers -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +---------------------------- Since the tangled output files are -- well -- tangled, it can be difficult to trace back from a Python error stack to the original line in the ``.w`` file that @@ -730,7 +969,7 @@ start and end syntax. This will lead to comments embedded in the tangled output which contain source line numbers for every (every!) chunk. Expression Context -~~~~~~~~~~~~~~~~~~~~ +------------------- There are two possible implementations for evaluation of a Python expression in the input. @@ -747,14 +986,19 @@ expression in the input. In this implementation, we adopt the latter approach, and evaluate expressions immediately. -A simple global context is created with the following variables defined. +A global context is created with the following variables defined. :os.path: - This is the standard ``os.path`` module. The complete ``os`` module is not - available. Just this one item. + This is the standard ``os.path`` module. + +:os.getcwd: + The complete ``os`` module is not available. Just this function. :datetime: This is the standard ``datetime`` module. + +:time: + The standard ``time`` module. :platform: This is the standard ``platform`` module. @@ -774,111 +1018,14 @@ A simple global context is created with the following variables defined. The ``.w`` file being processed. :thisApplication: - The name of the running **pyWeb** application. It may not be pyweb.py, + The name of the running **py-web-tool** application. It may not be pyweb.py, if some other script is being used. :__version__: - The version string in the **pyWeb** application. - - -Running **pyWeb** to Tangle and Weave --------------------------------------- - -Assuming that you have marked ``pyweb.py`` as executable, -you do the following. - -.. parsed-literal:: - - ./pyweb.py *file*... - -This will tangle the ``@o`` commands in each *file*. -It will also weave the output, and create *file*.txt. - -Command Line Options -~~~~~~~~~~~~~~~~~~~~~ - -Currently, the following command line options are accepted. - - -:-v: - Verbose logging. - -:-s: - Silent operation. - -:-c\ *x*: - Change the command character from ``@`` to ``*x*``. - -:-w\ *weaver*: - Choose a particular documentation weaver template. Currently the choices - are RST and HTML. - -:-xw: - Exclude weaving. This does tangling of source program files only. - -:-xt: - Exclude tangling. This does weaving of the document file only. - -:-p\ *command*: - Permit errors in the given list of commands. The most common - version is ``-pi`` to permit errors in locating an include file. - This is done in the following scenario: pass 1 uses ``-xw -pi`` to exclude - weaving and permit include-file errors; - the tangled program is run to create test results; pass 2 uses - ``-xt`` to exclude tangling and include the test results. - -Bootstrapping --------------- - -**pyWeb** is written using **pyWeb**. The distribution includes the original ``.w`` -files as well as a ``.py`` module. - -The bootstrap procedure is this. - -.. parsed-literal:: - - python pyweb.py pyweb.w - rst2html.py pyweb.rst pyweb.html - -The resulting ``pyweb.html`` file is the final documentation. - -Similarly, the tests are bootstrapped from ``.w`` files. - -.. parsed-literal:: - - cd test - python ../pyweb.py pyweb_test.w - PYTHONPATH=.. python test.py - rst2html.py pyweb_test.rst pyweb_test.html - -Dependencies -------------- - -**pyWeb** requires Python 3.3 or newer. - -If you create RST output, you'll want to use docutils to translate -the RST to HTML or LaTeX or any of the other formats supported by docutils. - -Acknowledgements ----------------- - -This application is very directly based on (derived from?) work that - preceded this, particularly the following: - -- Ross N. Williams' *FunnelWeb* http://www.ross.net/funnelweb/ - -- Norman Ramsey's *noweb* http://www.eecs.harvard.edu/~nr/noweb/ - -- Preston Briggs' *nuweb* http://sourceforge.net/projects/nuweb/ + The version string in the **py-web-tool** application. - Currently supported by Charles Martin and Marc W. Mengel - -Also, after using John Skaller's *interscript* http://interscript.sourceforge.net/ -for two large development efforts, I finally understood the feature set I really needed. - -Jason Fruit contributed to the previous version. -.. pyweb/overview.w +.. py-web-tool/src/overview.w Architecture and Design Overview ================================ @@ -910,7 +1057,8 @@ includes the sequence of Chunks as well as an index for the named chunks. Note that a named chunk may be created through a number of ``@d`` commands. This means that each named chunk may be a sequence of Chunks with a common name. - +They are concatenated in order to permit decomposing a single concept into sequentially described pieces. + Because a Chunk is composed of a sequence Commands, the weave and tangle actions can be delegated to each Chunk, and in turn, delegated to each Command that composes a Chunk. @@ -965,7 +1113,7 @@ Weaving The weaving operation depends on the target document markup language. There are several approaches to this problem. -- We can use a markup language unique to **pyWeb**, +- We can use a markup language unique to **py-web-tool**, and weave using markup in the desired target language. - We can use a standard markup language and use converters to transform @@ -978,11 +1126,11 @@ with common templates. We hate to repeat these templates; that's the job of a literate programming tool. Also, certain code characters must be properly escaped. -Since **pyWeb** must transform the code into a specific markup language, +Since **py-web-tool** must transform the code into a specific markup language, we opt using a **Strategy** pattern to encapsulate markup language details. Each alternative markup strategy is then a subclass of **Weaver**. This simplifies adding additional markup languages without inventing a -markup language unique to **pyWeb**. +markup language unique to **py-web-tool**. The author uses their preferred markup, and their preferred toolset to convert to other output languages. @@ -998,7 +1146,7 @@ provide a correct indentation. This required a command-line parameter to turn off indentation for languages like Fortran, where identation is not used. -In **pyWeb**, there are two options. The default behavior is that the +In **py-web-tool**, there are two options. The default behavior is that the indent of a ``@<`` command is used to set the indent of the material is expanded in place of this reference. If all ``@<`` commands are presented at the left margin, no indentation will be done. This is helpful simplification, @@ -1011,14 +1159,14 @@ Application ------------ The overall application has two layers to it. There are actions (Load, Tangle, Weave) -as well as a top-level application that parses the command line, creates +as well as a top-level main function that parses the command line, creates and configures the actions, and then closes up shop when all done. -The idea is that the Weaver Action should fit with SCons Builder. -We can see ``Weave( "someFile.w" )`` as sensible. Tangling is tougher -because the ``@o`` commands define the file dependencies there. +The idea is that the Weaver Action should be visible to tools like `PyInvoke `_. +We want ``Weave("someFile.w")`` to be a sensible task. -.. pyweb/impl.w + +.. py-web-tool/src/impl.w Implementation ============== @@ -1110,29 +1258,42 @@ fit elsewhere :class: code - |srarr|\ Error class - defines the errors raised (`94`_) - |srarr|\ Command class hierarchy - used to describe individual commands (`76`_) - |srarr|\ Chunk class hierarchy - used to describe input chunks (`51`_) - |srarr|\ Web class - describes the overall "web" of chunks (`95`_) - |srarr|\ Tokenizer class - breaks input into tokens (`132`_) - |srarr|\ Option Parser class - locates optional values on commands (`134`_), |srarr|\ (`135`_) - |srarr|\ WebReader class - parses the input file, building the Web structure (`114`_) + + |srarr|\ Error class - defines the errors raised (`96`_) + + |srarr|\ Command class hierarchy - used to describe individual commands (`78`_) + + |srarr|\ Chunk class hierarchy - used to describe input chunks (`52`_) + + |srarr|\ Web class - describes the overall "web" of chunks (`97`_) + + |srarr|\ Tokenizer class - breaks input into tokens (`133`_) + + |srarr|\ Option Parser class - locates optional values on commands (`135`_), |srarr|\ (`136`_), |srarr|\ (`137`_) + + |srarr|\ WebReader class - parses the input file, building the Web structure (`116`_) + |srarr|\ Emitter class hierarchy - used to control output files (`2`_) - |srarr|\ Reference class hierarchy - strategies for references to a chunk (`91`_), |srarr|\ (`92`_), |srarr|\ (`93`_) - |srarr|\ Action class hierarchy - used to describe basic actions of the application (`136`_) + |srarr|\ Reference class hierarchy - strategies for references to a chunk (`93`_), |srarr|\ (`94`_), |srarr|\ (`95`_) + + |srarr|\ Action class hierarchy - used to describe actions of the application (`138`_) .. .. class:: small - |loz| *Base Class Definitions (1)*. Used by: pyweb.py (`153`_) + |loz| *Base Class Definitions (1)*. Used by: pyweb.py (`155`_) +The above order is reasonably helpful for Python and minimizes forward +references. A ``Chunk`` and a ``Web`` do have a circular relationship. +We'll present the designs from the most important first, the `Emitters`_. + Emitters --------- -An ``Emitter`` instance is resposible for control of an output file format. +An ``Emitter`` instance is responsible for control of an output file format. This includes the necessary file naming, opening, writing and closing operations. It also includes providing the correct markup for the file type. @@ -1146,13 +1307,15 @@ formats. :class: code - |srarr|\ Emitter superclass (`3`_) - |srarr|\ Weaver subclass of Emitter to create documentation (`12`_) - |srarr|\ RST subclass of Weaver (`22`_) - |srarr|\ LaTeX subclass of Weaver (`23`_) - |srarr|\ HTML subclass of Weaver (`31`_), |srarr|\ (`32`_) - |srarr|\ Tangler subclass of Emitter to create source files with no markup (`43`_) - |srarr|\ TanglerMake subclass which is make-sensitive (`48`_) + |srarr|\ Emitter superclass (`4`_) + + |srarr|\ Weaver subclass of Emitter to create documentation (`13`_) + |srarr|\ RST subclass of Weaver (`23`_) + |srarr|\ LaTeX subclass of Weaver (`24`_) + |srarr|\ HTML subclass of Weaver (`32`_), |srarr|\ (`33`_) + + |srarr|\ Tangler subclass of Emitter to create source files with no markup (`44`_) + |srarr|\ TanglerMake subclass which is make-sensitive (`49`_) .. @@ -1164,31 +1327,29 @@ formats. An ``Emitter`` instance is created to contain the various details of writing an output file. Emitters are created as follows: -- A ``Web`` object will create a ``Weaver`` to **weave** the final document. +- A ``Web`` object will create a ``Weaver`` to **weave** a final document file. -- A ``Web`` object will create a ``Tangler`` to **tangle** each file. +- A ``Web`` object will create a ``Tangler`` to **tangle** each source code file. Since each ``Emitter`` instance is responsible for the details of one file type, different subclasses of ``Emitter`` are used when tangling source code files -(``Tangler``) and -weaving files that include source code plus markup (``Weaver``). +(``Tangler``) and weaving files that include source code plus markup (``Weaver``). -Further specialization is required when weaving HTML or LaTeX. Generally, this is -a matter of providing three things: +Further specialization is required when weaving HTML or LaTeX or some other markup language. +Generally, this is a matter of providing three things: -- Boilerplate text to replace various pyWeb constructs, +- Templates with markup to replace various **py-web-tool** constructs, - Escape rules to make source code amenable to the markup language, - A header to provide overall includes or other setup. - -An additional part of the escape rules can include using a syntax coloring +An additional part of the escape rules could be expanded to include using a syntax coloring toolset instead of simply applying escapes. In the case of **tangle**, the following algorithm is used: - Visit each each output ``Chunk`` (``@o``), doing the following: + Visit each each output ``Chunk`` (``@o`` command), doing the following: 1. Open the ``Tangler`` instance using the target file name. @@ -1207,19 +1368,19 @@ In the case of **tangle**, the following algorithm is used: In the case of **weave**, the following algorithm is used: - 1. Open the ``Weaver`` instance using the source file name. This name is transformed - by the weaver to an output file name appropriate to the language. + 1. Open the ``Weaver`` instance using the target file name. This name is transformed + by the weaver to an output file name appropriate to the target markup language. 2. Visit each each sequential ``Chunk`` (anonymous, ``@d`` or ``@o``), doing the following: - 1. Visit each ``Chunk``, calling the Chunk's ``weave()`` method. + 1. When visiting each ``Chunk``, call the Chunk's ``weave()`` method. 1. Call the Weaver's ``docBegin()``, ``fileBegin()`` or ``codeBegin()`` method, depending on the subclass of Chunk. For ``fileBegin()`` and ``codeBegin()``, this writes the header for a code chunk in the weaver's markup language. - 2. Visit each ``Command``, call the Command's ``weave()`` method. + 2. Visit each ``Command``, calling the Command's ``weave()`` method. For ordinary text, the text is written to the Weaver using the ``codeBlock()`` method. For references to other chunks, the referenced chunk is woven using @@ -1233,10 +1394,10 @@ In the case of **weave**, the following algorithm is used: Emitter Superclass ~~~~~~~~~~~~~~~~~~ -The ``Emitter`` class is not a concrete class; it is never instantiated. It +The ``Emitter`` class is an abstract base class. It contains common features factored out of the ``Weaver`` and ``Tangler`` subclasses. -Inheriting from the Emitter class generally requires overriding one or more +Inheriting from the ``Emitter`` class generally requires overriding one or more of the core methods: ``doOpen()``, and ``doClose()``. A subclass of Tangler, might override the code writing methods: ``quote()``, ``codeBlock()`` or ``codeFinish()``. @@ -1244,39 +1405,39 @@ A subclass of Tangler, might override the code writing methods: The ``Emitter`` class defines the basic framework used to create and write to an output file. This class follows the **Template** design pattern. This design pattern -directs us to factor the basic open(), close() and write() methods into two step algorithms. +directs us to factor the basic ``open()``, ``close()`` and ``write()`` methods into two step algorithms. .. parsed-literal:: - def open( self ): + def open(self) -> "Emitter": *common preparation* - self.doOpen() *#overridden by subclasses* + self.doOpen() *# overridden by subclasses* return self The *common preparation* section is generally internal -housekeeping. The ``doOpen()`` method would be overridden by subclasses to change the +housekeeping. The ``doOpen()`` method is overridden by subclasses to change the basic behavior. The class has the following attributes: -:fileName: - the name of the current open file created by the - open method +:filePath: + the ``Path`` object for the target file created by the + ``open()`` method. :theFile: - the current open file created by the - open method + the current open file object created by the + open method. :linesWritten: - the total number of ``'\n'`` characters written to the file + the total number of ``'\n'`` characters written to the file. :totalFiles: - count of total number of files + count of total number of files processed. :totalLines: - count of total number of lines + count of total number of lines. -Additionally, an emitter tracks an indentation context used by +Additionally, an ``Emitter`` object tracks an indentation context used by The ``codeBlock()`` method to indent each line written. :context: @@ -1284,7 +1445,8 @@ The ``codeBlock()`` method to indent each line written. ``clrIndent()`` and ``readdIndent()`` methods. :lastIndent: - the last indent used after writing a line of source code + the last indent used after writing a line of source code; + this is used to track places where a partial line of code has a substitution into it. :fragment: the last line written was a fragment and needs a ``'\n'``. @@ -1295,36 +1457,62 @@ The ``codeBlock()`` method to indent each line written. .. _`3`: -.. rubric:: Emitter superclass (3) = +.. rubric:: Imports (3) = +.. parsed-literal:: + :class: code + + from pathlib import Path + import abc + + +.. + + .. class:: small + + |loz| *Imports (3)*. Used by: pyweb.py (`155`_) + + + +.. _`4`: +.. rubric:: Emitter superclass (4) = .. parsed-literal:: :class: code class Emitter: """Emit an output file; handling indentation context.""" - code\_indent= 0 # Used by a Tangler - def \_\_init\_\_( self ): - self.fileName= "" - self.theFile= None - self.linesWritten= 0 - self.totalFiles= 0 - self.totalLines= 0 - self.fragment= False - self.logger= logging.getLogger( self.\_\_class\_\_.\_\_qualname\_\_ ) - self.log\_indent= logging.getLogger( "indent." + self.\_\_class\_\_.\_\_qualname\_\_ ) - self.readdIndent( self.code\_indent ) # Create context and initial lastIndent values - def \_\_str\_\_( self ): + + code\_indent = 0 #: Used by a Tangler + filePath : Path #: Path within the base directory (on the name is used) + output : Path #: Base directory to write + theFile: TextIO #: Open file being written + + def \_\_init\_\_(self) -> None: + self.logger = logging.getLogger(self.\_\_class\_\_.\_\_qualname\_\_) + self.log\_indent = logging.getLogger("indent." + self.\_\_class\_\_.\_\_qualname\_\_) + # Working State + self.lastIndent = 0 + self.fragment = False + self.context: list[int] = [] + self.readdIndent(self.code\_indent) # Create context and initial lastIndent values + # Summary + self.linesWritten = 0 + self.totalFiles = 0 + self.totalLines = 0 + + def \_\_str\_\_(self) -> str: return self.\_\_class\_\_.\_\_name\_\_ - |srarr|\ Emitter core open, close and write (`4`_) - |srarr|\ Emitter write a block of code (`7`_), |srarr|\ (`8`_), |srarr|\ (`9`_) - |srarr|\ Emitter indent control: set, clear and reset (`10`_) + + |srarr|\ Emitter core open, close and write (`5`_) + |srarr|\ Emitter write a block of code (`8`_), |srarr|\ (`9`_), |srarr|\ (`10`_) + |srarr|\ Emitter indent control: set, clear and reset (`11`_) .. .. class:: small - |loz| *Emitter superclass (3)*. Used by: Emitter class hierarchy... (`2`_) + |loz| *Emitter superclass (4)*. Used by: Emitter class hierarchy... (`2`_) The core ``open()`` method tracks the open files. @@ -1342,44 +1530,51 @@ This does some additional counting as well as writing the characters to the file. -.. _`4`: -.. rubric:: Emitter core open, close and write (4) = +.. _`5`: +.. rubric:: Emitter core open, close and write (5) = .. parsed-literal:: :class: code - def open( self, aFile ): + def open(self, aPath: Path) -> "Emitter": """Open a file.""" - self.fileName= aFile - self.linesWritten= 0 - self.doOpen( aFile ) + if not hasattr(self, 'output'): + self.output = Path.cwd() + self.filePath = self.output / aPath.name + self.logger.debug(f"Writing to {self.output} / {aPath.name} == {self.filePath}") + self.linesWritten = 0 + self.doOpen() return self - |srarr|\ Emitter doOpen, to be overridden by subclasses (`5`_) - def close( self ): + + |srarr|\ Emitter doOpen, to be overridden by subclasses (`6`_) + + def close(self) -> None: self.codeFinish() # Trailing newline for tangler only. self.doClose() self.totalFiles += 1 self.totalLines += self.linesWritten - |srarr|\ Emitter doClose, to be overridden by subclasses (`6`_) - def write( self, text ): + + |srarr|\ Emitter doClose, to be overridden by subclasses (`7`_) + + def write(self, text: str) -> None: if text is None: return + self.theFile.write(text) self.linesWritten += text.count('\\n') - self.theFile.write( text ) - # Context Manager - def \_\_enter\_\_( self ): + # Context Manager Interface -- used by \`\`open()\`\` method + def \_\_enter\_\_(self) -> "Emitter": return self - def \_\_exit\_\_( self, \*exc ): + + def \_\_exit\_\_(self, \*exc: Any) -> Literal[False]: self.close() return False - .. .. class:: small - |loz| *Emitter core open, close and write (4)*. Used by: Emitter superclass (`3`_) + |loz| *Emitter core open, close and write (5)*. Used by: Emitter superclass (`4`_) The ``doOpen()``, and ``doClose()`` @@ -1387,40 +1582,39 @@ methods are overridden by the various subclasses to perform the unique operation for the subclass. -.. _`5`: -.. rubric:: Emitter doOpen, to be overridden by subclasses (5) = +.. _`6`: +.. rubric:: Emitter doOpen, to be overridden by subclasses (6) = .. parsed-literal:: :class: code - def doOpen( self, aFile ): - self.logger.debug( "creating {!r}".format(self.fileName) ) + def doOpen(self) -> None: + self.logger.debug("Creating %r", self.filePath) .. .. class:: small - |loz| *Emitter doOpen, to be overridden by subclasses (5)*. Used by: Emitter core... (`4`_) + |loz| *Emitter doOpen, to be overridden by subclasses (6)*. Used by: Emitter core... (`5`_) -.. _`6`: -.. rubric:: Emitter doClose, to be overridden by subclasses (6) = +.. _`7`: +.. rubric:: Emitter doClose, to be overridden by subclasses (7) = .. parsed-literal:: :class: code - def doClose( self ): - self.logger.debug( "wrote {:d} lines to {!s}".format( - self.linesWritten, self.fileName) ) + def doClose(self) -> None: + self.logger.debug("Wrote %d lines to %r", self.linesWritten, self.filePath) .. .. class:: small - |loz| *Emitter doClose, to be overridden by subclasses (6)*. Used by: Emitter core... (`4`_) + |loz| *Emitter doClose, to be overridden by subclasses (7)*. Used by: Emitter core... (`5`_) The ``codeBlock()`` method writes several lines of code. It calls @@ -1471,42 +1665,51 @@ This feels a bit too complex. Indentation is a feature of a tangling a reference a NamedChunk. It's not really a general feature of emitters or even tanglers. -.. _`7`: -.. rubric:: Emitter write a block of code (7) = +.. _`8`: +.. rubric:: Emitter write a block of code (8) = .. parsed-literal:: :class: code - def codeBlock( self, text ): - """Indented write of a block of code. We buffer - The spaces from the last line to act as the indent for the next line. + def codeBlock(self, text: str) -> None: + """Indented write of a block of code. + Buffers the spaces from the last line provided to act as the indent for the next line. """ - indent= self.context[-1] - lines= text.split( '\\n' ) - if len(lines) == 1: # Fragment with no newline. - self.write('{!s}{!s}'.format(self.lastIndent\*' ', lines[0]) ) - self.lastIndent= 0 - self.fragment= True + indent = self.context[-1] + lines = text.split('\\n') + if len(lines) == 1: + # Fragment with no newline. + self.logger.debug("Fragment: %d, %r", self.lastIndent, lines[0]) + self.write(f"{self.lastIndent\*' '!s}{lines[0]!s}") + self.lastIndent = 0 + self.fragment = True else: - first, rest= lines[:1], lines[1:] - self.write('{!s}{!s}\\n'.format(self.lastIndent\*' ', first[0]) ) + # Multiple lines with one or more newlines. + first, rest = lines[:1], lines[1:] + self.logger.debug("First Line: %d, %r", self.lastIndent, first[0]) + self.write(f"{self.lastIndent\*' '!s}{first[0]!s}\\n") for l in rest[:-1]: - self.write( '{!s}{!s}\\n'.format(indent\*' ', l) ) + self.logger.debug("Next Line: %d, %r", indent, l) + self.write(f"{indent\*' '!s}{l!s}\\n") if rest[-1]: - self.write( '{!s}{!s}'.format(indent\*' ', rest[-1]) ) - self.lastIndent= 0 - self.fragment= True + # Last line is non-empty. + self.logger.debug("Last (Partial) Line: %d, %r", indent, rest[-1]) + self.write(f"{indent\*' '!s}{rest[-1]!s}") + self.lastIndent = 0 + self.fragment = True else: - # Buffer a next indent - self.lastIndent= len(rest[-1]) + indent - self.fragment= False + # Last line was empty, a trailing newline. + self.logger.debug("Last (Empty) Line: indent is %d", len(rest[-1]) + indent) + # Buffer the next indent + self.lastIndent = len(rest[-1]) + indent + self.fragment = False .. .. class:: small - |loz| *Emitter write a block of code (7)*. Used by: Emitter superclass (`3`_) + |loz| *Emitter write a block of code (8)*. Used by: Emitter superclass (`4`_) The ``quote()`` method quotes a single line of source code. @@ -1520,21 +1723,21 @@ However, since the author's original document sections contain HTML these will not be altered. -.. _`8`: -.. rubric:: Emitter write a block of code (8) += +.. _`9`: +.. rubric:: Emitter write a block of code (9) += .. parsed-literal:: :class: code - quoted\_chars = [ + quoted\_chars: list[tuple[str, str]] = [ # Must be empty for tangling. ] - def quote( self, aLine ): + def quote(self, aLine: str) -> str: """Each individual line of code; often overridden by weavers to quote the code.""" - clean= aLine + clean = aLine for from\_, to\_ in self.quoted\_chars: - clean= clean.replace( from\_, to\_ ) + clean = clean.replace(from\_, to\_) return clean @@ -1542,19 +1745,19 @@ HTML these will not be altered. .. class:: small - |loz| *Emitter write a block of code (8)*. Used by: Emitter superclass (`3`_) + |loz| *Emitter write a block of code (9)*. Used by: Emitter superclass (`4`_) The ``codeFinish()`` method handles a trailing fragmentary line when tangling. -.. _`9`: -.. rubric:: Emitter write a block of code (9) += +.. _`10`: +.. rubric:: Emitter write a block of code (10) += .. parsed-literal:: :class: code - def codeFinish( self ): + def codeFinish(self) -> None: if self.fragment: self.write('\\n') @@ -1563,7 +1766,7 @@ The ``codeFinish()`` method handles a trailing fragmentary line when tangling. .. class:: small - |loz| *Emitter write a block of code (9)*. Used by: Emitter superclass (`3`_) + |loz| *Emitter write a block of code (10)*. Used by: Emitter superclass (`4`_) These three methods are used when to be sure that the included text is indented correctly with respect to the @@ -1594,36 +1797,40 @@ It's an additional indent for woven code; not used for tangled code. In particul requires this. ``readdIndent()`` uses this initial offset for weaving. -.. _`10`: -.. rubric:: Emitter indent control: set, clear and reset (10) = +.. _`11`: +.. rubric:: Emitter indent control: set, clear and reset (11) = .. parsed-literal:: :class: code - def addIndent( self, increment ): - self.lastIndent= self.context[-1]+increment - self.context.append( self.lastIndent ) - self.log\_indent.debug( "addIndent {!s}: {!r}".format(increment, self.context) ) - def setIndent( self, indent ): - self.lastIndent= self.context[-1] - self.context.append( indent ) - self.log\_indent.debug( "setIndent {!s}: {!r}".format(indent, self.context) ) - def clrIndent( self ): + def addIndent(self, increment: int) -> None: + self.lastIndent = self.context[-1]+increment + self.context.append(self.lastIndent) + self.log\_indent.debug("addIndent %d: %r", increment, self.context) + + def setIndent(self, indent: int) -> None: + self.context.append(indent) + self.lastIndent = self.context[-1] + self.log\_indent.debug("setIndent %d: %r", indent, self.context) + + def clrIndent(self) -> None: if len(self.context) > 1: self.context.pop() - self.lastIndent= self.context[-1] - self.log\_indent.debug( "clrIndent {!r}".format(self.context) ) - def readdIndent( self, indent=0 ): - self.lastIndent= indent - self.context= [self.lastIndent] - self.log\_indent.debug( "readdIndent {!s}: {!r}".format(indent, self.context) ) + self.lastIndent = self.context[-1] + self.log\_indent.debug("clrIndent %r", self.context) + + def readdIndent(self, indent: int = 0) -> None: + """Resets the indentation context.""" + self.lastIndent = indent + self.context = [self.lastIndent] + self.log\_indent.debug("readdIndent %d: %r", indent, self.context) .. .. class:: small - |loz| *Emitter indent control: set, clear and reset (10)*. Used by: Emitter superclass (`3`_) + |loz| *Emitter indent control: set, clear and reset (11)*. Used by: Emitter superclass (`4`_) Weaver subclass of Emitter @@ -1631,7 +1838,7 @@ Weaver subclass of Emitter A Weaver is an Emitter that produces the final user-focused document. This will include the source document with the code blocks surrounded by -markup to present that code properly. In effect, the pyWeb ``@`` commands +markup to present that code properly. In effect, the **py-web-tool** ``@`` commands are replaced by markup. The Weaver class uses a simple set of templates to product RST markup as the default @@ -1657,7 +1864,7 @@ This class hierarch depends heavily on the ``string`` module. Class-level variables include the following :extension: - The filename extension used by this weaver. + The Path's suffix used by this weaver. :code_indent: The number of spaces to indent code to separate code blocks from @@ -1673,60 +1880,69 @@ Instance-level configuration values: Either an instance of ``TransitiveReference()`` or ``SimpleReference()`` -.. _`11`: -.. rubric:: Imports (11) = +.. _`12`: +.. rubric:: Imports (12) += .. parsed-literal:: :class: code import string + from textwrap import dedent, indent, shorten .. .. class:: small - |loz| *Imports (11)*. Used by: pyweb.py (`153`_) + |loz| *Imports (12)*. Used by: pyweb.py (`155`_) -.. _`12`: -.. rubric:: Weaver subclass of Emitter to create documentation (12) = +.. _`13`: +.. rubric:: Weaver subclass of Emitter to create documentation (13) = .. parsed-literal:: :class: code - class Weaver( Emitter ): + class Weaver(Emitter): """Format various types of XRef's and code blocks when weaving. - RST format. - Requires \`\`.. include:: \`\` - and \`\`.. include:: \`\` + + For RST format we splice in the following two lines + :: + + .. include:: + .. include:: """ - extension= ".rst" - code\_indent= 4 - header= """\\n.. include:: \\n.. include:: \\n""" + extension = ".rst" + code\_indent = 4 + # Not actually used. + header = dedent(""" + .. include:: + .. include:: + """) - def \_\_init\_\_( self ): + reference\_style : "Reference" + + def \_\_init\_\_(self) -> None: super().\_\_init\_\_() - self.reference\_style= None # Must be configured. - |srarr|\ Weaver doOpen, doClose and addIndent overrides (`13`_) + |srarr|\ Weaver doOpen, doClose and addIndent overrides (`14`_) # Template Expansions. - |srarr|\ Weaver quoted characters (`14`_) - |srarr|\ Weaver document chunk begin-end (`15`_) - |srarr|\ Weaver reference summary, used by code chunk and file chunk (`16`_) - |srarr|\ Weaver code chunk begin-end (`17`_) - |srarr|\ Weaver file chunk begin-end (`18`_) - |srarr|\ Weaver reference command output (`19`_) - |srarr|\ Weaver cross reference output methods (`20`_), |srarr|\ (`21`_) + |srarr|\ Weaver quoted characters (`15`_) + |srarr|\ Weaver document chunk begin-end (`16`_) + |srarr|\ Weaver reference summary, used by code chunk and file chunk (`17`_) + |srarr|\ Weaver code chunk begin-end (`18`_) + |srarr|\ Weaver file chunk begin-end (`19`_) + |srarr|\ Weaver reference command output (`20`_) + |srarr|\ Weaver cross reference output methods (`21`_), |srarr|\ (`22`_) .. .. class:: small - |loz| *Weaver subclass of Emitter to create documentation (12)*. Used by: Emitter class hierarchy... (`2`_) + |loz| *Weaver subclass of Emitter to create documentation (13)*. Used by: Emitter class hierarchy... (`2`_) The ``doOpen()`` method opens the file for writing. For weavers, the file extension @@ -1740,26 +1956,29 @@ the local indentation required to weave a code chunk. The "indent" can vary beca we're not always starting a fresh line with ``weaveReferenceTo()``. -.. _`13`: -.. rubric:: Weaver doOpen, doClose and addIndent overrides (13) = +.. _`14`: +.. rubric:: Weaver doOpen, doClose and addIndent overrides (14) = .. parsed-literal:: :class: code - def doOpen( self, basename ): - self.fileName= basename + self.extension - self.logger.info( "Weaving {!r}".format(self.fileName) ) - self.theFile= open( self.fileName, "w" ) - self.readdIndent( self.code\_indent ) - def doClose( self ): + def doOpen(self) -> None: + """Create the final woven document.""" + self.filePath = self.filePath.with\_suffix(self.extension) + self.logger.info("Weaving '%s'", self.filePath) + self.theFile = self.filePath.open("w") + self.readdIndent(self.code\_indent) + + def doClose(self) -> None: self.theFile.close() - self.logger.info( "Wrote {:d} lines to {!r}".format( - self.linesWritten, self.fileName) ) - def addIndent( self, increment=0 ): + self.logger.info("Wrote %d lines to %r", self.linesWritten, self.filePath) + + def addIndent(self, increment: int = 0) -> None: """increment not used when weaving""" - self.context.append( self.context[-1] ) - self.log\_indent.debug( "addIndent {!s}: {!r}".format(self.lastIndent, self.context) ) - def codeFinish( self ): + self.context.append(self.context[-1]) + self.log\_indent.debug("addIndent %d: %r", self.lastIndent, self.context) + + def codeFinish(self) -> None: pass # Not needed when weaving @@ -1767,38 +1986,39 @@ we're not always starting a fresh line with ``weaveReferenceTo()``. .. class:: small - |loz| *Weaver doOpen, doClose and addIndent overrides (13)*. Used by: Weaver subclass of Emitter... (`12`_) + |loz| *Weaver doOpen, doClose and addIndent overrides (14)*. Used by: Weaver subclass of Emitter... (`13`_) -This is an overly simplistic list. We use the ``parsed-literal`` -directive because we're including links and what-not in the code. +The following list of markup escapes for RST may not be **all** that are requiresd. +The general template for cude uses ``parsed-literal`` +directive because it can include links comingled with the code. We have to quote certain inline markup -- but only when the characters are paired in a way that might confuse RST. -We really should use patterns like ```.*?```, ``_.*?_``, ``\*.*?\*``, and ``\|.*?\|`` +We could use patterns like ```.*?```, ``_.*?_``, ``\*.*?\*``, and ``\|.*?\|`` to look for paired RST inline markup and quote just these special character occurrences. -.. _`14`: -.. rubric:: Weaver quoted characters (14) = +.. _`15`: +.. rubric:: Weaver quoted characters (15) = .. parsed-literal:: :class: code - quoted\_chars = [ - # prevent some RST markup from being recognized - ('\\\\',r'\\\\'), # Must be first. - ('\`',r'\\\`'), - ('\_',r'\\\_'), - ('\*',r'\\\*'), - ('\|',r'\\\|'), + # Prevent some RST markup from being recognized (and processed) in code. + quoted\_chars: list[tuple[str, str]] = [ + ('\\\\', r'\\\\'), # Must be first. + ('\`', r'\\\`'), + ('\_', r'\\\_'), + ('\*', r'\\\*'), + ('\|', r'\\\|'), ] .. .. class:: small - |loz| *Weaver quoted characters (14)*. Used by: Weaver subclass of Emitter... (`12`_) + |loz| *Weaver quoted characters (15)*. Used by: Weaver subclass of Emitter... (`13`_) The remaining methods apply a chunk to a template. @@ -1811,15 +2031,16 @@ of possible additional processing. -.. _`15`: -.. rubric:: Weaver document chunk begin-end (15) = +.. _`16`: +.. rubric:: Weaver document chunk begin-end (16) = .. parsed-literal:: :class: code - def docBegin( self, aChunk ): + def docBegin(self, aChunk: Chunk) -> None: pass - def docEnd( self, aChunk ): + + def docEnd(self, aChunk: Chunk) -> None: pass @@ -1827,7 +2048,7 @@ of possible additional processing. .. class:: small - |loz| *Weaver document chunk begin-end (15)*. Used by: Weaver subclass of Emitter... (`12`_) + |loz| *Weaver document chunk begin-end (16)*. Used by: Weaver subclass of Emitter... (`13`_) Each code chunk includes the places where the chunk is referenced. @@ -1839,22 +2060,28 @@ Each code chunk includes the places where the chunk is referenced. Currently, something more complex is used. -.. _`16`: -.. rubric:: Weaver reference summary, used by code chunk and file chunk (16) = +.. _`17`: +.. rubric:: Weaver reference summary, used by code chunk and file chunk (17) = .. parsed-literal:: :class: code - ref\_template = string.Template( "${refList}" ) + ref\_template = string.Template( + "${refList}" + ) ref\_separator = "; " - ref\_item\_template = string.Template( "$fullName (\`${seq}\`\_)" ) - def references( self, aChunk ): - references= aChunk.references\_list( self ) + ref\_item\_template = string.Template( + "$fullName (\`${seq}\`\_)" + ) + + def references(self, aChunk: Chunk) -> str: + references = aChunk.references(self) if len(references) != 0: - refList= [ - self.ref\_item\_template.substitute( seq=s, fullName=n ) - for n,s in references ] - return self.ref\_template.substitute( refList=self.ref\_separator.join( refList ) ) + refList = [ + self.ref\_item\_template.substitute(seq=s, fullName=n) + for n, s in references + ] + return self.ref\_template.substitute(refList=self.ref\_separator.join(refList)) else: return "" @@ -1863,7 +2090,7 @@ Each code chunk includes the places where the chunk is referenced. .. class:: small - |loz| *Weaver reference summary, used by code chunk and file chunk (16)*. Used by: Weaver subclass of Emitter... (`12`_) + |loz| *Weaver reference summary, used by code chunk and file chunk (17)*. Used by: Weaver subclass of Emitter... (`13`_) @@ -1877,31 +2104,47 @@ refer to this chunk can be emitted. -.. _`17`: -.. rubric:: Weaver code chunk begin-end (17) = +.. _`18`: +.. rubric:: Weaver code chunk begin-end (18) = .. parsed-literal:: :class: code - cb\_template = string.Template( "\\n.. \_\`${seq}\`:\\n.. rubric:: ${fullName} (${seq}) ${concat}\\n.. parsed-literal::\\n :class: code\\n\\n" ) + cb\_template = string.Template( + dedent(""" + .. \_\`${seq}\`: + .. rubric:: ${fullName} (${seq}) ${concat} + .. parsed-literal:: + :class: code + + """) + ) + + ce\_template = string.Template( + dedent(""" + .. + + .. class:: small + + \|loz\| \*${fullName} (${seq})\*. Used by: ${references} + """) + ) - def codeBegin( self, aChunk ): + def codeBegin(self, aChunk: Chunk) -> None: txt = self.cb\_template.substitute( - seq= aChunk.seq, - lineNumber= aChunk.lineNumber, - fullName= aChunk.fullName, - concat= "=" if aChunk.initial else "+=", # RST Separator + seq=aChunk.seq, + lineNumber=aChunk.lineNumber, + fullName=aChunk.fullName, + concat="=" if aChunk.initial else "+=", ) - self.write( txt ) + self.write(txt) - ce\_template = string.Template( "\\n..\\n\\n .. class:: small\\n\\n \|loz\| \*${fullName} (${seq})\*. Used by: ${references}\\n" ) - - def codeEnd( self, aChunk ): + def codeEnd(self, aChunk: Chunk) -> None: txt = self.ce\_template.substitute( - seq= aChunk.seq, - lineNumber= aChunk.lineNumber, - fullName= aChunk.fullName, - references= self.references( aChunk ), + seq = aChunk.seq, + lineNumber = aChunk.lineNumber, + fullName = aChunk.fullName, + references = self.references(aChunk), ) self.write(txt) @@ -1910,9 +2153,13 @@ refer to this chunk can be emitted. .. class:: small - |loz| *Weaver code chunk begin-end (17)*. Used by: Weaver subclass of Emitter... (`12`_) + |loz| *Weaver code chunk begin-end (18)*. Used by: Weaver subclass of Emitter... (`13`_) +**TODO:** Is this really necessary? Should we inject additional material +into the woven output? It seems like a potentially bad idea because +of the complications of various markup tool chains. + The ``fileBegin()`` method emits the necessary material prior to a chunk of source code, defined with the ``@o`` command. A subclass would override this to provide specific text @@ -1925,40 +2172,56 @@ There shouldn't be a list of references to a file. We assert that this list is always empty. -.. _`18`: -.. rubric:: Weaver file chunk begin-end (18) = +.. _`19`: +.. rubric:: Weaver file chunk begin-end (19) = .. parsed-literal:: :class: code - fb\_template = string.Template( "\\n.. \_\`${seq}\`:\\n.. rubric:: ${fullName} (${seq}) ${concat}\\n.. parsed-literal::\\n :class: code\\n\\n" ) + fb\_template = string.Template( + dedent(""" + .. \_\`${seq}\`: + .. rubric:: ${fullName} (${seq}) ${concat} + .. parsed-literal:: + :class: code + + """) + ) - def fileBegin( self, aChunk ): - txt= self.fb\_template.substitute( - seq= aChunk.seq, - lineNumber= aChunk.lineNumber, - fullName= aChunk.fullName, - concat= "=" if aChunk.initial else "+=", # RST Separator - ) - self.write( txt ) + fe\_template = string.Template( + dedent(""" + .. + + .. class:: small + + \|loz\| \*${fullName} (${seq})\*. + """) + ) - fe\_template= string.Template( "\\n..\\n\\n .. class:: small\\n\\n \|loz\| \*${fullName} (${seq})\*.\\n" ) + def fileBegin(self, aChunk: Chunk) -> None: + txt = self.fb\_template.substitute( + seq=aChunk.seq, + lineNumber=aChunk.lineNumber, + fullName=aChunk.fullName, + concat="=" if aChunk.initial else "+=", + ) + self.write(txt) - def fileEnd( self, aChunk ): - assert len(self.references( aChunk )) == 0 - txt= self.fe\_template.substitute( - seq= aChunk.seq, - lineNumber= aChunk.lineNumber, - fullName= aChunk.fullName, - references= [] ) - self.write( txt ) + def fileEnd(self, aChunk: Chunk) -> None: + assert len(self.references(aChunk)) == 0 + txt = self.fe\_template.substitute( + seq=aChunk.seq, + lineNumber=aChunk.lineNumber, + fullName=aChunk.fullName, + references=[]) + self.write(txt) .. .. class:: small - |loz| *Weaver file chunk begin-end (18)*. Used by: Weaver subclass of Emitter... (`12`_) + |loz| *Weaver file chunk begin-end (19)*. Used by: Weaver subclass of Emitter... (`13`_) The ``referenceTo()`` method emits a reference to @@ -1974,26 +2237,31 @@ in a sequence of references. It's usually a ``", "``, but that might be changed a simple ``" "`` because it looks better. -.. _`19`: -.. rubric:: Weaver reference command output (19) = +.. _`20`: +.. rubric:: Weaver reference command output (20) = .. parsed-literal:: :class: code - refto\_name\_template= string.Template(r"\|srarr\|\\ ${fullName} (\`${seq}\`\_)") - refto\_seq\_template= string.Template("\|srarr\|\\ (\`${seq}\`\_)") - refto\_seq\_separator= ", " + refto\_name\_template = string.Template( + r"\|srarr\|\\ ${fullName} (\`${seq}\`\_)" + ) + refto\_seq\_template = string.Template( + r"\|srarr\|\\ (\`${seq}\`\_)" + ) + refto\_seq\_separator = ", " - def referenceTo( self, aName, seq ): + def referenceTo(self, aName: str \| None, seq: int) -> str: """Weave a reference to a chunk. Provide name to get a full reference. - name=None to get a short reference.""" + name=None to get a short reference. + """ if aName: - return self.refto\_name\_template.substitute( fullName= aName, seq= seq ) + return self.refto\_name\_template.substitute(fullName=aName, seq=seq) else: - return self.refto\_seq\_template.substitute( seq= seq ) + return self.refto\_seq\_template.substitute(seq=seq) - def referenceSep( self ): + def referenceSep(self) -> str: """Separator between references.""" return self.refto\_seq\_separator @@ -2002,7 +2270,7 @@ a simple ``" "`` because it looks better. .. class:: small - |loz| *Weaver reference command output (19)*. Used by: Weaver subclass of Emitter... (`12`_) + |loz| *Weaver reference command output (20)*. Used by: Weaver subclass of Emitter... (`13`_) The ``xrefHead()`` method puts decoration in front of cross-reference @@ -2028,79 +2296,103 @@ The default behavior simply writes the Python data structure used to represent cross reference information. A subclass may override this to change the look of the final woven document. +Note that the ``xref_item_template`` and ``xref_empty_template`` have no leading ``\n`` character. They have +an indentation on the first line, however, to make ``dedent()`` work. The spaces to create proper +RST indentation are a bit fiddly here. -.. _`20`: -.. rubric:: Weaver cross reference output methods (20) = + +.. _`21`: +.. rubric:: Weaver cross reference output methods (21) = .. parsed-literal:: :class: code - xref\_head\_template = string.Template( "\\n" ) - xref\_foot\_template = string.Template( "\\n" ) - xref\_item\_template = string.Template( ":${fullName}:\\n ${refList}\\n" ) - xref\_empty\_template = string.Template( "(None)\\n" ) + xref\_head\_template = string.Template( + dedent(""" + """) + ) + xref\_foot\_template = string.Template( + dedent(""" + """) + ) + xref\_item\_template = string.Template( + dedent(""" :${fullName}: + ${refList} + """) + ) + xref\_empty\_template = string.Template( + dedent(""" (None) + """) + ) - def xrefHead( self ): + def xrefHead(self) -> None: txt = self.xref\_head\_template.substitute() - self.write( txt ) + self.write(txt) - def xrefFoot( self ): + def xrefFoot(self) -> None: txt = self.xref\_foot\_template.substitute() - self.write( txt ) + self.write(txt) - def xrefLine( self, name, refList ): - refList= [ self.referenceTo( None, r ) for r in refList ] - txt= self.xref\_item\_template.substitute( fullName= name, refList = " ".join(refList) ) # RST Separator - self.write( txt ) + def xrefLine(self, name: str, refList: list[int]) -> None: + refList\_txt = [self.referenceTo(None, r) for r in refList] + txt = self.xref\_item\_template.substitute(fullName=name, refList = " ".join(refList\_txt)) # RST Separator + self.write(txt) - def xrefEmpty( self ): - self.write( self.xref\_empty\_template.substitute() ) + def xrefEmpty(self) -> None: + self.write(self.xref\_empty\_template.substitute()) .. .. class:: small - |loz| *Weaver cross reference output methods (20)*. Used by: Weaver subclass of Emitter... (`12`_) + |loz| *Weaver cross reference output methods (21)*. Used by: Weaver subclass of Emitter... (`13`_) Cross-reference definition line -.. _`21`: -.. rubric:: Weaver cross reference output methods (21) += +.. _`22`: +.. rubric:: Weaver cross reference output methods (22) += .. parsed-literal:: :class: code - name\_def\_template = string.Template( '[\`${seq}\`\_]' ) - name\_ref\_template = string.Template( '\`${seq}\`\_' ) + name\_def\_template = string.Template( + '[\`${seq}\`\_]' + ) + name\_ref\_template = string.Template( + '\`${seq}\`\_' + ) - def xrefDefLine( self, name, defn, refList ): - templates = { defn: self.name\_def\_template } - refTxt= [ templates.get(r,self.name\_ref\_template).substitute( seq= r ) - for r in sorted( refList + [defn] ) - ] + def xrefDefLine(self, name: str, defn: int, refList: list[int]) -> None: + """Special template for the definition, default reference for all others.""" + templates = {defn: self.name\_def\_template} + refTxt = [ + templates.get(r, self.name\_ref\_template).substitute(seq=r) + for r in sorted(refList + [defn]) + ] # Generic space separator - txt= self.xref\_item\_template.substitute( fullName= name, refList = " ".join(refTxt) ) - self.write( txt ) + txt = self.xref\_item\_template.substitute(fullName=name, refList=" ".join(refTxt)) + self.write(txt) .. .. class:: small - |loz| *Weaver cross reference output methods (21)*. Used by: Weaver subclass of Emitter... (`12`_) + |loz| *Weaver cross reference output methods (22)*. Used by: Weaver subclass of Emitter... (`13`_) RST subclass of Weaver ~~~~~~~~~~~~~~~~~~~~~~~~~~ -A degenerate case. This slightly simplifies the configuration and makes the output +A degenerate case: the base ``Weaver`` class does ``RST``. +Using this class name slightly simplifies the configuration and makes the output look a little nicer. -.. _`22`: -.. rubric:: RST subclass of Weaver (22) = +.. _`23`: +.. rubric:: RST subclass of Weaver (23) = .. parsed-literal:: :class: code @@ -2112,7 +2404,7 @@ look a little nicer. .. class:: small - |loz| *RST subclass of Weaver (22)*. Used by: Emitter class hierarchy... (`2`_) + |loz| *RST subclass of Weaver (23)*. Used by: Emitter class hierarchy... (`2`_) @@ -2127,10 +2419,10 @@ given to the ``weave()`` method of the Web. .. parsed-literal:: - w= Web() - WebReader().load(w,"somefile.w") - weave_latex= LaTeX() - w.weave( weave_latex ) + w = Web() + WebReader().load(w, "somefile.w") + weave_latex = LaTeX() + w.weave(weave_latex) Note that the template language and LaTeX both use ``$``. This means that all ``$`` that are intended to be output to LaTeX @@ -2144,34 +2436,37 @@ function pretty well in most L\ !sub:`A`\ T\ !sub:`E`\ X documents. -.. _`23`: -.. rubric:: LaTeX subclass of Weaver (23) = +.. _`24`: +.. rubric:: LaTeX subclass of Weaver (24) = .. parsed-literal:: :class: code - class LaTeX( Weaver ): + class LaTeX(Weaver): """LaTeX formatting for XRef's and code blocks when weaving. - Requires \\\\usepackage{fancyvrb} + Requires \`\`\\\\usepackage{fancyvrb}\`\` """ - extension= ".tex" - code\_indent= 0 - header= """\\n\\\\usepackage{fancyvrb}\\n""" + extension = ".tex" + code\_indent = 0 + # Not actually used + header = dedent(""" + \\\\usepackage{fancyvrb} + """) - |srarr|\ LaTeX code chunk begin (`24`_) - |srarr|\ LaTeX code chunk end (`25`_) - |srarr|\ LaTeX file output begin (`26`_) - |srarr|\ LaTeX file output end (`27`_) - |srarr|\ LaTeX references summary at the end of a chunk (`28`_) - |srarr|\ LaTeX write a line of code (`29`_) - |srarr|\ LaTeX reference to a chunk (`30`_) + |srarr|\ LaTeX code chunk begin (`25`_) + |srarr|\ LaTeX code chunk end (`26`_) + |srarr|\ LaTeX file output begin (`27`_) + |srarr|\ LaTeX file output end (`28`_) + |srarr|\ LaTeX references summary at the end of a chunk (`29`_) + |srarr|\ LaTeX write a line of code (`30`_) + |srarr|\ LaTeX reference to a chunk (`31`_) .. .. class:: small - |loz| *LaTeX subclass of Weaver (23)*. Used by: Emitter class hierarchy... (`2`_) + |loz| *LaTeX subclass of Weaver (24)*. Used by: Emitter class hierarchy... (`2`_) The LaTeX ``open()`` method opens the woven file by replacing the @@ -2182,26 +2477,31 @@ The LaTeX ``codeBegin()`` template writes the header prior to a chunk of source code. It aligns the block to the left, prints an italicised header, and opens a preformatted block. - +There's no leading ``\n`` in the template -- we're trying to avoid an indent when weaving. +To make ``dedent()`` work, we have to provide the same leading whitespace on the first line +to match the subsequent lines. -.. _`24`: -.. rubric:: LaTeX code chunk begin (24) = + +.. _`25`: +.. rubric:: LaTeX code chunk begin (25) = .. parsed-literal:: :class: code - cb\_template = string.Template( """\\\\label{pyweb${seq}} - \\\\begin{flushleft} - \\\\textit{Code example ${fullName} (${seq})} - \\\\begin{Verbatim}[commandchars=\\\\\\\\\\\\{\\\\},codes={\\\\catcode\`$$=3\\\\catcode\`^=7},frame=single]\\n""") # Prevent indent + cb\_template = string.Template( + dedent(""" \\\\label{pyweb${seq}} + \\\\begin{flushleft} + \\\\textit{Code example ${fullName} (${seq})} + \\\\begin{Verbatim}[commandchars=\\\\\\\\\\\\{\\\\},codes={\\\\catcode\`$$=3\\\\catcode\`^=7},frame=single] + """) + ) .. .. class:: small - |loz| *LaTeX code chunk begin (24)*. Used by: LaTeX subclass... (`23`_) - + |loz| *LaTeX code chunk begin (25)*. Used by: LaTeX subclass... (`24`_) The LaTeX ``codeEnd()`` template writes the trailer subsequent to @@ -2211,23 +2511,26 @@ to the chunk that invokes this chunk; finally, it restores paragraph indentation. -.. _`25`: -.. rubric:: LaTeX code chunk end (25) = +.. _`26`: +.. rubric:: LaTeX code chunk end (26) = .. parsed-literal:: :class: code - ce\_template= string.Template(""" - \\\\end{Verbatim} - ${references} - \\\\end{flushleft}\\n""") # Prevent indentation + ce\_template = string.Template( + dedent(""" + \\\\end{Verbatim} + ${references} + \\\\end{flushleft} + """) + ) .. .. class:: small - |loz| *LaTeX code chunk end (25)*. Used by: LaTeX subclass... (`23`_) + |loz| *LaTeX code chunk end (26)*. Used by: LaTeX subclass... (`24`_) @@ -2237,20 +2540,20 @@ start of a code chunk. -.. _`26`: -.. rubric:: LaTeX file output begin (26) = +.. _`27`: +.. rubric:: LaTeX file output begin (27) = .. parsed-literal:: :class: code - fb\_template= cb\_template + fb\_template = cb\_template .. .. class:: small - |loz| *LaTeX file output begin (26)*. Used by: LaTeX subclass... (`23`_) + |loz| *LaTeX file output begin (27)*. Used by: LaTeX subclass... (`24`_) The LaTeX ``fileEnd()`` template writes the trailer subsequent to @@ -2259,50 +2562,66 @@ a tangled file. This closes the preformatted block, calls the LaTeX invokes this chunk, and restores normal indentation. -.. _`27`: -.. rubric:: LaTeX file output end (27) = +.. _`28`: +.. rubric:: LaTeX file output end (28) = .. parsed-literal:: :class: code - fe\_template= ce\_template + fe\_template = ce\_template .. .. class:: small - |loz| *LaTeX file output end (27)*. Used by: LaTeX subclass... (`23`_) + |loz| *LaTeX file output end (28)*. Used by: LaTeX subclass... (`24`_) The ``references()`` template writes a list of references after a chunk of code. Each reference includes the example number, the title, and a reference to the LaTeX section and page numbers on which the referring block appears. + +The spacing around ``ref_item_template`` and ``ref_template`` are particularly fiddly. +This isn't easy to prepare with ``dedent()``. The ``indent()`` provides the indent +that makes the resulting LaTeX readable, distinct from the indent that makes the code readable. -.. _`28`: -.. rubric:: LaTeX references summary at the end of a chunk (28) = +.. _`29`: +.. rubric:: LaTeX references summary at the end of a chunk (29) = .. parsed-literal:: :class: code - ref\_item\_template = string.Template( """ - \\\\item Code example ${fullName} (${seq}) (Sect. \\\\ref{pyweb${seq}}, p. \\\\pageref{pyweb${seq}})\\n""") - ref\_template = string.Template( """ - \\\\footnotesize - Used by: - \\\\begin{list}{}{} - ${refList} - \\\\end{list} - \\\\normalsize\\n""") + ref\_item\_template = string.Template( + indent( + dedent(""" + \\\\item Code example ${fullName} (${seq}) (Sect. \\\\ref{pyweb${seq}}, p. \\\\pageref{pyweb${seq}}) + """), + ' ' + ) + ) + + ref\_template = string.Template( + indent( + dedent(""" + \\\\footnotesize + Used by: + \\\\begin{list}{}{} + ${refList} + \\\\end{list} + \\\\normalsize"""), + ' ' + ) + ) .. .. class:: small - |loz| *LaTeX references summary at the end of a chunk (28)*. Used by: LaTeX subclass... (`23`_) + |loz| *LaTeX references summary at the end of a chunk (29)*. Used by: LaTeX subclass... (`24`_) The ``quote()`` method quotes a single line of code to the @@ -2313,16 +2632,16 @@ block. Our one compromise is a thin space if the phrase -.. _`29`: -.. rubric:: LaTeX write a line of code (29) = +.. _`30`: +.. rubric:: LaTeX write a line of code (30) = .. parsed-literal:: :class: code - quoted\_chars = [ - ("\\\\end{Verbatim}", "\\\\end\\,{Verbatim}"), # Allow \\end{Verbatim} - ("\\\\{","\\\\\\,{"), # Prevent unexpected commands in Verbatim - ("$","\\\\$"), # Prevent unexpected math in Verbatim + quoted\_chars: list[tuple[str, str]] = [ + ("\\\\end{Verbatim}", "\\\\end\\\\,{Verbatim}"), # Allow \\end{Verbatim} in a Verbatim context + ("\\\\{", "\\\\\\\\,{"), # Prevent unexpected commands in Verbatim + ("$", "\\\\$"), # Prevent unexpected math in Verbatim ] @@ -2330,7 +2649,7 @@ block. Our one compromise is a thin space if the phrase .. class:: small - |loz| *LaTeX write a line of code (29)*. Used by: LaTeX subclass... (`23`_) + |loz| *LaTeX write a line of code (30)*. Used by: LaTeX subclass... (`24`_) The ``referenceTo()`` template writes a reference to another chunk of @@ -2339,28 +2658,31 @@ the current line of code. -.. _`30`: -.. rubric:: LaTeX reference to a chunk (30) = +.. _`31`: +.. rubric:: LaTeX reference to a chunk (31) = .. parsed-literal:: :class: code - refto\_name\_template= string.Template("""$$\\\\triangleright$$ Code Example ${fullName} (${seq})""") - refto\_seq\_template= string.Template("""(${seq})""") + refto\_name\_template = string.Template( + """$$\\\\triangleright$$ Code Example ${fullName} (${seq})""" + ) + + refto\_seq\_template = string.Template( + """(${seq})""" + ) .. .. class:: small - |loz| *LaTeX reference to a chunk (30)*. Used by: LaTeX subclass... (`23`_) + |loz| *LaTeX reference to a chunk (31)*. Used by: LaTeX subclass... (`24`_) HTML subclasses of Weaver ~~~~~~~~~~~~~~~~~~~~~~~~~~ -This works, but, it's not clear that it should be kept. - An instance of ``HTML`` can be used by the ``Web`` object to weave an output document. The instance is created outside the Web, and given to the ``weave()`` method of the Web. @@ -2368,10 +2690,10 @@ given to the ``weave()`` method of the Web. .. parsed-literal:: - w= Web() + w = Web() WebReader().load(w,"somefile.w") - weave_html= HTML() - w.weave( weave_html ) + weave_html = HTML() + w.weave(weave_html) Variations in the output formatting are accomplished by having @@ -2379,7 +2701,6 @@ variant subclasses of HTML. In this implementation, we have two variations: full path references, and short references. The base class produces complete reference paths; a subclass produces abbreviated references. - The ``HTML`` subclass defines a Weaver that is customized to produce HTML output of code sections and cross reference information. @@ -2390,51 +2711,52 @@ An ``HTMLShort`` subclass defines a Weaver that produces HTML output with abbreviated (no name) cross references at the end of the chunk. -.. _`31`: -.. rubric:: HTML subclass of Weaver (31) = +.. _`32`: +.. rubric:: HTML subclass of Weaver (32) = .. parsed-literal:: :class: code - class HTML( Weaver ): + class HTML(Weaver): """HTML formatting for XRef's and code blocks when weaving.""" - extension= ".html" - code\_indent= 0 - header= "" - |srarr|\ HTML code chunk begin (`33`_) - |srarr|\ HTML code chunk end (`34`_) - |srarr|\ HTML output file begin (`35`_) - |srarr|\ HTML output file end (`36`_) - |srarr|\ HTML references summary at the end of a chunk (`37`_) - |srarr|\ HTML write a line of code (`38`_) - |srarr|\ HTML reference to a chunk (`39`_) - |srarr|\ HTML simple cross reference markup (`40`_) + extension = ".html" + code\_indent = 0 + header = "" + + |srarr|\ HTML code chunk begin (`34`_) + |srarr|\ HTML code chunk end (`35`_) + |srarr|\ HTML output file begin (`36`_) + |srarr|\ HTML output file end (`37`_) + |srarr|\ HTML references summary at the end of a chunk (`38`_) + |srarr|\ HTML write a line of code (`39`_) + |srarr|\ HTML reference to a chunk (`40`_) + |srarr|\ HTML simple cross reference markup (`41`_) .. .. class:: small - |loz| *HTML subclass of Weaver (31)*. Used by: Emitter class hierarchy... (`2`_) + |loz| *HTML subclass of Weaver (32)*. Used by: Emitter class hierarchy... (`2`_) -.. _`32`: -.. rubric:: HTML subclass of Weaver (32) += +.. _`33`: +.. rubric:: HTML subclass of Weaver (33) += .. parsed-literal:: :class: code - class HTMLShort( HTML ): + class HTMLShort(HTML): """HTML formatting for XRef's and code blocks when weaving with short references.""" - |srarr|\ HTML short references summary at the end of a chunk (`42`_) + |srarr|\ HTML short references summary at the end of a chunk (`43`_) .. .. class:: small - |loz| *HTML subclass of Weaver (32)*. Used by: Emitter class hierarchy... (`2`_) + |loz| *HTML subclass of Weaver (33)*. Used by: Emitter class hierarchy... (`2`_) The ``codeBegin()`` template starts a chunk of code, defined with ``@d``, providing a label @@ -2442,24 +2764,30 @@ and HTML tags necessary to set the code off visually. -.. _`33`: -.. rubric:: HTML code chunk begin (33) = +.. _`34`: +.. rubric:: HTML code chunk begin (34) = .. parsed-literal:: :class: code - cb\_template= string.Template(""" - - -

    ${fullName} (${seq}) ${concat}

    -
    \\n""")
    +    cb\_template = string.Template(
    +        indent(
    +            dedent("""
    +                
    +                
    +                

    ${fullName} (${seq}) ${concat}

    +
    
    +                """),
    +            '    '
    +        )
    +    )
         
     
     ..
     
         ..  class:: small
     
    -        |loz| *HTML code chunk begin (33)*. Used by: HTML subclass... (`31`_)
    +        |loz| *HTML code chunk begin (34)*. Used by: HTML subclass... (`32`_)
     
     
     The ``codeEnd()`` template ends a chunk of code, providing a HTML tags necessary 
    @@ -2467,47 +2795,59 @@ to finish the code block visually.  This calls the references method to
     write the list of chunks that reference this chunk.
     
     
    -..  _`34`:
    -..  rubric:: HTML code chunk end (34) =
    +..  _`35`:
    +..  rubric:: HTML code chunk end (35) =
     ..  parsed-literal::
         :class: code
     
         
    -    ce\_template= string.Template("""
    -    
    -

    ${fullName} (${seq}). - ${references} -

    \\n""") + ce\_template = string.Template( + indent( + dedent(""" +
    +

    ${fullName} (${seq}). + ${references} +

    + """), + ' ' + ) + ) .. .. class:: small - |loz| *HTML code chunk end (34)*. Used by: HTML subclass... (`31`_) + |loz| *HTML code chunk end (35)*. Used by: HTML subclass... (`32`_) The ``fileBegin()`` template starts a chunk of code, defined with ``@o``, providing a label and HTML tags necessary to set the code off visually. -.. _`35`: -.. rubric:: HTML output file begin (35) = +.. _`36`: +.. rubric:: HTML output file begin (36) = .. parsed-literal:: :class: code - fb\_template= string.Template(""" - -

    \`\`${fullName}\`\` (${seq}) ${concat}

    -
    \\n""") # Prevent indent
    +    fb\_template = string.Template(
    +        indent(
    +            dedent("""            
    +                
    +                

    \`\`${fullName}\`\` (${seq}) ${concat}

    +
    
    +            """), # No leading \\\\n.
    +            '    '
    +        )
    +    )
         
     
     ..
     
         ..  class:: small
     
    -        |loz| *HTML output file begin (35)*. Used by: HTML subclass... (`31`_)
    +        |loz| *HTML output file begin (36)*. Used by: HTML subclass... (`32`_)
     
     
     The ``fileEnd()`` template ends a chunk of code, providing a HTML tags necessary 
    @@ -2515,23 +2855,29 @@ to finish the code block visually.  This calls the references method to
     write the list of chunks that reference this chunk.
     
     
    -..  _`36`:
    -..  rubric:: HTML output file end (36) =
    +..  _`37`:
    +..  rubric:: HTML output file end (37) =
     ..  parsed-literal::
         :class: code
     
         
    -    fe\_template= string.Template( """
    -

    ◊ \`\`${fullName}\`\` (${seq}). - ${references} -

    \\n""") + fe\_template = string.Template( + indent( + dedent("""
    +

    ◊ \`\`${fullName}\`\` (${seq}). + ${references} +

    + """), + ' ' + ) + ) .. .. class:: small - |loz| *HTML output file end (36)*. Used by: HTML subclass... (`31`_) + |loz| *HTML output file end (37)*. Used by: HTML subclass... (`32`_) The ``references()`` template writes the list of chunks that refer to this chunk. @@ -2539,23 +2885,26 @@ Note that this list could be rather long because of the possibility of transitive references. -.. _`37`: -.. rubric:: HTML references summary at the end of a chunk (37) = +.. _`38`: +.. rubric:: HTML references summary at the end of a chunk (38) = .. parsed-literal:: :class: code ref\_item\_template = string.Template( - '${fullName} (${seq})' + '${fullName} (${seq})' + ) + + ref\_template = string.Template( + ' Used by ${refList}.' ) - ref\_template = string.Template( ' Used by ${refList}.' ) .. .. class:: small - |loz| *HTML references summary at the end of a chunk (37)*. Used by: HTML subclass... (`31`_) + |loz| *HTML references summary at the end of a chunk (38)*. Used by: HTML subclass... (`32`_) The ``quote()`` method quotes an individual line of code for HTML purposes. @@ -2563,14 +2912,14 @@ This encodes the four basic HTML entities (``<``, ``>``, ``&``, ``"``) to preven as HTML. -.. _`38`: -.. rubric:: HTML write a line of code (38) = +.. _`39`: +.. rubric:: HTML write a line of code (39) = .. parsed-literal:: :class: code - quoted\_chars = [ - ("&", "&"), # Must be first + quoted\_chars: list[tuple[str, str]] = [ + ("&", "&"), # Must be first ("<", "<"), (">", ">"), ('"', """), @@ -2581,7 +2930,7 @@ as HTML. .. class:: small - |loz| *HTML write a line of code (38)*. Used by: HTML subclass... (`31`_) + |loz| *HTML write a line of code (39)*. Used by: HTML subclass... (`32`_) The ``referenceTo()`` template writes a reference to another chunk. It uses the @@ -2589,17 +2938,18 @@ direct ``write()`` method so that the reference is indented properly with the surrounding source code. -.. _`39`: -.. rubric:: HTML reference to a chunk (39) = +.. _`40`: +.. rubric:: HTML reference to a chunk (40) = .. parsed-literal:: :class: code refto\_name\_template = string.Template( - '${fullName} (${seq})' + '${fullName} (${seq})' ) + refto\_seq\_template = string.Template( - '(${seq})' + '(${seq})' ) @@ -2607,7 +2957,7 @@ surrounding source code. .. class:: small - |loz| *HTML reference to a chunk (39)*. Used by: HTML subclass... (`31`_) + |loz| *HTML reference to a chunk (40)*. Used by: HTML subclass... (`32`_) The ``xrefHead()`` method writes the heading for any of the cross reference blocks created by @@ -2620,23 +2970,33 @@ The ``xrefLine()`` method writes a line for the file or macro cross reference bl ``@f`` or ``@m``. In this implementation, the cross references are simply unordered lists. -.. _`40`: -.. rubric:: HTML simple cross reference markup (40) = +.. _`41`: +.. rubric:: HTML simple cross reference markup (41) = .. parsed-literal:: :class: code - xref\_head\_template = string.Template( "
    \\n" ) - xref\_foot\_template = string.Template( "
    \\n" ) - xref\_item\_template = string.Template( "
    ${fullName}
    ${refList}
    \\n" ) - |srarr|\ HTML write user id cross reference line (`41`_) + xref\_head\_template = string.Template( + dedent("""
    + """) + ) + xref\_foot\_template = string.Template( + dedent("""
    + """) + ) + xref\_item\_template = string.Template( + dedent("""
    ${fullName}
    ${refList}
    + """) + ) + + |srarr|\ HTML write user id cross reference line (`42`_) .. .. class:: small - |loz| *HTML simple cross reference markup (40)*. Used by: HTML subclass... (`31`_) + |loz| *HTML simple cross reference markup (41)*. Used by: HTML subclass... (`32`_) The ``xrefDefLine()`` method writes a line for the user identifier cross reference blocks created by @@ -2645,21 +3005,26 @@ is included in the correct order with the other instances, but is bold and marke -.. _`41`: -.. rubric:: HTML write user id cross reference line (41) = +.. _`42`: +.. rubric:: HTML write user id cross reference line (42) = .. parsed-literal:: :class: code - name\_def\_template = string.Template( '•${seq}' ) - name\_ref\_template = string.Template( '${seq}' ) + name\_def\_template = string.Template( + '•${seq}' + ) + + name\_ref\_template = string.Template( + '${seq}' + ) .. .. class:: small - |loz| *HTML write user id cross reference line (41)*. Used by: HTML simple cross reference markup (`40`_) + |loz| *HTML write user id cross reference line (42)*. Used by: HTML simple cross reference markup (`41`_) The HTMLShort subclass enhances the HTML class to provide short @@ -2669,20 +3034,22 @@ Note that this list could be rather long because of the possibility of transitive references. -.. _`42`: -.. rubric:: HTML short references summary at the end of a chunk (42) = +.. _`43`: +.. rubric:: HTML short references summary at the end of a chunk (43) = .. parsed-literal:: :class: code - ref\_item\_template = string.Template( '(${seq})' ) + ref\_item\_template = string.Template( + '(${seq})' + ) .. .. class:: small - |loz| *HTML short references summary at the end of a chunk (42)*. Used by: HTML subclass... (`32`_) + |loz| *HTML short references summary at the end of a chunk (43)*. Used by: HTML subclass... (`33`_) Tangler subclass of Emitter @@ -2693,10 +3060,10 @@ instance of ``Tangler`` is given to the ``Web`` class ``tangle()`` method. .. parsed-literal:: - w= Web() + w = Web() WebReader().load(w,"somefile.w") - t= Tangler() - w.tangle( t ) + t = Tangler() + w.tangle(t) The ``Tangler`` subclass extends an Emitter to **tangle** the various @@ -2723,29 +3090,30 @@ There are three configurable values: Show the source line numbers in the output via additional comments. -.. _`43`: -.. rubric:: Tangler subclass of Emitter to create source files with no markup (43) = +.. _`44`: +.. rubric:: Tangler subclass of Emitter to create source files with no markup (44) = .. parsed-literal:: :class: code - class Tangler( Emitter ): + class Tangler(Emitter): """Tangle output files.""" - def \_\_init\_\_( self ): + def \_\_init\_\_(self) -> None: super().\_\_init\_\_() - self.comment\_start= None - self.comment\_end= "" - self.include\_line\_numbers= False - |srarr|\ Tangler doOpen, and doClose overrides (`44`_) - |srarr|\ Tangler code chunk begin (`45`_) - |srarr|\ Tangler code chunk end (`46`_) + self.comment\_start: str = "#" + self.comment\_end: str = "" + self.include\_line\_numbers = False + + |srarr|\ Tangler doOpen, and doClose overrides (`45`_) + |srarr|\ Tangler code chunk begin (`46`_) + |srarr|\ Tangler code chunk end (`47`_) .. .. class:: small - |loz| *Tangler subclass of Emitter to create source files with no markup (43)*. Used by: Emitter class hierarchy... (`2`_) + |loz| *Tangler subclass of Emitter to create source files with no markup (44)*. Used by: Emitter class hierarchy... (`2`_) The default for all tanglers is to create the named file. @@ -2758,37 +3126,31 @@ This ``doClose()`` method overrides the ``Emitter`` class ``doClose()`` method b actual file created by open. -.. _`44`: -.. rubric:: Tangler doOpen, and doClose overrides (44) = +.. _`45`: +.. rubric:: Tangler doOpen, and doClose overrides (45) = .. parsed-literal:: :class: code - def checkPath( self ): - if "/" in self.fileName: - dirname, \_, \_ = self.fileName.rpartition("/") - try: - os.makedirs( dirname ) - self.logger.info( "Creating {!r}".format(dirname) ) - except OSError as e: - # Already exists. Could check for errno.EEXIST. - self.logger.debug( "Exception {!r} creating {!r}".format(e, dirname) ) - def doOpen( self, aFile ): - self.fileName= aFile + def checkPath(self) -> None: + self.filePath.parent.mkdir(parents=True, exist\_ok=True) + + def doOpen(self) -> None: + """Tangle out of the output files.""" self.checkPath() - self.theFile= open( aFile, "w" ) - self.logger.info( "Tangling {!r}".format(aFile) ) - def doClose( self ): + self.theFile = self.filePath.open("w") + self.logger.info("Tangling '%s'", self.filePath) + + def doClose(self) -> None: self.theFile.close() - self.logger.info( "Wrote {:d} lines to {!r}".format( - self.linesWritten, self.fileName) ) + self.logger.info("Wrote %d lines to %r", self.linesWritten, self.filePath) .. .. class:: small - |loz| *Tangler doOpen, and doClose overrides (44)*. Used by: Tangler subclass of Emitter... (`43`_) + |loz| *Tangler doOpen, and doClose overrides (45)*. Used by: Tangler subclass of Emitter... (`44`_) The ``codeBegin()`` method starts emitting a new chunk of code. @@ -2796,31 +3158,27 @@ It does this by setting the Tangler's indent to the prevailing indent at the start of the ``@<`` reference command. -.. _`45`: -.. rubric:: Tangler code chunk begin (45) = +.. _`46`: +.. rubric:: Tangler code chunk begin (46) = .. parsed-literal:: :class: code - def codeBegin( self, aChunk ): - self.log\_indent.debug( " None: + self.log\_indent.debug("{!s}".format(aChunk.fullName) ) + def codeEnd(self, aChunk: Chunk) -> None: + self.log\_indent.debug(">%r", aChunk.fullName) .. .. class:: small - |loz| *Tangler code chunk end (46)*. Used by: Tangler subclass of Emitter... (`43`_) + |loz| *Tangler code chunk end (47)*. Used by: Tangler subclass of Emitter... (`44`_) TanglerMake subclass of Tangler @@ -2854,15 +3212,15 @@ instance of ``TanglerMake`` is given to the ``Web`` class ``tangle()`` method. .. parsed-literal:: - w= Web() + w = Web() WebReader().load(w,"somefile.w") - t= TanglerMake() - w.tangle( t ) + t = TanglerMake() + w.tangle(t) The ``TanglerMake`` subclass extends ``Tangler`` to make the source files more make-friendly. This subclass of ``Tangler`` does not **touch** an output file -where there is no change. This is helpful when **pyWeb**\ 's output is +where there is no change. This is helpful when **py-web-tool**\ 's output is sent to **make**. Using ``TanglerMake`` assures that only files with real changes are rewritten, minimizing recompilation of an application for changes to the associated documentation. @@ -2871,8 +3229,8 @@ This subclass of ``Tangler`` changes how files are opened and closed. -.. _`47`: -.. rubric:: Imports (47) += +.. _`48`: +.. rubric:: Imports (48) += .. parsed-literal:: :class: code @@ -2884,30 +3242,32 @@ are opened and closed. .. class:: small - |loz| *Imports (47)*. Used by: pyweb.py (`153`_) + |loz| *Imports (48)*. Used by: pyweb.py (`155`_) -.. _`48`: -.. rubric:: TanglerMake subclass which is make-sensitive (48) = +.. _`49`: +.. rubric:: TanglerMake subclass which is make-sensitive (49) = .. parsed-literal:: :class: code - class TanglerMake( Tangler ): + class TanglerMake(Tangler): """Tangle output files, leaving files untouched if there are no changes.""" - def \_\_init\_\_( self, \*args ): - super().\_\_init\_\_( \*args ) - self.tempname= None - |srarr|\ TanglerMake doOpen override, using a temporary file (`49`_) - |srarr|\ TanglerMake doClose override, comparing temporary to original (`50`_) + tempname : str + def \_\_init\_\_(self, \*args: Any) -> None: + super().\_\_init\_\_(\*args) + + |srarr|\ TanglerMake doOpen override, using a temporary file (`50`_) + + |srarr|\ TanglerMake doClose override, comparing temporary to original (`51`_) .. .. class:: small - |loz| *TanglerMake subclass which is make-sensitive (48)*. Used by: Emitter class hierarchy... (`2`_) + |loz| *TanglerMake subclass which is make-sensitive (49)*. Used by: Emitter class hierarchy... (`2`_) A ``TanglerMake`` creates a temporary file to collect the @@ -2917,23 +3277,23 @@ a "touch" if the new file is the same as the original. -.. _`49`: -.. rubric:: TanglerMake doOpen override, using a temporary file (49) = +.. _`50`: +.. rubric:: TanglerMake doOpen override, using a temporary file (50) = .. parsed-literal:: :class: code - def doOpen( self, aFile ): - fd, self.tempname= tempfile.mkstemp( dir=os.curdir ) - self.theFile= os.fdopen( fd, "w" ) - self.logger.info( "Tangling {!r}".format(aFile) ) + def doOpen(self) -> None: + fd, self.tempname = tempfile.mkstemp(dir=os.curdir) + self.theFile = os.fdopen(fd, "w") + self.logger.info("Tangling '%s'", self.filePath) .. .. class:: small - |loz| *TanglerMake doOpen override, using a temporary file (49)*. Used by: TanglerMake subclass... (`48`_) + |loz| *TanglerMake doOpen override, using a temporary file (50)*. Used by: TanglerMake subclass... (`49`_) If there is a previous file: compare the temporary file and the previous file. @@ -2946,38 +3306,38 @@ and time) if nothing has changed. -.. _`50`: -.. rubric:: TanglerMake doClose override, comparing temporary to original (50) = +.. _`51`: +.. rubric:: TanglerMake doClose override, comparing temporary to original (51) = .. parsed-literal:: :class: code - def doClose( self ): + def doClose(self) -> None: self.theFile.close() try: - same= filecmp.cmp( self.tempname, self.fileName ) + same = filecmp.cmp(self.tempname, self.filePath) except OSError as e: - same= False # Doesn't exist. Could check for errno.ENOENT + same = False # Doesn't exist. (Could check for errno.ENOENT) if same: - self.logger.info( "No change to {!r}".format(self.fileName) ) - os.remove( self.tempname ) + self.logger.info("Unchanged '%s'", self.filePath) + os.remove(self.tempname) else: # Windows requires the original file name be removed first. - self.checkPath() try: - os.remove( self.fileName ) + self.filePath.unlink() except OSError as e: - pass # Doesn't exist. Could check for errno.ENOENT - os.rename( self.tempname, self.fileName ) - self.logger.info( "Wrote {:d} lines to {!r}".format( - self.linesWritten, self.fileName) ) + pass # Doesn't exist. (Could check for errno.ENOENT) + self.checkPath() + self.filePath.hardlink\_to(self.tempname) + os.remove(self.tempname) + self.logger.info("Wrote %d lines to %s", self.linesWritten, self.filePath) .. .. class:: small - |loz| *TanglerMake doClose override, comparing temporary to original (50)*. Used by: TanglerMake subclass... (`48`_) + |loz| *TanglerMake doClose override, comparing temporary to original (51)*. Used by: TanglerMake subclass... (`49`_) Chunks @@ -2998,22 +3358,25 @@ Each ``Chunk`` instance has one or more pieces of the original input text. This text can be program source, a reference command, or the documentation source. -.. _`51`: -.. rubric:: Chunk class hierarchy - used to describe input chunks (51) = +.. _`52`: +.. rubric:: Chunk class hierarchy - used to describe input chunks (52) = .. parsed-literal:: :class: code - |srarr|\ Chunk class (`52`_) - |srarr|\ NamedChunk class (`63`_), |srarr|\ (`68`_) - |srarr|\ OutputChunk class (`69`_) - |srarr|\ NamedDocumentChunk class (`73`_) + |srarr|\ Chunk base class for anonymous chunks of the file (`53`_) + + |srarr|\ NamedChunk class for defined names (`65`_), |srarr|\ (`70`_) + + |srarr|\ OutputChunk class (`71`_) + + |srarr|\ NamedDocumentChunk class (`75`_) .. .. class:: small - |loz| *Chunk class hierarchy - used to describe input chunks (51)*. Used by: Base Class Definitions (`1`_) + |loz| *Chunk class hierarchy - used to describe input chunks (52)*. Used by: Base Class Definitions (`1`_) The ``Chunk`` class is both the superclass for this hierarchy and the implementation @@ -3052,11 +3415,11 @@ The basic outline for creating a ``Chunk`` instance is as follows: .. parsed-literal:: - w= Web( ) - c= Chunk() - c.webAdd( w ) - c.append( *...some Command...* ) - c.append( *...some Command...* ) + w = Web() + c = Chunk() + c.webAdd(w) + c.append(*...some Command...*) + c.append(*...some Command...*) Before weaving or tangling, a cross reference is created for all user identifiers in all of the ``Chunk`` instances. @@ -3067,13 +3430,13 @@ the identifier. .. parsed-literal:: - ident= [] + ident = [] for c in *the Web's named chunk list*: - ident.extend( c.getUserIDRefs() ) + ident.extend(c.getUserIDRefs()) for i in ident: - pattern= re.compile('\W{!s}\W'.format(i) ) + pattern = re.compile(f'\\W{i!s}\\W' ) for c in *the Web's named chunk list*: - c.searchForRE( pattern ) + c.searchForRE(pattern) A ``Chunk`` is woven or tangled by the ``Web``. The basic outline for weaving is as follows. The tangling action is essentially the same. @@ -3081,7 +3444,7 @@ as follows. The tangling action is essentially the same. .. parsed-literal:: for c in *the Web's chunk list*: - c.weave( aWeaver ) + c.weave(aWeaver) The ``Chunk`` class contains the overall definitions for all of the various specialized subclasses. In particular, it contains the ``append()``, @@ -3141,7 +3504,7 @@ The ``Chunk`` constructor initializes the following instance variables: A weakref to the web which contains this Chunk. We want to inherit information from the ``Web`` overall. -:fileName: +:filePath: The file which contained this chunk's initial ``@o`` or ``@d``. :name: @@ -3158,71 +3521,82 @@ The ``Chunk`` constructor initializes the following instance variables: is the list of Chunks this chunk references. -.. _`52`: -.. rubric:: Chunk class (52) = +.. _`53`: +.. rubric:: Chunk base class for anonymous chunks of the file (53) = .. parsed-literal:: :class: code class Chunk: """Anonymous piece of input file: will be output through the weaver only.""" - # construction and insertion into the web - def \_\_init\_\_( self ): - self.commands= [ ] # The list of children of this chunk - self.user\_id\_list= None - self.initial= None - self.name= '' - self.fullName= None - self.seq= None - self.fileName= '' - self.referencedBy= [] # Chunks which reference this chunk. Ideally just one. - self.references= [] # Names that this chunk references - - def \_\_str\_\_( self ): - return "\\n".join( map( str, self.commands ) ) - def \_\_repr\_\_( self ): - return "{!s}('{!s}')".format( self.\_\_class\_\_.\_\_name\_\_, self.name ) - |srarr|\ Chunk append a command (`53`_) - |srarr|\ Chunk append text (`54`_) - |srarr|\ Chunk add to the web (`55`_) + web: weakref.ReferenceType["Web"] + previous\_command: "Command" + initial: bool + filePath: Path + + def \_\_init\_\_(self) -> None: + self.logger = logging.getLogger(self.\_\_class\_\_.\_\_qualname\_\_) + self.commands: list["Command"] = [] # The list of children of this chunk + self.user\_id\_list: list[str] = [] + self.name: str = "" + self.fullName: str = "" + self.seq: int = 0 + self.referencedBy: list[Chunk] = [] # Chunks which reference this chunk. Ideally just one. + self.references\_list: list[str] = [] # Names that this chunk references + self.refCount = 0 + + def \_\_str\_\_(self) -> str: + return "\\n".join(map(str, self.commands)) + def \_\_repr\_\_(self) -> str: + return f"{self.\_\_class\_\_.\_\_name\_\_!s}({self.name!r})" + def \_\_eq\_\_(self, other: Any) -> bool: + match other: + case Chunk(): + return self.name == other.name and self.commands == other.commands + case \_: + return NotImplemented + + |srarr|\ Chunk append a command (`54`_) + |srarr|\ Chunk append text (`55`_) + |srarr|\ Chunk add to the web (`56`_) - |srarr|\ Chunk generate references from this Chunk (`58`_) - |srarr|\ Chunk superclass make Content definition (`56`_) - |srarr|\ Chunk examination: starts with, matches pattern (`57`_) - |srarr|\ Chunk references to this Chunk (`59`_) + |srarr|\ Chunk generate references from this Chunk (`60`_) + |srarr|\ Chunk superclass make Content definition (`57`_) + |srarr|\ Chunk examination: starts with, matches pattern (`59`_) + |srarr|\ Chunk references to this Chunk (`61`_) - |srarr|\ Chunk weave this Chunk into the documentation (`60`_) - |srarr|\ Chunk tangle this Chunk into a code file (`61`_) - |srarr|\ Chunk indent adjustments (`62`_) + |srarr|\ Chunk weave this Chunk into the documentation (`62`_) + |srarr|\ Chunk tangle this Chunk into a code file (`63`_) + |srarr|\ Chunk indent adjustments (`64`_) .. .. class:: small - |loz| *Chunk class (52)*. Used by: Chunk class hierarchy... (`51`_) + |loz| *Chunk base class for anonymous chunks of the file (53)*. Used by: Chunk class hierarchy... (`52`_) The ``append()`` method simply appends a ``Command`` instance to this chunk. -.. _`53`: -.. rubric:: Chunk append a command (53) = +.. _`54`: +.. rubric:: Chunk append a command (54) = .. parsed-literal:: :class: code - def append( self, command ): + def append(self, command: Command) -> None: """Add another Command to this chunk.""" - self.commands.append( command ) - command.chunk= self + self.commands.append(command) + command.chunk = self .. .. class:: small - |loz| *Chunk append a command (53)*. Used by: Chunk class (`52`_) + |loz| *Chunk append a command (54)*. Used by: Chunk base class... (`53`_) The ``appendText()`` method appends a ``TextCommand`` to this chunk, @@ -3236,30 +3610,26 @@ The reason for appending is that a ``TextCommand`` has an implicit indentation. be a separate ``TextCommand`` because it will wind up indented. -.. _`54`: -.. rubric:: Chunk append text (54) = +.. _`55`: +.. rubric:: Chunk append text (55) = .. parsed-literal:: :class: code - def appendText( self, text, lineNumber=0 ): - """Append a single character to the most recent TextCommand.""" - try: - # Works for TextCommand, otherwise breaks - self.commands[-1].text += text - except IndexError as e: - # First command? Then the list will have been empty. - self.commands.append( self.makeContent(text,lineNumber) ) - except AttributeError as e: - # Not a TextCommand? Then there won't be a text attribute. - self.commands.append( self.makeContent(text,lineNumber) ) + def appendText(self, text: str, lineNumber: int = 0) -> None: + """Append a string to the most recent TextCommand.""" + match self.commands: + case [\*Command, TextCommand()]: + self.commands[-1].text += text + case \_: + self.commands.append(self.makeContent(text, lineNumber)) .. .. class:: small - |loz| *Chunk append text (54)*. Used by: Chunk class (`52`_) + |loz| *Chunk append text (55)*. Used by: Chunk base class... (`53`_) The ``webAdd()`` method adds this chunk to the given document web. @@ -3269,22 +3639,22 @@ Each subclass of the ``Chunk`` class must override this to be sure that the vari of the ``Web`` class to append an anonymous, unindexed chunk. -.. _`55`: -.. rubric:: Chunk add to the web (55) = +.. _`56`: +.. rubric:: Chunk add to the web (56) = .. parsed-literal:: :class: code - def webAdd( self, web ): + def webAdd(self, web: "Web") -> None: """Add self to a Web as anonymous chunk.""" - web.add( self ) + web.add(self) .. .. class:: small - |loz| *Chunk add to the web (55)*. Used by: Chunk class (`52`_) + |loz| *Chunk add to the web (56)*. Used by: Chunk base class... (`53`_) This superclass creates a specific Command for a given piece of content. @@ -3295,21 +3665,21 @@ A Named Chunk using ``@[`` and ``@]`` creates text. -.. _`56`: -.. rubric:: Chunk superclass make Content definition (56) = +.. _`57`: +.. rubric:: Chunk superclass make Content definition (57) = .. parsed-literal:: :class: code - def makeContent( self, text, lineNumber=0 ): - return TextCommand( text, lineNumber ) + def makeContent(self, text: str, lineNumber: int = 0) -> Command: + return TextCommand(text, lineNumber) .. .. class:: small - |loz| *Chunk superclass make Content definition (56)*. Used by: Chunk class (`52`_) + |loz| *Chunk superclass make Content definition (57)*. Used by: Chunk base class... (`53`_) The ``startsWith()`` method examines a the first ``Command`` instance this @@ -3331,29 +3701,49 @@ with the given regular expression. If so, this can be reported to the Web insta and accumulated as part of a cross reference for this ``Chunk``. -.. _`57`: -.. rubric:: Chunk examination: starts with, matches pattern (57) = +.. _`58`: +.. rubric:: Imports (58) += .. parsed-literal:: :class: code - - def startswith( self, prefix ): - """Examine the first command's starting text.""" - return len(self.commands) >= 1 and self.commands[0].startswith( prefix ) - - def searchForRE( self, rePat ): - """Visit each command, applying the pattern.""" - for c in self.commands: - if c.searchForRE( rePat ): - return self - return None - + from typing import Pattern, Match, Optional, Any, Literal + +.. + + .. class:: small + + |loz| *Imports (58)*. Used by: pyweb.py (`155`_) + + + +.. _`59`: +.. rubric:: Chunk examination: starts with, matches pattern (59) = +.. parsed-literal:: + :class: code + + + def startswith(self, prefix: str) -> bool: + """Examine the first command's starting text.""" + return len(self.commands) >= 1 and self.commands[0].startswith(prefix) + + def searchForRE(self, rePat: Pattern[str]) -> Optional["Chunk"]: + """Visit each command, applying the pattern.""" + for c in self.commands: + if c.searchForRE(rePat): + return self + return None + @property - def lineNumber( self ): + def lineNumber(self) -> int \| None: """Return the first command's line number or None.""" return self.commands[0].lineNumber if len(self.commands) >= 1 else None - def getUserIDRefs( self ): + def setUserIDRefs(self, text: str) -> None: + """Used by NamedChunk subclass.""" + pass + + def getUserIDRefs(self) -> list[str]: + """Used by NamedChunk subclass.""" return [] @@ -3361,7 +3751,7 @@ and accumulated as part of a cross reference for this ``Chunk``. .. class:: small - |loz| *Chunk examination: starts with, matches pattern (57)*. Used by: Chunk class (`52`_) + |loz| *Chunk examination: starts with, matches pattern (59)*. Used by: Chunk base class... (`53`_) The chunk search in the ``searchForRE()`` method parallels weaving and tangling a ``Chunk``. @@ -3377,17 +3767,17 @@ context information. -.. _`58`: -.. rubric:: Chunk generate references from this Chunk (58) = +.. _`60`: +.. rubric:: Chunk generate references from this Chunk (60) = .. parsed-literal:: :class: code - def genReferences( self, aWeb ): + def genReferences(self, aWeb: "Web") -> Iterator[str]: """Generate references from this Chunk.""" try: for t in self.commands: - ref= t.ref( aWeb ) + ref = t.ref(aWeb) if ref is not None: yield ref except Error as e: @@ -3398,7 +3788,7 @@ context information. .. class:: small - |loz| *Chunk generate references from this Chunk (58)*. Used by: Chunk class (`52`_) + |loz| *Chunk generate references from this Chunk (60)*. Used by: Chunk base class... (`53`_) The list of references to a Chunk uses a **Strategy** plug-in @@ -3409,22 +3799,24 @@ configuration item. This is a **Strategy** showing how to compute the list of re The Weaver pushed it into the Web so that it is available for each ``Chunk``. -.. _`59`: -.. rubric:: Chunk references to this Chunk (59) = +.. _`61`: +.. rubric:: Chunk references to this Chunk (61) = .. parsed-literal:: :class: code - def references\_list( self, theWeaver ): + def references(self, theWeaver: "Weaver") -> list[tuple[str, int]]: """Extract name, sequence from Chunks into a list.""" - return [ (c.name, c.seq) - for c in theWeaver.reference\_style.chunkReferencedBy( self ) ] + return [ + (c.name, c.seq) + for c in theWeaver.reference\_style.chunkReferencedBy(self) + ] .. .. class:: small - |loz| *Chunk references to this Chunk (59)*. Used by: Chunk class (`52`_) + |loz| *Chunk references to this Chunk (61)*. Used by: Chunk base class... (`53`_) The ``weave()`` method weaves this chunk into the final document as follows: @@ -3443,22 +3835,22 @@ context information. -.. _`60`: -.. rubric:: Chunk weave this Chunk into the documentation (60) = +.. _`62`: +.. rubric:: Chunk weave this Chunk into the documentation (62) = .. parsed-literal:: :class: code - def weave( self, aWeb, aWeaver ): + def weave(self, aWeb: "Web", aWeaver: "Weaver") -> None: """Create the nicely formatted document from an anonymous chunk.""" - aWeaver.docBegin( self ) + aWeaver.docBegin(self) for cmd in self.commands: - cmd.weave( aWeb, aWeaver ) - aWeaver.docEnd( self ) - def weaveReferenceTo( self, aWeb, aWeaver ): + cmd.weave(aWeb, aWeaver) + aWeaver.docEnd(self) + def weaveReferenceTo(self, aWeb: "Web", aWeaver: "Weaver") -> None: """Create a reference to this chunk -- except for anonymous chunks.""" raise Exception( "Cannot reference an anonymous chunk.""") - def weaveShortReferenceTo( self, aWeb, aWeaver ): + def weaveShortReferenceTo(self, aWeb: "Web", aWeaver: "Weaver") -> None: """Create a short reference to this chunk -- except for anonymous chunks.""" raise Exception( "Cannot reference an anonymous chunk.""") @@ -3467,29 +3859,29 @@ context information. .. class:: small - |loz| *Chunk weave this Chunk into the documentation (60)*. Used by: Chunk class (`52`_) + |loz| *Chunk weave this Chunk into the documentation (62)*. Used by: Chunk base class... (`53`_) Anonymous chunks cannot be tangled. Any attempt indicates a serious problem with this program or the input file. -.. _`61`: -.. rubric:: Chunk tangle this Chunk into a code file (61) = +.. _`63`: +.. rubric:: Chunk tangle this Chunk into a code file (63) = .. parsed-literal:: :class: code - def tangle( self, aWeb, aTangler ): + def tangle(self, aWeb: "Web", aTangler: "Tangler") -> None: """Create source code -- except anonymous chunks should not be tangled""" - raise Error( 'Cannot tangle an anonymous chunk', self ) + raise Error('Cannot tangle an anonymous chunk', self) .. .. class:: small - |loz| *Chunk tangle this Chunk into a code file (61)*. Used by: Chunk class (`52`_) + |loz| *Chunk tangle this Chunk into a code file (63)*. Used by: Chunk base class... (`53`_) Generally, a Chunk with a reference will adjust the indentation for @@ -3498,23 +3890,23 @@ a subclass may not indent when tangling and may -- instead -- put stuff flush at left margin by forcing the local indent to zero. -.. _`62`: -.. rubric:: Chunk indent adjustments (62) = +.. _`64`: +.. rubric:: Chunk indent adjustments (64) = .. parsed-literal:: :class: code - def reference\_indent( self, aWeb, aTangler, amount ): - aTangler.addIndent( amount ) # Or possibly set indent to local zero. + def reference\_indent(self, aWeb: "Web", aTangler: "Tangler", amount: int) -> None: + aTangler.addIndent(amount) # Or possibly set indent to local zero. - def reference\_dedent( self, aWeb, aTangler ): + def reference\_dedent(self, aWeb: "Web", aTangler: "Tangler") -> None: aTangler.clrIndent() .. .. class:: small - |loz| *Chunk indent adjustments (62)*. Used by: Chunk class (`52`_) + |loz| *Chunk indent adjustments (64)*. Used by: Chunk base class... (`53`_) NamedChunk class @@ -3567,34 +3959,37 @@ This class introduces some additional attributes. -.. _`63`: -.. rubric:: NamedChunk class (63) = +.. _`65`: +.. rubric:: NamedChunk class for defined names (65) = .. parsed-literal:: :class: code - class NamedChunk( Chunk ): + class NamedChunk(Chunk): """Named piece of input file: will be output as both tangler and weaver.""" - def \_\_init\_\_( self, name ): + def \_\_init\_\_(self, name: str) -> None: super().\_\_init\_\_() - self.name= name - self.user\_id\_list= [] - self.refCount= 0 - def \_\_str\_\_( self ): - return "{!r}: {!s}".format( self.name, Chunk.\_\_str\_\_(self) ) - def makeContent( self, text, lineNumber=0 ): - return CodeCommand( text, lineNumber ) - |srarr|\ NamedChunk user identifiers set and get (`64`_) - |srarr|\ NamedChunk add to the web (`65`_) - |srarr|\ NamedChunk weave into the documentation (`66`_) - |srarr|\ NamedChunk tangle into the source file (`67`_) + self.name = name + self.user\_id\_list = [] + self.refCount = 0 + + def \_\_str\_\_(self) -> str: + return f"{self.name!r}: {self!s}" + + def makeContent(self, text: str, lineNumber: int = 0) -> Command: + return CodeCommand(text, lineNumber) + + |srarr|\ NamedChunk user identifiers set and get (`66`_) + |srarr|\ NamedChunk add to the web (`67`_) + |srarr|\ NamedChunk weave into the documentation (`68`_) + |srarr|\ NamedChunk tangle into the source file (`69`_) .. .. class:: small - |loz| *NamedChunk class (63)*. Used by: Chunk class hierarchy... (`51`_) + |loz| *NamedChunk class for defined names (65)*. Used by: Chunk class hierarchy... (`52`_) The ``setUserIDRefs()`` method accepts a list of user identifiers that are @@ -3602,16 +3997,16 @@ associated with this chunk. These are provided after the ``@|`` separator in a ``@d`` named chunk. These are used by the ``@u`` cross reference generator. -.. _`64`: -.. rubric:: NamedChunk user identifiers set and get (64) = +.. _`66`: +.. rubric:: NamedChunk user identifiers set and get (66) = .. parsed-literal:: :class: code - def setUserIDRefs( self, text ): + def setUserIDRefs(self, text: str) -> None: """Save user ID's associated with this chunk.""" - self.user\_id\_list= text.split() - def getUserIDRefs( self ): + self.user\_id\_list = text.split() + def getUserIDRefs(self) -> list[str]: return self.user\_id\_list @@ -3619,7 +4014,7 @@ in a ``@d`` named chunk. These are used by the ``@u`` cross reference generator .. class:: small - |loz| *NamedChunk user identifiers set and get (64)*. Used by: NamedChunk class (`63`_) + |loz| *NamedChunk user identifiers set and get (66)*. Used by: NamedChunk class... (`65`_) The ``webAdd()`` method adds this chunk to the given document ``Web`` instance. @@ -3628,22 +4023,22 @@ Each class of ``Chunk`` must override this to be sure that the various of the ``Web`` class to append a named chunk. -.. _`65`: -.. rubric:: NamedChunk add to the web (65) = +.. _`67`: +.. rubric:: NamedChunk add to the web (67) = .. parsed-literal:: :class: code - def webAdd( self, web ): + def webAdd(self, web: "Web") -> None: """Add self to a Web as named chunk, update xrefs.""" - web.addNamed( self ) + web.addNamed(self) .. .. class:: small - |loz| *NamedChunk add to the web (65)*. Used by: NamedChunk class (`63`_) + |loz| *NamedChunk add to the web (67)*. Used by: NamedChunk class... (`65`_) The ``weave()`` method weaves this chunk into the final document as follows: @@ -3670,37 +4065,37 @@ The woven references simply follow whatever preceded them on the line; the inden -.. _`66`: -.. rubric:: NamedChunk weave into the documentation (66) = +.. _`68`: +.. rubric:: NamedChunk weave into the documentation (68) = .. parsed-literal:: :class: code - def weave( self, aWeb, aWeaver ): + def weave(self, aWeb: "Web", aWeaver: "Weaver") -> None: """Create the nicely formatted document from a chunk of code.""" - self.fullName= aWeb.fullNameFor( self.name ) + self.fullName = aWeb.fullNameFor(self.name) aWeaver.addIndent() - aWeaver.codeBegin( self ) + aWeaver.codeBegin(self) for cmd in self.commands: - cmd.weave( aWeb, aWeaver ) + cmd.weave(aWeb, aWeaver) aWeaver.clrIndent( ) - aWeaver.codeEnd( self ) - def weaveReferenceTo( self, aWeb, aWeaver ): + aWeaver.codeEnd(self) + def weaveReferenceTo(self, aWeb: "Web", aWeaver: "Weaver") -> None: """Create a reference to this chunk.""" - self.fullName= aWeb.fullNameFor( self.name ) - txt= aWeaver.referenceTo( self.fullName, self.seq ) - aWeaver.codeBlock( txt ) - def weaveShortReferenceTo( self, aWeb, aWeaver ): + self.fullName = aWeb.fullNameFor(self.name) + txt = aWeaver.referenceTo(self.fullName, self.seq) + aWeaver.codeBlock(txt) + def weaveShortReferenceTo(self, aWeb: "Web", aWeaver: "Weaver") -> None: """Create a shortened reference to this chunk.""" - txt= aWeaver.referenceTo( None, self.seq ) - aWeaver.codeBlock( txt ) + txt = aWeaver.referenceTo(None, self.seq) + aWeaver.codeBlock(txt) .. .. class:: small - |loz| *NamedChunk weave into the documentation (66)*. Used by: NamedChunk class (`63`_) + |loz| *NamedChunk weave into the documentation (68)*. Used by: NamedChunk class... (`65`_) The ``tangle()`` method tangles this chunk into the final document as follows: @@ -3717,58 +4112,58 @@ context information. -.. _`67`: -.. rubric:: NamedChunk tangle into the source file (67) = +.. _`69`: +.. rubric:: NamedChunk tangle into the source file (69) = .. parsed-literal:: :class: code - def tangle( self, aWeb, aTangler ): + def tangle(self, aWeb: "Web", aTangler: "Tangler") -> None: """Create source code. Use aWeb to resolve @. Format as correctly indented source text """ - self.previous\_command= TextCommand( "", self.commands[0].lineNumber ) - aTangler.codeBegin( self ) + self.previous\_command = TextCommand("", self.commands[0].lineNumber) + aTangler.codeBegin(self) for t in self.commands: try: - t.tangle( aWeb, aTangler ) + t.tangle(aWeb, aTangler) except Error as e: raise - self.previous\_command= t - aTangler.codeEnd( self ) + self.previous\_command = t + aTangler.codeEnd(self) .. .. class:: small - |loz| *NamedChunk tangle into the source file (67)*. Used by: NamedChunk class (`63`_) + |loz| *NamedChunk tangle into the source file (69)*. Used by: NamedChunk class... (`65`_) There's a second variation on NamedChunk, one that doesn't indent based on context. It simply sets an indent at the left margin. -.. _`68`: -.. rubric:: NamedChunk class (68) += +.. _`70`: +.. rubric:: NamedChunk class for defined names (70) += .. parsed-literal:: :class: code - class NamedChunk\_Noindent( NamedChunk ): + class NamedChunk\_Noindent(NamedChunk): """Named piece of input file: will be output as both tangler and weaver.""" - def reference\_indent( self, aWeb, aTangler, amount ): - aTangler.setIndent( 0 ) + def reference\_indent(self, aWeb: "Web", aTangler: "Tangler", amount: int) -> None: + aTangler.setIndent(0) - def reference\_dedent( self, aWeb, aTangler ): + def reference\_dedent(self, aWeb: "Web", aTangler: "Tangler") -> None: aTangler.clrIndent() .. .. class:: small - |loz| *NamedChunk class (68)*. Used by: Chunk class hierarchy... (`51`_) + |loz| *NamedChunk class for defined names (70)*. Used by: Chunk class hierarchy... (`52`_) OutputChunk class @@ -3790,28 +4185,28 @@ use different ``Weaver`` methods for different kinds of text. All other methods, including the tangle method are identical to ``NamedChunk``. -.. _`69`: -.. rubric:: OutputChunk class (69) = +.. _`71`: +.. rubric:: OutputChunk class (71) = .. parsed-literal:: :class: code - class OutputChunk( NamedChunk ): + class OutputChunk(NamedChunk): """Named piece of input file, defines an output tangle.""" - def \_\_init\_\_( self, name, comment\_start=None, comment\_end="" ): - super().\_\_init\_\_( name ) - self.comment\_start= comment\_start - self.comment\_end= comment\_end - |srarr|\ OutputChunk add to the web (`70`_) - |srarr|\ OutputChunk weave (`71`_) - |srarr|\ OutputChunk tangle (`72`_) + def \_\_init\_\_(self, name: str, comment\_start: str = "", comment\_end: str = "") -> None: + super().\_\_init\_\_(name) + self.comment\_start = comment\_start + self.comment\_end = comment\_end + |srarr|\ OutputChunk add to the web (`72`_) + |srarr|\ OutputChunk weave (`73`_) + |srarr|\ OutputChunk tangle (`74`_) .. .. class:: small - |loz| *OutputChunk class (69)*. Used by: Chunk class hierarchy... (`51`_) + |loz| *OutputChunk class (71)*. Used by: Chunk class hierarchy... (`52`_) The ``webAdd()`` method adds this chunk to the given document ``Web``. @@ -3820,22 +4215,22 @@ Each class of ``Chunk`` must override this to be sure that the various of the ``Web`` class to append a file output chunk. -.. _`70`: -.. rubric:: OutputChunk add to the web (70) = +.. _`72`: +.. rubric:: OutputChunk add to the web (72) = .. parsed-literal:: :class: code - def webAdd( self, web ): + def webAdd(self, web: "Web") -> None: """Add self to a Web as output chunk, update xrefs.""" - web.addOutput( self ) + web.addOutput(self) .. .. class:: small - |loz| *OutputChunk add to the web (70)*. Used by: OutputChunk class (`69`_) + |loz| *OutputChunk add to the web (72)*. Used by: OutputChunk class... (`71`_) The ``weave()`` method weaves this chunk into the final document as follows: @@ -3855,48 +4250,48 @@ context information. -.. _`71`: -.. rubric:: OutputChunk weave (71) = +.. _`73`: +.. rubric:: OutputChunk weave (73) = .. parsed-literal:: :class: code - def weave( self, aWeb, aWeaver ): + def weave(self, aWeb: "Web", aWeaver: "Weaver") -> None: """Create the nicely formatted document from a chunk of code.""" - self.fullName= aWeb.fullNameFor( self.name ) - aWeaver.fileBegin( self ) + self.fullName = aWeb.fullNameFor(self.name) + aWeaver.fileBegin(self) for cmd in self.commands: - cmd.weave( aWeb, aWeaver ) - aWeaver.fileEnd( self ) + cmd.weave(aWeb, aWeaver) + aWeaver.fileEnd(self) .. .. class:: small - |loz| *OutputChunk weave (71)*. Used by: OutputChunk class (`69`_) + |loz| *OutputChunk weave (73)*. Used by: OutputChunk class... (`71`_) When we tangle, we provide the output Chunk's comment information to the Tangler to be sure that -- if line numbers were requested -- they can be included properly. -.. _`72`: -.. rubric:: OutputChunk tangle (72) = +.. _`74`: +.. rubric:: OutputChunk tangle (74) = .. parsed-literal:: :class: code - def tangle( self, aWeb, aTangler ): - aTangler.comment\_start= self.comment\_start - aTangler.comment\_end= self.comment\_end - super().tangle( aWeb, aTangler ) + def tangle(self, aWeb: "Web", aTangler: "Tangler") -> None: + aTangler.comment\_start = self.comment\_start + aTangler.comment\_end = self.comment\_end + super().tangle(aWeb, aTangler) .. .. class:: small - |loz| *OutputChunk tangle (72)*. Used by: OutputChunk class (`69`_) + |loz| *OutputChunk tangle (74)*. Used by: OutputChunk class... (`71`_) NamedDocumentChunk class @@ -3920,25 +4315,27 @@ All other methods, including the tangle method are identical to ``NamedChunk``. -.. _`73`: -.. rubric:: NamedDocumentChunk class (73) = +.. _`75`: +.. rubric:: NamedDocumentChunk class (75) = .. parsed-literal:: :class: code - class NamedDocumentChunk( NamedChunk ): + class NamedDocumentChunk(NamedChunk): """Named piece of input file with document source, defines an output tangle.""" - def makeContent( self, text, lineNumber=0 ): - return TextCommand( text, lineNumber ) - |srarr|\ NamedDocumentChunk weave (`74`_) - |srarr|\ NamedDocumentChunk tangle (`75`_) + + def makeContent(self, text: str, lineNumber: int = 0) -> Command: + return TextCommand(text, lineNumber) + + |srarr|\ NamedDocumentChunk weave (`76`_) + |srarr|\ NamedDocumentChunk tangle (`77`_) .. .. class:: small - |loz| *NamedDocumentChunk class (73)*. Used by: Chunk class hierarchy... (`51`_) + |loz| *NamedDocumentChunk class (75)*. Used by: Chunk class hierarchy... (`52`_) The ``weave()`` method quietly ignores this chunk in the document. @@ -3954,48 +4351,48 @@ to insert the entire chunk. -.. _`74`: -.. rubric:: NamedDocumentChunk weave (74) = +.. _`76`: +.. rubric:: NamedDocumentChunk weave (76) = .. parsed-literal:: :class: code - def weave( self, aWeb, aWeaver ): + def weave(self, aWeb: "Web", aWeaver: "Weaver") -> None: """Ignore this when producing the document.""" pass - def weaveReferenceTo( self, aWeb, aWeaver ): + def weaveReferenceTo(self, aWeb: "Web", aWeaver: "Weaver") -> None: """On a reference to this chunk, expand the body in place.""" for cmd in self.commands: - cmd.weave( aWeb, aWeaver ) - def weaveShortReferenceTo( self, aWeb, aWeaver ): + cmd.weave(aWeb, aWeaver) + def weaveShortReferenceTo(self, aWeb: "Web", aWeaver: "Weaver") -> None: """On a reference to this chunk, expand the body in place.""" - self.weaveReferenceTo( aWeb, aWeaver ) + self.weaveReferenceTo(aWeb, aWeaver) .. .. class:: small - |loz| *NamedDocumentChunk weave (74)*. Used by: NamedDocumentChunk class (`73`_) + |loz| *NamedDocumentChunk weave (76)*. Used by: NamedDocumentChunk class... (`75`_) -.. _`75`: -.. rubric:: NamedDocumentChunk tangle (75) = +.. _`77`: +.. rubric:: NamedDocumentChunk tangle (77) = .. parsed-literal:: :class: code - def tangle( self, aWeb, aTangler ): + def tangle(self, aWeb: "Web", aTangler: "Tangler") -> None: """Raise an exception on an attempt to tangle.""" - raise Error( "Cannot tangle a chunk defined with @[.""" ) + raise Error("Cannot tangle a chunk defined with @[.""") .. .. class:: small - |loz| *NamedDocumentChunk tangle (75)*. Used by: NamedDocumentChunk class (`73`_) + |loz| *NamedDocumentChunk tangle (77)*. Used by: NamedDocumentChunk class... (`75`_) Commands @@ -4017,26 +4414,33 @@ cross reference information and tangle a file or weave the final document. -.. _`76`: -.. rubric:: Command class hierarchy - used to describe individual commands (76) = +.. _`78`: +.. rubric:: Command class hierarchy - used to describe individual commands (78) = .. parsed-literal:: :class: code - |srarr|\ Command superclass (`77`_) - |srarr|\ TextCommand class to contain a document text block (`80`_) - |srarr|\ CodeCommand class to contain a program source code block (`81`_) - |srarr|\ XrefCommand superclass for all cross-reference commands (`82`_) - |srarr|\ FileXrefCommand class for an output file cross-reference (`83`_) - |srarr|\ MacroXrefCommand class for a named chunk cross-reference (`84`_) - |srarr|\ UserIdXrefCommand class for a user identifier cross-reference (`85`_) - |srarr|\ ReferenceCommand class for chunk references (`86`_) + |srarr|\ Command superclass (`79`_) + + |srarr|\ TextCommand class to contain a document text block (`82`_) + + |srarr|\ CodeCommand class to contain a program source code block (`83`_) + + |srarr|\ XrefCommand superclass for all cross-reference commands (`84`_) + + |srarr|\ FileXrefCommand class for an output file cross-reference (`85`_) + + |srarr|\ MacroXrefCommand class for a named chunk cross-reference (`86`_) + + |srarr|\ UserIdXrefCommand class for a user identifier cross-reference (`87`_) + + |srarr|\ ReferenceCommand class for chunk references (`88`_) .. .. class:: small - |loz| *Command class hierarchy - used to describe individual commands (76)*. Used by: Base Class Definitions (`1`_) + |loz| *Command class hierarchy - used to describe individual commands (78)*. Used by: Base Class Definitions (`1`_) Command Superclass @@ -4051,7 +4455,7 @@ of the methods provided in this superclass. .. parsed-literal:: - class MyNewCommand( Command ): + class MyNewCommand(Command): *... overrides for various methods ...* Additionally, a subclass of ``WebReader`` must be defined to parse the new command @@ -4100,73 +4504,87 @@ The attributes of a ``Command`` instance includes the line number on which the command began, in ``lineNumber``. -.. _`77`: -.. rubric:: Command superclass (77) = +.. _`79`: +.. rubric:: Command superclass (79) = .. parsed-literal:: :class: code - class Command: + class Command(abc.ABC): """A Command is the lowest level of granularity in the input stream.""" - def \_\_init\_\_( self, fromLine=0 ): - self.lineNumber= fromLine+1 # tokenizer is zero-based - self.chunk= None - self.logger= logging.getLogger( self.\_\_class\_\_.\_\_qualname\_\_ ) - def \_\_str\_\_( self ): - return "at {!r}".format(self.lineNumber) - |srarr|\ Command analysis features: starts-with and Regular Expression search (`78`_) - |srarr|\ Command tangle and weave functions (`79`_) + chunk : "Chunk" + text : str + def \_\_init\_\_(self, fromLine: int = 0) -> None: + self.lineNumber = fromLine+1 # tokenizer is zero-based + self.logger = logging.getLogger(self.\_\_class\_\_.\_\_qualname\_\_) + + def \_\_str\_\_(self) -> str: + return f"at {self.lineNumber!r}" + + def \_\_eq\_\_(self, other: Any) -> bool: + match other: + case Command(): + return self.lineNumber == other.lineNumber and self.text == other.text + case \_: + return NotImplemented + + |srarr|\ Command analysis features: starts-with and Regular Expression search (`80`_) + |srarr|\ Command tangle and weave functions (`81`_) .. .. class:: small - |loz| *Command superclass (77)*. Used by: Command class hierarchy... (`76`_) + |loz| *Command superclass (79)*. Used by: Command class hierarchy... (`78`_) -.. _`78`: -.. rubric:: Command analysis features: starts-with and Regular Expression search (78) = +.. _`80`: +.. rubric:: Command analysis features: starts-with and Regular Expression search (80) = .. parsed-literal:: :class: code - def startswith( self, prefix ): - return None - def searchForRE( self, rePat ): - return None - def indent( self ): + def startswith(self, prefix: str) -> bool: + return False + def searchForRE(self, rePat: Pattern[str]) -> Match[str] \| None: return None + def indent(self) -> int: + return 0 .. .. class:: small - |loz| *Command analysis features: starts-with and Regular Expression search (78)*. Used by: Command superclass (`77`_) + |loz| *Command analysis features: starts-with and Regular Expression search (80)*. Used by: Command superclass (`79`_) -.. _`79`: -.. rubric:: Command tangle and weave functions (79) = +.. _`81`: +.. rubric:: Command tangle and weave functions (81) = .. parsed-literal:: :class: code - def ref( self, aWeb ): + def ref(self, aWeb: "Web") -> str \| None: return None - def weave( self, aWeb, aWeaver ): - pass - def tangle( self, aWeb, aTangler ): - pass + + @abc.abstractmethod + def weave(self, aWeb: "Web", aWeaver: "Weaver") -> None: + ... + + @abc.abstractmethod + def tangle(self, aWeb: "Web", aTangler: "Tangler") -> None: + ... .. .. class:: small - |loz| *Command tangle and weave functions (79)*. Used by: Command superclass (`77`_) + |loz| *Command tangle and weave functions (81)*. Used by: Command superclass (`79`_) TextCommand class @@ -4186,26 +4604,27 @@ This subclass provides a concrete implementation for all of the methods. Since text is the author's original markup language, it is emitted directly to the weaver or tangler. +**TODO:** Use textwrap.shorten to snip off first 32 chars of the text. -.. _`80`: -.. rubric:: TextCommand class to contain a document text block (80) = +.. _`82`: +.. rubric:: TextCommand class to contain a document text block (82) = .. parsed-literal:: :class: code - class TextCommand( Command ): + class TextCommand(Command): """A piece of document source text.""" - def \_\_init\_\_( self, text, fromLine=0 ): - super().\_\_init\_\_( fromLine ) - self.text= text - def \_\_str\_\_( self ): - return "at {!r}: {!r}...".format(self.lineNumber,self.text[:32]) - def startswith( self, prefix ): - return self.text.startswith( prefix ) - def searchForRE( self, rePat ): - return rePat.search( self.text ) - def indent( self ): + def \_\_init\_\_(self, text: str, fromLine: int = 0) -> None: + super().\_\_init\_\_(fromLine) + self.text = text + def \_\_str\_\_(self) -> str: + return f"at {self.lineNumber!r}: {self.text[:32]!r}..." + def startswith(self, prefix: str) -> bool: + return self.text.startswith(prefix) + def searchForRE(self, rePat: Pattern[str]) -> Match[str] \| None: + return rePat.search(self.text) + def indent(self) -> int: if self.text.endswith('\\n'): return 0 try: @@ -4213,17 +4632,17 @@ or tangler. return len(last\_line) except IndexError: return 0 - def weave( self, aWeb, aWeaver ): - aWeaver.write( self.text ) - def tangle( self, aWeb, aTangler ): - aTangler.write( self.text ) + def weave(self, aWeb: "Web", aWeaver: "Weaver") -> None: + aWeaver.write(self.text) + def tangle(self, aWeb: "Web", aTangler: "Tangler") -> None: + aTangler.write(self.text) .. .. class:: small - |loz| *TextCommand class to contain a document text block (80)*. Used by: Command class hierarchy... (`76`_) + |loz| *TextCommand class to contain a document text block (82)*. Used by: Command class hierarchy... (`78`_) CodeCommand class @@ -4247,25 +4666,25 @@ indentation is maintained. -.. _`81`: -.. rubric:: CodeCommand class to contain a program source code block (81) = +.. _`83`: +.. rubric:: CodeCommand class to contain a program source code block (83) = .. parsed-literal:: :class: code - class CodeCommand( TextCommand ): + class CodeCommand(TextCommand): """A piece of program source code.""" - def weave( self, aWeb, aWeaver ): - aWeaver.codeBlock( aWeaver.quote( self.text ) ) - def tangle( self, aWeb, aTangler ): - aTangler.codeBlock( self.text ) + def weave(self, aWeb: "Web", aWeaver: "Weaver") -> None: + aWeaver.codeBlock(aWeaver.quote(self.text)) + def tangle(self, aWeb: "Web", aTangler: "Tangler") -> None: + aTangler.codeBlock(self.text) .. .. class:: small - |loz| *CodeCommand class to contain a program source code block (81)*. Used by: Command class hierarchy... (`76`_) + |loz| *CodeCommand class to contain a program source code block (83)*. Used by: Command class hierarchy... (`78`_) XrefCommand superclass @@ -4294,22 +4713,24 @@ is illegal. An exception is raised and processing stops. -.. _`82`: -.. rubric:: XrefCommand superclass for all cross-reference commands (82) = +.. _`84`: +.. rubric:: XrefCommand superclass for all cross-reference commands (84) = .. parsed-literal:: :class: code - class XrefCommand( Command ): + class XrefCommand(Command): """Any of the Xref-goes-here commands in the input.""" - def \_\_str\_\_( self ): - return "at {!r}: cross reference".format(self.lineNumber) - def formatXref( self, xref, aWeaver ): + def \_\_str\_\_(self) -> str: + return f"at {self.lineNumber!r}: cross reference" + + def formatXref(self, xref: dict[str, list[int]], aWeaver: "Weaver") -> None: aWeaver.xrefHead() for n in sorted(xref): - aWeaver.xrefLine( n, xref[n] ) + aWeaver.xrefLine(n, xref[n]) aWeaver.xrefFoot() - def tangle( self, aWeb, aTangler ): + + def tangle(self, aWeb: "Web", aTangler: "Tangler") -> None: raise Error('Illegal tangling of a cross reference command.') @@ -4317,7 +4738,7 @@ is illegal. An exception is raised and processing stops. .. class:: small - |loz| *XrefCommand superclass for all cross-reference commands (82)*. Used by: Command class hierarchy... (`76`_) + |loz| *XrefCommand superclass for all cross-reference commands (84)*. Used by: Command class hierarchy... (`78`_) FileXrefCommand class @@ -4333,24 +4754,24 @@ the ``formatXref()`` method of the ``XrefCommand`` superclass for format this r -.. _`83`: -.. rubric:: FileXrefCommand class for an output file cross-reference (83) = +.. _`85`: +.. rubric:: FileXrefCommand class for an output file cross-reference (85) = .. parsed-literal:: :class: code - class FileXrefCommand( XrefCommand ): + class FileXrefCommand(XrefCommand): """A FileXref command.""" - def weave( self, aWeb, aWeaver ): + def weave(self, aWeb: "Web", aWeaver: "Weaver") -> None: """Weave a File Xref from @o commands.""" - self.formatXref( aWeb.fileXref(), aWeaver ) + self.formatXref(aWeb.fileXref(), aWeaver) .. .. class:: small - |loz| *FileXrefCommand class for an output file cross-reference (83)*. Used by: Command class hierarchy... (`76`_) + |loz| *FileXrefCommand class for an output file cross-reference (85)*. Used by: Command class hierarchy... (`78`_) MacroXrefCommand class @@ -4366,24 +4787,24 @@ the ``formatXref()`` method of the ``XrefCommand`` superclass method for format -.. _`84`: -.. rubric:: MacroXrefCommand class for a named chunk cross-reference (84) = +.. _`86`: +.. rubric:: MacroXrefCommand class for a named chunk cross-reference (86) = .. parsed-literal:: :class: code - class MacroXrefCommand( XrefCommand ): + class MacroXrefCommand(XrefCommand): """A MacroXref command.""" - def weave( self, aWeb, aWeaver ): + def weave(self, aWeb: "Web", aWeaver: "Weaver") -> None: """Weave the Macro Xref from @d commands.""" - self.formatXref( aWeb.chunkXref(), aWeaver ) + self.formatXref(aWeb.chunkXref(), aWeaver) .. .. class:: small - |loz| *MacroXrefCommand class for a named chunk cross-reference (84)*. Used by: Command class hierarchy... (`76`_) + |loz| *MacroXrefCommand class for a named chunk cross-reference (86)*. Used by: Command class hierarchy... (`78`_) UserIdXrefCommand class @@ -4408,22 +4829,22 @@ algorithm, which is similar to the algorithm in the ``XrefCommand`` superclass. -.. _`85`: -.. rubric:: UserIdXrefCommand class for a user identifier cross-reference (85) = +.. _`87`: +.. rubric:: UserIdXrefCommand class for a user identifier cross-reference (87) = .. parsed-literal:: :class: code - class UserIdXrefCommand( XrefCommand ): + class UserIdXrefCommand(XrefCommand): """A UserIdXref command.""" - def weave( self, aWeb, aWeaver ): + def weave(self, aWeb: "Web", aWeaver: "Weaver") -> None: """Weave a user identifier Xref from @d commands.""" - ux= aWeb.userNamesXref() + ux = aWeb.userNamesXref() if len(ux) != 0: aWeaver.xrefHead() for u in sorted(ux): - defn, refList= ux[u] - aWeaver.xrefDefLine( u, defn, refList ) + defn, refList = ux[u] + aWeaver.xrefDefLine(u, defn, refList) aWeaver.xrefFoot() else: aWeaver.xrefEmpty() @@ -4433,7 +4854,7 @@ algorithm, which is similar to the algorithm in the ``XrefCommand`` superclass. .. class:: small - |loz| *UserIdXrefCommand class for a user identifier cross-reference (85)*. Used by: Command class hierarchy... (`76`_) + |loz| *UserIdXrefCommand class for a user identifier cross-reference (87)*. Used by: Command class hierarchy... (`78`_) ReferenceCommand class @@ -4465,33 +4886,35 @@ of a ``ReferenceCommand``. -.. _`86`: -.. rubric:: ReferenceCommand class for chunk references (86) = +.. _`88`: +.. rubric:: ReferenceCommand class for chunk references (88) = .. parsed-literal:: :class: code - class ReferenceCommand( Command ): + class ReferenceCommand(Command): """A reference to a named chunk, via @.""" - def \_\_init\_\_( self, refTo, fromLine=0 ): - super().\_\_init\_\_( fromLine ) - self.refTo= refTo - self.fullname= None - self.sequenceList= None - self.chunkList= [] - def \_\_str\_\_( self ): - return "at {!r}: reference to chunk {!r}".format(self.lineNumber,self.refTo) - |srarr|\ ReferenceCommand resolve a referenced chunk name (`87`_) - |srarr|\ ReferenceCommand refers to a chunk (`88`_) - |srarr|\ ReferenceCommand weave a reference to a chunk (`89`_) - |srarr|\ ReferenceCommand tangle a referenced chunk (`90`_) + def \_\_init\_\_(self, refTo: str, fromLine: int = 0) -> None: + super().\_\_init\_\_(fromLine) + self.refTo = refTo + self.fullname = None + self.sequenceList = None + self.chunkList: list[Chunk] = [] + + def \_\_str\_\_(self) -> str: + return "at {self.lineNumber!r}: reference to chunk {self.refTo!r}" + + |srarr|\ ReferenceCommand resolve a referenced chunk name (`89`_) + |srarr|\ ReferenceCommand refers to a chunk (`90`_) + |srarr|\ ReferenceCommand weave a reference to a chunk (`91`_) + |srarr|\ ReferenceCommand tangle a referenced chunk (`92`_) .. .. class:: small - |loz| *ReferenceCommand class for chunk references (86)*. Used by: Command class hierarchy... (`76`_) + |loz| *ReferenceCommand class for chunk references (88)*. Used by: Command class hierarchy... (`78`_) The ``resolve()`` method queries the overall ``Web`` instance for the full @@ -4501,23 +4924,23 @@ to the chunk. -.. _`87`: -.. rubric:: ReferenceCommand resolve a referenced chunk name (87) = +.. _`89`: +.. rubric:: ReferenceCommand resolve a referenced chunk name (89) = .. parsed-literal:: :class: code - def resolve( self, aWeb ): + def resolve(self, aWeb: "Web") -> None: """Expand our chunk name and list of parts""" - self.fullName= aWeb.fullNameFor( self.refTo ) - self.chunkList= aWeb.getchunk( self.refTo ) + self.fullName = aWeb.fullNameFor(self.refTo) + self.chunkList = aWeb.getchunk(self.refTo) .. .. class:: small - |loz| *ReferenceCommand resolve a referenced chunk name (87)*. Used by: ReferenceCommand class... (`86`_) + |loz| *ReferenceCommand resolve a referenced chunk name (89)*. Used by: ReferenceCommand class... (`88`_) The ``ref()`` method is a request that is delegated by a ``Chunk``; @@ -4527,15 +4950,15 @@ Chinks to which it refers. -.. _`88`: -.. rubric:: ReferenceCommand refers to a chunk (88) = +.. _`90`: +.. rubric:: ReferenceCommand refers to a chunk (90) = .. parsed-literal:: :class: code - def ref( self, aWeb ): + def ref(self, aWeb: "Web") -> str: """Find and return the full name for this reference.""" - self.resolve( aWeb ) + self.resolve(aWeb) return self.fullName @@ -4543,7 +4966,7 @@ Chinks to which it refers. .. class:: small - |loz| *ReferenceCommand refers to a chunk (88)*. Used by: ReferenceCommand class... (`86`_) + |loz| *ReferenceCommand refers to a chunk (90)*. Used by: ReferenceCommand class... (`88`_) The ``weave()`` method inserts a markup reference to a named @@ -4552,23 +4975,23 @@ this appropriately for the document type being woven. -.. _`89`: -.. rubric:: ReferenceCommand weave a reference to a chunk (89) = +.. _`91`: +.. rubric:: ReferenceCommand weave a reference to a chunk (91) = .. parsed-literal:: :class: code - def weave( self, aWeb, aWeaver ): + def weave(self, aWeb: "Web", aWeaver: "Weaver") -> None: """Create the nicely formatted reference to a chunk of code.""" - self.resolve( aWeb ) - aWeb.weaveChunk( self.fullName, aWeaver ) + self.resolve(aWeb) + aWeb.weaveChunk(self.fullName, aWeaver) .. .. class:: small - |loz| *ReferenceCommand weave a reference to a chunk (89)*. Used by: ReferenceCommand class... (`86`_) + |loz| *ReferenceCommand weave a reference to a chunk (91)*. Used by: ReferenceCommand class... (`88`_) The ``tangle()`` method inserts the resolved chunk in this @@ -4580,34 +5003,34 @@ Or where indentation is set to a local zero because the included Chunk is a no-indent Chunk. -.. _`90`: -.. rubric:: ReferenceCommand tangle a referenced chunk (90) = +.. _`92`: +.. rubric:: ReferenceCommand tangle a referenced chunk (92) = .. parsed-literal:: :class: code - def tangle( self, aWeb, aTangler ): + def tangle(self, aWeb: "Web", aTangler: "Tangler") -> None: """Create source code.""" - self.resolve( aWeb ) + self.resolve(aWeb) - self.logger.debug( "Indent {!r} + {!r}".format(aTangler.context, self.chunk.previous\_command.indent()) ) - self.chunk.reference\_indent( aWeb, aTangler, self.chunk.previous\_command.indent() ) + self.logger.debug("Indent %r + %r", aTangler.context, self.chunk.previous\_command.indent()) + self.chunk.reference\_indent(aWeb, aTangler, self.chunk.previous\_command.indent()) - self.logger.debug( "Tangling chunk {!r}".format(self.fullName) ) + self.logger.debug("Tangling %r with chunks %r", self.fullName, self.chunkList) if len(self.chunkList) != 0: for p in self.chunkList: - p.tangle( aWeb, aTangler ) + p.tangle(aWeb, aTangler) else: - raise Error( "Attempt to tangle an undefined Chunk, {!s}.".format( self.fullName, ) ) + raise Error(f"Attempt to tangle an undefined Chunk, {self.fullName!s}.") - self.chunk.reference\_dedent( aWeb, aTangler ) + self.chunk.reference\_dedent(aWeb, aTangler) .. .. class:: small - |loz| *ReferenceCommand tangle a referenced chunk (90)*. Used by: ReferenceCommand class... (`86`_) + |loz| *ReferenceCommand tangle a referenced chunk (92)*. Used by: ReferenceCommand class... (`88`_) Reference Strategy @@ -4630,24 +5053,26 @@ this object. -.. _`91`: -.. rubric:: Reference class hierarchy - strategies for references to a chunk (91) = +.. _`93`: +.. rubric:: Reference class hierarchy - strategies for references to a chunk (93) = .. parsed-literal:: :class: code - class Reference: - def \_\_init\_\_( self ): - self.logger= logging.getLogger( self.\_\_class\_\_.\_\_qualname\_\_ ) - def chunkReferencedBy( self, aChunk ): + class Reference(abc.ABC): + def \_\_init\_\_(self) -> None: + self.logger = logging.getLogger(self.\_\_class\_\_.\_\_qualname\_\_) + + @abc.abstractmethod + def chunkReferencedBy(self, aChunk: Chunk) -> list[Chunk]: """Return a list of Chunks.""" - pass + ... .. .. class:: small - |loz| *Reference class hierarchy - strategies for references to a chunk (91)*. Used by: Base Class Definitions (`1`_) + |loz| *Reference class hierarchy - strategies for references to a chunk (93)*. Used by: Base Class Definitions (`1`_) SimpleReference Class @@ -4657,22 +5082,22 @@ The SimpleReference subclass does the simplest version of resolution. It returns the ``Chunks`` referenced. -.. _`92`: -.. rubric:: Reference class hierarchy - strategies for references to a chunk (92) += +.. _`94`: +.. rubric:: Reference class hierarchy - strategies for references to a chunk (94) += .. parsed-literal:: :class: code - class SimpleReference( Reference ): - def chunkReferencedBy( self, aChunk ): - refBy= aChunk.referencedBy + class SimpleReference(Reference): + def chunkReferencedBy(self, aChunk: Chunk) -> list[Chunk]: + refBy = aChunk.referencedBy return refBy .. .. class:: small - |loz| *Reference class hierarchy - strategies for references to a chunk (92)*. Used by: Base Class Definitions (`1`_) + |loz| *Reference class hierarchy - strategies for references to a chunk (94)*. Used by: Base Class Definitions (`1`_) TransitiveReference Class @@ -4685,32 +5110,32 @@ This requires walking through the ``Web`` to locate "parents" of each referenced ``Chunk``. -.. _`93`: -.. rubric:: Reference class hierarchy - strategies for references to a chunk (93) += +.. _`95`: +.. rubric:: Reference class hierarchy - strategies for references to a chunk (95) += .. parsed-literal:: :class: code - class TransitiveReference( Reference ): - def chunkReferencedBy( self, aChunk ): - refBy= aChunk.referencedBy - self.logger.debug( "References: {!s}({:d}) {!r}".format(aChunk.name, aChunk.seq, refBy) ) - return self.allParentsOf( refBy ) - def allParentsOf( self, chunkList, depth=0 ): + class TransitiveReference(Reference): + def chunkReferencedBy(self, aChunk: Chunk) -> list[Chunk]: + refBy = aChunk.referencedBy + self.logger.debug("References: %r(%d) %r", aChunk.name, aChunk.seq, refBy) + return self.allParentsOf(refBy) + def allParentsOf(self, chunkList: list[Chunk], depth: int = 0) -> list[Chunk]: """Transitive closure of parents via recursive ascent. """ final = [] for c in chunkList: - final.append( c ) - final.extend( self.allParentsOf( c.referencedBy, depth+1 ) ) - self.logger.debug( "References: {0:>{indent}s} {1!s}".format('--', final, indent=2\*depth) ) + final.append(c) + final.extend(self.allParentsOf(c.referencedBy, depth+1)) + self.logger.debug(f"References: {'--':>{2\*depth}s} {final!s}") return final .. .. class:: small - |loz| *Reference class hierarchy - strategies for references to a chunk (93)*. Used by: Base Class Definitions (`1`_) + |loz| *Reference class hierarchy - strategies for references to a chunk (95)*. Used by: Base Class Definitions (`1`_) @@ -4732,7 +5157,7 @@ The typical creation is as follows: .. parsed-literal:: - raise Error("No full name for {!r}".format(chunk.name), chunk) + raise Error(f"No full name for {chunk.name!r}", chunk) A typical exception-handling suite might look like this: @@ -4741,9 +5166,9 @@ A typical exception-handling suite might look like this: try: *...something that may raise an Error or Exception...* except Error as e: - print( e.args ) # this is a pyWeb internal Error + print(e.args) # this is a pyWeb internal Error except Exception as w: - print( w.args ) # this is some other Python Exception + print(w.args) # this is some other Python Exception The ``Error`` class is a subclass of ``Exception`` used to differentiate application-specific @@ -4752,19 +5177,19 @@ but merely creates a distinct class to facilitate writing ``except`` statements. -.. _`94`: -.. rubric:: Error class - defines the errors raised (94) = +.. _`96`: +.. rubric:: Error class - defines the errors raised (96) = .. parsed-literal:: :class: code - class Error( Exception ): pass + class Error(Exception): pass .. .. class:: small - |loz| *Error class - defines the errors raised (94)*. Used by: Base Class Definitions (`1`_) + |loz| *Error class - defines the errors raised (96)*. Used by: Base Class Definitions (`1`_) The Web and WebReader Classes @@ -4799,8 +5224,8 @@ Fundamentally, a ``Web`` is a hybrid list-dictionary. A web instance has a number of attributes. -:webFileName: - the name of the original .w file. +:web_path: + the Path of the source ``.w`` file. :chunkSeq: the sequence of ``Chunk`` instances as seen in the input file. @@ -4828,37 +5253,39 @@ A web instance has a number of attributes. named chunk. -.. _`95`: -.. rubric:: Web class - describes the overall "web" of chunks (95) = +.. _`97`: +.. rubric:: Web class - describes the overall "web" of chunks (97) = .. parsed-literal:: :class: code class Web: """The overall Web of chunks.""" - def \_\_init\_\_( self ): - self.webFileName= None - self.chunkSeq= [] - self.output= {} # Map filename to Chunk - self.named= {} # Map chunkname to Chunk - self.sequence= 0 - self.logger= logging.getLogger( self.\_\_class\_\_.\_\_qualname\_\_ ) - def \_\_str\_\_( self ): - return "Web {!r}".format( self.webFileName, ) + def \_\_init\_\_(self, file\_path: Path \| None = None) -> None: + self.web\_path = file\_path + self.chunkSeq: list[Chunk] = [] + self.output: dict[str, list[Chunk]] = {} # Map filename to Chunk + self.named: dict[str, list[Chunk]] = {} # Map chunkname to Chunk + self.sequence = 0 + self.errors = 0 + self.logger = logging.getLogger(self.\_\_class\_\_.\_\_qualname\_\_) + + def \_\_str\_\_(self) -> str: + return f"Web {self.web\_path!r}" - |srarr|\ Web construction methods used by Chunks and WebReader (`97`_) - |srarr|\ Web Chunk name resolution methods (`102`_), |srarr|\ (`103`_) - |srarr|\ Web Chunk cross reference methods (`104`_), |srarr|\ (`106`_), |srarr|\ (`107`_), |srarr|\ (`108`_) - |srarr|\ Web determination of the language from the first chunk (`111`_) - |srarr|\ Web tangle the output files (`112`_) - |srarr|\ Web weave the output document (`113`_) + |srarr|\ Web construction methods used by Chunks and WebReader (`99`_) + |srarr|\ Web Chunk name resolution methods (`104`_), |srarr|\ (`105`_) + |srarr|\ Web Chunk cross reference methods (`106`_), |srarr|\ (`108`_), |srarr|\ (`109`_), |srarr|\ (`110`_) + |srarr|\ Web determination of the language from the first chunk (`113`_) + |srarr|\ Web tangle the output files (`114`_) + |srarr|\ Web weave the output document (`115`_) .. .. class:: small - |loz| *Web class - describes the overall "web" of chunks (95)*. Used by: Base Class Definitions (`1`_) + |loz| *Web class - describes the overall "web" of chunks (97)*. Used by: Base Class Definitions (`1`_) Web Construction @@ -4880,12 +5307,11 @@ to contain a more complete description of the chunk. We include a weakref to the ``Web`` to each ``Chunk``. -.. _`96`: -.. rubric:: Imports (96) += +.. _`98`: +.. rubric:: Imports (98) += .. parsed-literal:: :class: code - import weakref @@ -4893,26 +5319,26 @@ We include a weakref to the ``Web`` to each ``Chunk``. .. class:: small - |loz| *Imports (96)*. Used by: pyweb.py (`153`_) + |loz| *Imports (98)*. Used by: pyweb.py (`155`_) -.. _`97`: -.. rubric:: Web construction methods used by Chunks and WebReader (97) = +.. _`99`: +.. rubric:: Web construction methods used by Chunks and WebReader (99) = .. parsed-literal:: :class: code - |srarr|\ Web add full chunk names, ignoring abbreviated names (`98`_) - |srarr|\ Web add an anonymous chunk (`99`_) - |srarr|\ Web add a named macro chunk (`100`_) - |srarr|\ Web add an output file definition chunk (`101`_) + |srarr|\ Web add full chunk names, ignoring abbreviated names (`100`_) + |srarr|\ Web add an anonymous chunk (`101`_) + |srarr|\ Web add a named macro chunk (`102`_) + |srarr|\ Web add an output file definition chunk (`103`_) .. .. class:: small - |loz| *Web construction methods used by Chunks and WebReader (97)*. Used by: Web class... (`95`_) + |loz| *Web construction methods used by Chunks and WebReader (99)*. Used by: Web class... (`97`_) A name is only added to the known names when it is @@ -4955,22 +5381,22 @@ uses an abbreviated name. We would no longer need to return a value from this function, either. -.. _`98`: -.. rubric:: Web add full chunk names, ignoring abbreviated names (98) = +.. _`100`: +.. rubric:: Web add full chunk names, ignoring abbreviated names (100) = .. parsed-literal:: :class: code - def addDefName( self, name ): + def addDefName(self, name: str) -> str \| None: """Reference to or definition of a chunk name.""" - nm= self.fullNameFor( name ) + nm = self.fullNameFor(name) if nm is None: return None if nm[-3:] == '...': - self.logger.debug( "Abbreviated reference {!r}".format(name) ) + self.logger.debug("Abbreviated reference %r", name) return None # first occurance is a forward reference using an abbreviation if nm not in self.named: - self.named[nm]= [] - self.logger.debug( "Adding empty chunk {!r}".format(name) ) + self.named[nm] = [] + self.logger.debug("Adding empty chunk %r", name) return nm @@ -4978,7 +5404,7 @@ uses an abbreviated name. .. class:: small - |loz| *Web add full chunk names, ignoring abbreviated names (98)*. Used by: Web construction... (`97`_) + |loz| *Web add full chunk names, ignoring abbreviated names (100)*. Used by: Web construction... (`99`_) An anonymous ``Chunk`` is kept in a sequence of Chunks, used for @@ -4986,23 +5412,23 @@ tangling. -.. _`99`: -.. rubric:: Web add an anonymous chunk (99) = +.. _`101`: +.. rubric:: Web add an anonymous chunk (101) = .. parsed-literal:: :class: code - def add( self, chunk ): + def add(self, chunk: Chunk) -> None: """Add an anonymous chunk.""" - self.chunkSeq.append( chunk ) - chunk.web= weakref.ref(self) + self.chunkSeq.append(chunk) + chunk.web = weakref.ref(self) .. .. class:: small - |loz| *Web add an anonymous chunk (99)*. Used by: Web construction... (`97`_) + |loz| *Web add an anonymous chunk (101)*. Used by: Web construction... (`99`_) A named ``Chunk`` is defined with a ``@d`` command. @@ -5036,34 +5462,34 @@ in the list. Otherwise, it's False. The ``addDefName()`` no longer needs to return a value. -.. _`100`: -.. rubric:: Web add a named macro chunk (100) = +.. _`102`: +.. rubric:: Web add a named macro chunk (102) = .. parsed-literal:: :class: code - def addNamed( self, chunk ): + def addNamed(self, chunk: Chunk) -> None: """Add a named chunk to a sequence with a given name.""" - self.chunkSeq.append( chunk ) - chunk.web= weakref.ref(self) - nm= self.addDefName( chunk.name ) + self.chunkSeq.append(chunk) + chunk.web = weakref.ref(self) + nm = self.addDefName(chunk.name) if nm: # We found the full name for this chunk self.sequence += 1 - chunk.seq= self.sequence - chunk.fullName= nm - self.named[nm].append( chunk ) - chunk.initial= len(self.named[nm]) == 1 - self.logger.debug( "Extending chunk {!r} from {!r}".format(nm, chunk.name) ) + chunk.seq = self.sequence + chunk.fullName = nm + self.named[nm].append(chunk) + chunk.initial = len(self.named[nm]) == 1 + self.logger.debug("Extending chunk %r from %r", nm, chunk.name) else: - raise Error("No full name for {!r}".format(chunk.name), chunk) + raise Error(f"No full name for {chunk.name!r}", chunk) .. .. class:: small - |loz| *Web add a named macro chunk (100)*. Used by: Web construction... (`97`_) + |loz| *Web add a named macro chunk (102)*. Used by: Web construction... (`99`_) An output file definition ``Chunk`` is defined with an ``@o`` @@ -5094,23 +5520,23 @@ If the chunk list was empty, this is the first chunk, the -.. _`101`: -.. rubric:: Web add an output file definition chunk (101) = +.. _`103`: +.. rubric:: Web add an output file definition chunk (103) = .. parsed-literal:: :class: code - def addOutput( self, chunk ): + def addOutput(self, chunk: Chunk) -> None: """Add an output chunk to a sequence with a given name.""" - self.chunkSeq.append( chunk ) - chunk.web= weakref.ref(self) + self.chunkSeq.append(chunk) + chunk.web = weakref.ref(self) if chunk.name not in self.output: self.output[chunk.name] = [] - self.logger.debug( "Adding chunk {!r}".format(chunk.name) ) + self.logger.debug("Adding chunk %r", chunk.name) self.sequence += 1 - chunk.seq= self.sequence - chunk.fullName= chunk.name - self.output[chunk.name].append( chunk ) + chunk.seq = self.sequence + chunk.fullName = chunk.name + self.output[chunk.name].append(chunk) chunk.initial = len(self.output[chunk.name]) == 1 @@ -5118,7 +5544,7 @@ If the chunk list was empty, this is the first chunk, the .. class:: small - |loz| *Web add an output file definition chunk (101)*. Used by: Web construction... (`97`_) + |loz| *Web add an output file definition chunk (103)*. Used by: Web construction... (`99`_) Web Chunk Name Resolution @@ -5149,30 +5575,37 @@ The ``fullNameFor()`` method resolves full name for a chunk as follows: -.. _`102`: -.. rubric:: Web Chunk name resolution methods (102) = +.. _`104`: +.. rubric:: Web Chunk name resolution methods (104) = .. parsed-literal:: :class: code - def fullNameFor( self, name ): + def fullNameFor(self, name: str) -> str: """Resolve "..." names into the full name.""" - if name in self.named: return name - if name[-3:] == '...': - best= [ n for n in self.named.keys() - if n.startswith( name[:-3] ) ] - if len(best) > 1: - raise Error("Ambiguous abbreviation {!r}, matches {!r}".format( name, list(sorted(best)) ) ) - elif len(best) == 1: - return best[0] - return name + if name in self.named: + return name + elif name.endswith('...'): + best = [n + for n in self.named + if n.startswith(name[:-3]) + ] + match best: + case []: + return name + case [singleton]: + return singleton + case \_: + raise Error(f"Ambiguous abbreviation {name!r}, matches {sorted(best)!r}") + else: + return name .. .. class:: small - |loz| *Web Chunk name resolution methods (102)*. Used by: Web class... (`95`_) + |loz| *Web Chunk name resolution methods (104)*. Used by: Web class... (`97`_) The ``getchunk()`` method locates a named sequence of chunks by first determining the full name @@ -5183,40 +5616,38 @@ is unresolvable. It might be more helpful for debugging to emit this as an error in the weave and tangle results and keep processing. This would allow an author to -catch multiple errors in a single run of pyWeb. +catch multiple errors in a single run of **py-web-tool** . -.. _`103`: -.. rubric:: Web Chunk name resolution methods (103) += +.. _`105`: +.. rubric:: Web Chunk name resolution methods (105) += .. parsed-literal:: :class: code - def getchunk( self, name ): + def getchunk(self, name: str) -> list[Chunk]: """Locate a named sequence of chunks.""" - nm= self.fullNameFor( name ) + nm = self.fullNameFor(name) if nm in self.named: return self.named[nm] - raise Error( "Cannot resolve {!r} in {!r}".format(name,self.named.keys()) ) + raise Error(f"Cannot resolve {name!r} in {self.named.keys()!r}") .. .. class:: small - |loz| *Web Chunk name resolution methods (103)*. Used by: Web class... (`95`_) + |loz| *Web Chunk name resolution methods (105)*. Used by: Web class... (`97`_) Web Cross-Reference Support ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Cross-reference support includes creating and reporting on the various cross-references available in a web. This includes creating the list of chunks that reference a given chunk; and returning the file, macro and user identifier cross references. - Each ``Chunk`` has a list ``Reference`` commands that shows the chunks to which a chunk refers. These relationships must be reversed to show the chunks that refer to a given chunk. This is done by traversing @@ -5224,7 +5655,6 @@ the entire web of named chunks and recording each chunk-to-chunk reference. This mapping has the referred-to chunk as the key, and a sequence of referring chunks as the value. - The accumulation is initiated by the web's ``createUsedBy()`` method. This method visits a ``Chunk``, calling the ``genReferences()`` method, passing in the ``Web`` instance @@ -5234,35 +5664,35 @@ of each ``Command`` instance in the chunk. Most commands do nothing, but a ``ReferenceCommand`` will resolve the name to which it refers. - When the ``createUsedBy()`` method has accumulated the entire cross reference, it also assures that all chunks are used exactly once. -.. _`104`: -.. rubric:: Web Chunk cross reference methods (104) = +.. _`106`: +.. rubric:: Web Chunk cross reference methods (106) = .. parsed-literal:: :class: code - def createUsedBy( self ): + def createUsedBy(self) -> None: """Update every piece of a Chunk to show how the chunk is referenced. Each piece can then report where it's used in the web. """ for aChunk in self.chunkSeq: #usage = (self.fullNameFor(aChunk.name), aChunk.seq) - for aRefName in aChunk.genReferences( self ): - for c in self.getchunk( aRefName ): - c.referencedBy.append( aChunk ) + for aRefName in aChunk.genReferences(self): + for c in self.getchunk(aRefName): + c.referencedBy.append(aChunk) c.refCount += 1 - |srarr|\ Web Chunk check reference counts are all one (`105`_) + + |srarr|\ Web Chunk check reference counts are all one (`107`_) .. .. class:: small - |loz| *Web Chunk cross reference methods (104)*. Used by: Web class... (`95`_) + |loz| *Web Chunk cross reference methods (106)*. Used by: Web class... (`97`_) We verify that the reference count for a @@ -5270,39 +5700,39 @@ Chunk is exactly one. We don't gracefully tolerate multiple references to a Chunk or unreferenced chunks. -.. _`105`: -.. rubric:: Web Chunk check reference counts are all one (105) = +.. _`107`: +.. rubric:: Web Chunk check reference counts are all one (107) = .. parsed-literal:: :class: code for nm in self.no\_reference(): - self.logger.warn( "No reference to {!r}".format(nm) ) + self.logger.warning("No reference to %r", nm) for nm in self.multi\_reference(): - self.logger.warn( "Multiple references to {!r}".format(nm) ) + self.logger.warning("Multiple references to %r", nm) for nm in self.no\_definition(): - self.logger.error( "No definition for {!r}".format(nm) ) + self.logger.error("No definition for %r", nm) self.errors += 1 .. .. class:: small - |loz| *Web Chunk check reference counts are all one (105)*. Used by: Web Chunk cross reference methods... (`104`_) + |loz| *Web Chunk check reference counts are all one (107)*. Used by: Web Chunk cross reference methods... (`106`_) -The one-pass version +An alternative one-pass version of the above algorithm: .. parsed-literal:: - for nm,cl in self.named.items(): + for nm, cl in self.named.items(): if len(cl) > 0: if cl[0].refCount == 0: - self.logger.warn( "No reference to {!r}".format(nm) ) + self.logger.warning("No reference to %r", nm) elif cl[0].refCount > 1: - self.logger.warn( "Multiple references to {!r}".format(nm) ) + self.logger.warning("Multiple references to %r", nm) else: - self.logger.error( "No definition for {!r}".format(nm) ) + self.logger.error("No definition for %r", nm) We use three methods to filter chunk names into @@ -5315,51 +5745,49 @@ is a list of chunks referenced but not defined. -.. _`106`: -.. rubric:: Web Chunk cross reference methods (106) += +.. _`108`: +.. rubric:: Web Chunk cross reference methods (108) += .. parsed-literal:: :class: code - def no\_reference( self ): - return [ nm for nm,cl in self.named.items() if len(cl)>0 and cl[0].refCount == 0 ] - def multi\_reference( self ): - return [ nm for nm,cl in self.named.items() if len(cl)>0 and cl[0].refCount > 1 ] - def no\_definition( self ): - return [ nm for nm,cl in self.named.items() if len(cl) == 0 ] + def no\_reference(self) -> list[str]: + return [nm for nm, cl in self.named.items() if len(cl)>0 and cl[0].refCount == 0] + def multi\_reference(self) -> list[str]: + return [nm for nm, cl in self.named.items() if len(cl)>0 and cl[0].refCount > 1] + def no\_definition(self) -> list[str]: + return [nm for nm, cl in self.named.items() if len(cl) == 0] .. .. class:: small - |loz| *Web Chunk cross reference methods (106)*. Used by: Web class... (`95`_) + |loz| *Web Chunk cross reference methods (108)*. Used by: Web class... (`97`_) The ``fileXref()`` method visits all named file output chunks in ``output`` and collects the sequence numbers of each section in the sequence of chunks. - The ``chunkXref()`` method uses the same algorithm as a the ``fileXref()`` method, but applies it to the ``named`` mapping. - -.. _`107`: -.. rubric:: Web Chunk cross reference methods (107) += +.. _`109`: +.. rubric:: Web Chunk cross reference methods (109) += .. parsed-literal:: :class: code - def fileXref( self ): - fx= {} - for f,cList in self.output.items(): - fx[f]= [ c.seq for c in cList ] + def fileXref(self) -> dict[str, list[int]]: + fx = {} + for f, cList in self.output.items(): + fx[f] = [c.seq for c in cList] return fx - def chunkXref( self ): - mx= {} - for n,cList in self.named.items(): - mx[n]= [ c.seq for c in cList ] + def chunkXref(self) -> dict[str, list[int]]: + mx = {} + for n, cList in self.named.items(): + mx[n] = [c.seq for c in cList] return mx @@ -5367,7 +5795,7 @@ but applies it to the ``named`` mapping. .. class:: small - |loz| *Web Chunk cross reference methods (107)*. Used by: Web class... (`95`_) + |loz| *Web Chunk cross reference methods (109)*. Used by: Web class... (`97`_) The ``userNamesXref()`` method creates a mapping for each @@ -5375,9 +5803,8 @@ user identifier. The value for this mapping is a tuple with the chunk that defined the identifer (via a ``@|`` command), and a sequence of chunks that reference the identifier. - For example: -``{ 'Web': ( 87, (88,93,96,101,102,104) ), 'Chunk': ( 53, (54,55,56,60,57,58,59) ) }``, +``{'Web': (87, (88,93,96,101,102,104)), 'Chunk': (53, (54,55,56,60,57,58,59))}``, shows that the identifier ``'Web'`` is defined in chunk with a sequence number of 87, and referenced in the sequence of chunks that follow. @@ -5393,30 +5820,32 @@ This works in two passes: -.. _`108`: -.. rubric:: Web Chunk cross reference methods (108) += +.. _`110`: +.. rubric:: Web Chunk cross reference methods (110) += .. parsed-literal:: :class: code - def userNamesXref( self ): - ux= {} - self.\_gatherUserId( self.named, ux ) - self.\_gatherUserId( self.output, ux ) - self.\_updateUserId( self.named, ux ) - self.\_updateUserId( self.output, ux ) + def userNamesXref(self) -> dict[str, tuple[int, list[int]]]: + ux: dict[str, tuple[int, list[int]]] = {} + self.\_gatherUserId(self.named, ux) + self.\_gatherUserId(self.output, ux) + self.\_updateUserId(self.named, ux) + self.\_updateUserId(self.output, ux) return ux - def \_gatherUserId( self, chunkMap, ux ): - |srarr|\ collect all user identifiers from a given map into ux (`109`_) - def \_updateUserId( self, chunkMap, ux ): - |srarr|\ find user identifier usage and update ux from the given map (`110`_) + + def \_gatherUserId(self, chunkMap: dict[str, list[Chunk]], ux: dict[str, tuple[int, list[int]]]) -> None: + |srarr|\ collect all user identifiers from a given map into ux (`111`_) + + def \_updateUserId(self, chunkMap: dict[str, list[Chunk]], ux: dict[str, tuple[int, list[int]]]) -> None: + |srarr|\ find user identifier usage and update ux from the given map (`112`_) .. .. class:: small - |loz| *Web Chunk cross reference methods (108)*. Used by: Web class... (`95`_) + |loz| *Web Chunk cross reference methods (110)*. Used by: Web class... (`97`_) User identifiers are collected by visiting each of the sequence of @@ -5428,8 +5857,8 @@ list as a default action. -.. _`109`: -.. rubric:: collect all user identifiers from a given map into ux (109) = +.. _`111`: +.. rubric:: collect all user identifiers from a given map into ux (111) = .. parsed-literal:: :class: code @@ -5437,13 +5866,13 @@ list as a default action. for n,cList in chunkMap.items(): for c in cList: for id in c.getUserIDRefs(): - ux[id]= ( c.seq, [] ) + ux[id] = (c.seq, []) .. .. class:: small - |loz| *collect all user identifiers from a given map into ux (109)*. Used by: Web Chunk cross reference methods... (`108`_) + |loz| *collect all user identifiers from a given map into ux (111)*. Used by: Web Chunk cross reference methods... (`110`_) User identifiers are cross-referenced by visiting @@ -5454,26 +5883,26 @@ this is appended to the sequence of chunks that reference the original user iden -.. _`110`: -.. rubric:: find user identifier usage and update ux from the given map (110) = +.. _`112`: +.. rubric:: find user identifier usage and update ux from the given map (112) = .. parsed-literal:: :class: code # examine source for occurrences of all names in ux.keys() for id in ux.keys(): - self.logger.debug( "References to {!r}".format(id) ) - idpat= re.compile( r'\\W{!s}\\W'.format(id) ) + self.logger.debug("References to %r", id) + idpat = re.compile(f'\\\\W{id}\\\\W') for n,cList in chunkMap.items(): for c in cList: - if c.seq != ux[id][0] and c.searchForRE( idpat ): - ux[id][1].append( c.seq ) + if c.seq != ux[id][0] and c.searchForRE(idpat): + ux[id][1].append(c.seq) .. .. class:: small - |loz| *find user identifier usage and update ux from the given map (110)*. Used by: Web Chunk cross reference methods... (`108`_) + |loz| *find user identifier usage and update ux from the given map (112)*. Used by: Web Chunk cross reference methods... (`110`_) Loop Detection @@ -5526,17 +5955,17 @@ LaTeX files typically begin with '%' or '\'. Everything else is probably RST. -.. _`111`: -.. rubric:: Web determination of the language from the first chunk (111) = +.. _`113`: +.. rubric:: Web determination of the language from the first chunk (113) = .. parsed-literal:: :class: code - def language( self, preferredWeaverClass=None ): + def language(self, preferredWeaverClass: type["Weaver"] \| None = None) -> "Weaver": """Construct a weaver appropriate to the document's language""" if preferredWeaverClass: return preferredWeaverClass() - self.logger.debug( "Picking a weaver based on first chunk {!r}".format(self.chunkSeq[0][:4]) ) + self.logger.debug("Picking a weaver based on first chunk %r", str(self.chunkSeq[0])[:4]) if self.chunkSeq[0].startswith('<'): return HTML() if self.chunkSeq[0].startswith('%') or self.chunkSeq[0].startswith('\\\\'): @@ -5548,7 +5977,7 @@ Everything else is probably RST. .. class:: small - |loz| *Web determination of the language from the first chunk (111)*. Used by: Web class... (`95`_) + |loz| *Web determination of the language from the first chunk (113)*. Used by: Web class... (`97`_) The ``tangle()`` method of the ``Web`` class performs @@ -5557,24 +5986,24 @@ named output file. Note that several ``Chunks`` may share the file name, requir the file be composed of material from each ``Chunk``, in order. -.. _`112`: -.. rubric:: Web tangle the output files (112) = +.. _`114`: +.. rubric:: Web tangle the output files (114) = .. parsed-literal:: :class: code - def tangle( self, aTangler ): + def tangle(self, aTangler: "Tangler") -> None: for f, c in self.output.items(): - with aTangler.open(f): + with aTangler.open(Path(f)): for p in c: - p.tangle( self, aTangler ) + p.tangle(self, aTangler) .. .. class:: small - |loz| *Web tangle the output files (112)*. Used by: Web class... (`95`_) + |loz| *Web tangle the output files (114)*. Used by: Web class... (`97`_) The ``weave()`` method of the ``Web`` class creates the final documentation. @@ -5592,34 +6021,36 @@ The decision is delegated to the referenced chunk. Should it go in ``ReferenceCommand weave...``? -.. _`113`: -.. rubric:: Web weave the output document (113) = +.. _`115`: +.. rubric:: Web weave the output document (115) = .. parsed-literal:: :class: code - def weave( self, aWeaver ): - self.logger.debug( "Weaving file from {!r}".format(self.webFileName) ) - basename, \_ = os.path.splitext( self.webFileName ) - with aWeaver.open(basename): + def weave(self, aWeaver: "Weaver") -> None: + self.logger.debug("Weaving file from '%s'", self.web\_path) + if not self.web\_path: + raise Error("No filename supplied for weaving.") + with aWeaver.open(self.web\_path): for c in self.chunkSeq: - c.weave( self, aWeaver ) - def weaveChunk( self, name, aWeaver ): - self.logger.debug( "Weaving chunk {!r}".format(name) ) - chunkList= self.getchunk(name) + c.weave(self, aWeaver) + + def weaveChunk(self, name: str, aWeaver: "Weaver") -> None: + self.logger.debug("Weaving chunk %r", name) + chunkList = self.getchunk(name) if not chunkList: - raise Error( "No Definition for {!r}".format(name) ) - chunkList[0].weaveReferenceTo( self, aWeaver ) + raise Error(f"No Definition for {name!r}") + chunkList[0].weaveReferenceTo(self, aWeaver) for p in chunkList[1:]: - aWeaver.write( aWeaver.referenceSep() ) - p.weaveShortReferenceTo( self, aWeaver ) + aWeaver.write(aWeaver.referenceSep()) + p.weaveShortReferenceTo(self, aWeaver) .. .. class:: small - |loz| *Web weave the output document (113)*. Used by: Web class... (`95`_) + |loz| *Web weave the output document (115)*. Used by: Web class... (`97`_) The WebReader Class @@ -5631,7 +6062,7 @@ initial ``WebReader`` instance is created with code like the following: .. parsed-literal:: - p= WebReader() + p = WebReader() p.command = options.commandCharacter This will define the command character; usually provided as a command-line parameter to the application. @@ -5641,7 +6072,7 @@ instance is created with code like the following: .. parsed-literal:: - c= WebReader( parent=parentWebReader ) + c = WebReader(parent=parentWebReader) @@ -5694,7 +6125,7 @@ The class has the following attributes: :_source: The open source being used by ``load()``. -:fileName: +:filePath: is used to pass the file name to the Web instance. :theWeb: @@ -5712,8 +6143,8 @@ The class has the following attributes: Summaries -.. _`114`: -.. rubric:: WebReader class - parses the input file, building the Web structure (114) = +.. _`116`: +.. rubric:: WebReader class - parses the input file, building the Web structure (116) = .. parsed-literal:: :class: code @@ -5721,56 +6152,63 @@ The class has the following attributes: class WebReader: """Parse an input file, creating Chunks and Commands.""" - output\_option\_parser= OptionParser( - OptionDef( "-start", nargs=1, default=None ), - OptionDef( "-end", nargs=1, default="" ), - OptionDef( "argument", nargs='\*' ), - ) + output\_option\_parser = OptionParser( + OptionDef("-start", nargs=1, default=None), + OptionDef("-end", nargs=1, default=""), + OptionDef("argument", nargs='\*'), + ) - definition\_option\_parser= OptionParser( - OptionDef( "-indent", nargs=0 ), - OptionDef( "-noindent", nargs=0 ), - OptionDef( "argument", nargs='\*' ), - ) + definition\_option\_parser = OptionParser( + OptionDef("-indent", nargs=0), + OptionDef("-noindent", nargs=0), + OptionDef("argument", nargs='\*'), + ) + + # Configuration + command: str + permitList: list[str] + base\_path : Path + + # State of the reader + \_source: TextIO + filePath: Path + theWeb: "Web" + tokenizer: Tokenizer + aChunk: Chunk - def \_\_init\_\_( self, parent=None ): - self.logger= logging.getLogger( self.\_\_class\_\_.\_\_qualname\_\_ ) + def \_\_init\_\_(self, parent: Optional["WebReader"] = None) -> None: + self.logger = logging.getLogger(self.\_\_class\_\_.\_\_qualname\_\_) # Configuration of this reader. - self.parent= parent + self.parent = parent if self.parent: - self.command= self.parent.command - self.permitList= self.parent.permitList + self.command = self.parent.command + self.permitList = self.parent.permitList else: # Defaults until overridden - self.command= '@' - self.permitList= [] - - # Load options - self.\_source= None - self.fileName= None - self.theWeb= None - - # State of reading and parsing. - self.tokenizer= None - self.aChunk= None - + self.command = '@' + self.permitList = [] + # Summary - self.totalLines= 0 - self.totalFiles= 0 - self.errors= 0 + self.totalLines = 0 + self.totalFiles = 0 + self.errors = 0 + + |srarr|\ WebReader command literals (`131`_) - |srarr|\ WebReader command literals (`130`_) - def \_\_str\_\_( self ): + def \_\_str\_\_(self) -> str: return self.\_\_class\_\_.\_\_name\_\_ + |srarr|\ WebReader location in the input stream (`128`_) - |srarr|\ WebReader load the web (`129`_) - |srarr|\ WebReader handle a command string (`115`_), |srarr|\ (`127`_) + + |srarr|\ WebReader load the web (`130`_) + + |srarr|\ WebReader handle a command string (`117`_), |srarr|\ (`127`_) .. .. class:: small - |loz| *WebReader class - parses the input file, building the Web structure (114)*. Used by: Base Class Definitions (`1`_) + |loz| *WebReader class - parses the input file, building the Web structure (116)*. Used by: Base Class Definitions (`1`_) Command recognition is done via a **Chain of Command**-like design. @@ -5799,56 +6237,53 @@ A subclass can override ``handleCommand()`` to by ``load()`` is to treat the command a text, but also issue a warning. -.. _`115`: -.. rubric:: WebReader handle a command string (115) = -.. parsed-literal:: - :class: code - - - def handleCommand( self, token ): - self.logger.debug( "Reading {!r}".format(token) ) - |srarr|\ major commands segment the input into separate Chunks (`116`_) - |srarr|\ minor commands add Commands to the current Chunk (`121`_) - elif token[:2] in (self.cmdlcurl,self.cmdlbrak): - # These should have been consumed as part of @o and @d parsing - self.logger.error( "Extra {!r} (possibly missing chunk name) near {!r}".format(token, self.location()) ) - self.errors += 1 - else: - return None # did not recogize the command - return True # did recognize the command - - -.. - - .. class:: small - - |loz| *WebReader handle a command string (115)*. Used by: WebReader class... (`114`_) - - -The following sequence of ``if``-``elif`` statements identifies -the structural commands that partition the input into separate ``Chunks``. - - -.. _`116`: -.. rubric:: major commands segment the input into separate Chunks (116) = +.. _`117`: +.. rubric:: WebReader handle a command string (117) = .. parsed-literal:: :class: code - if token[:2] == self.cmdo: - |srarr|\ start an OutputChunk, adding it to the web (`117`_) - elif token[:2] == self.cmdd: - |srarr|\ start a NamedChunk or NamedDocumentChunk, adding it to the web (`118`_) - elif token[:2] == self.cmdi: - |srarr|\ import another file (`119`_) - elif token[:2] in (self.cmdrcurl,self.cmdrbrak): - |srarr|\ finish a chunk, start a new Chunk adding it to the web (`120`_) - -.. - - .. class:: small + def handleCommand(self, token: str) -> bool: + self.logger.debug("Reading %r", token) + + match token[:2]: + case self.cmdo: + |srarr|\ start an OutputChunk, adding it to the web (`118`_) + case self.cmdd: + |srarr|\ start a NamedChunk or NamedDocumentChunk, adding it to the web (`119`_) + case self.cmdi: + |srarr|\ include another file (`120`_) + case self.cmdrcurl \| self.cmdrbrak: + |srarr|\ finish a chunk, start a new Chunk adding it to the web (`121`_) + case self.cmdpipe: + |srarr|\ assign user identifiers to the current chunk (`122`_) + case self.cmdf: + self.aChunk.append(FileXrefCommand(self.tokenizer.lineNumber)) + case self.cmdm: + self.aChunk.append(MacroXrefCommand(self.tokenizer.lineNumber)) + case self.cmdu: + self.aChunk.append(UserIdXrefCommand(self.tokenizer.lineNumber)) + case self.cmdlangl: + |srarr|\ add a reference command to the current chunk (`123`_) + case self.cmdlexpr: + |srarr|\ add an expression command to the current chunk (`125`_) + case self.cmdcmd: + |srarr|\ double at-sign replacement, append this character to previous TextCommand (`126`_) + case self.cmdlcurl \| self.cmdlbrak: + # These should have been consumed as part of @o and @d parsing + self.logger.error("Extra %r (possibly missing chunk name) near %r", token, self.location()) + self.errors += 1 + case \_: + return False # did not recogize the command + return True # did recognize the command + + +.. + + .. class:: small + + |loz| *WebReader handle a command string (117)*. Used by: WebReader class... (`116`_) - |loz| *major commands segment the input into separate Chunks (116)*. Used by: WebReader handle a command... (`115`_) An output chunk has the form ``@o`` *name* ``@{`` *content* ``@}``. @@ -5859,31 +6294,32 @@ to this chunk while waiting for the final ``@}`` token to end the chunk. We'll use an ``OptionParser`` to locate the optional parameters. This will then let us build an appropriate instance of ``OutputChunk``. -With some small additional changes, we could use ``OutputChunk( **options )``. +With some small additional changes, we could use ``OutputChunk(**options)``. -.. _`117`: -.. rubric:: start an OutputChunk, adding it to the web (117) = +.. _`118`: +.. rubric:: start an OutputChunk, adding it to the web (118) = .. parsed-literal:: :class: code - args= next(self.tokenizer) - self.expect( (self.cmdlcurl,) ) - options= self.output\_option\_parser.parse( args ) - self.aChunk= OutputChunk( name=options['argument'], - comment\_start= options.get('start',None), - comment\_end= options.get('end',""), - ) - self.aChunk.fileName= self.fileName - self.aChunk.webAdd( self.theWeb ) + args = next(self.tokenizer) + self.expect((self.cmdlcurl,)) + options = self.output\_option\_parser.parse(args) + self.aChunk = OutputChunk( + name=' '.join(options['argument']), + comment\_start=''.join(options.get('start', "# ")), + comment\_end=''.join(options.get('end', "")), + ) + self.aChunk.filePath = self.filePath + self.aChunk.webAdd(self.theWeb) # capture an OutputChunk up to @} .. .. class:: small - |loz| *start an OutputChunk, adding it to the web (117)*. Used by: major commands... (`116`_) + |loz| *start an OutputChunk, adding it to the web (118)*. Used by: WebReader handle a command... (`117`_) A named chunk has the form ``@d`` *name* ``@{`` *content* ``@}`` for @@ -5904,41 +6340,41 @@ Then we can use options to create an appropriate subclass of ``NamedChunk``. If "-indent" is in options, this is the default. If both are in the options, we can provide a warning, I guess. - **TODO** Add a warning for conflicting options. +**TODO:** Add a warning for conflicting options. -.. _`118`: -.. rubric:: start a NamedChunk or NamedDocumentChunk, adding it to the web (118) = +.. _`119`: +.. rubric:: start a NamedChunk or NamedDocumentChunk, adding it to the web (119) = .. parsed-literal:: :class: code - args= next(self.tokenizer) - brack= self.expect( (self.cmdlcurl,self.cmdlbrak) ) - options= self.output\_option\_parser.parse( args ) - name=options['argument'] + args = next(self.tokenizer) + brack = self.expect((self.cmdlcurl,self.cmdlbrak)) + options = self.output\_option\_parser.parse(args) + name = ' '.join(options['argument']) if brack == self.cmdlbrak: - self.aChunk= NamedDocumentChunk( name ) + self.aChunk = NamedDocumentChunk(name) elif brack == self.cmdlcurl: if '-noindent' in options: - self.aChunk= NamedChunk\_Noindent( name ) + self.aChunk = NamedChunk\_Noindent(name) else: - self.aChunk= NamedChunk( name ) + self.aChunk = NamedChunk(name) elif brack == None: pass # Error noted by expect() else: - raise Error( "Design Error" ) + raise Error("Design Error") - self.aChunk.fileName= self.fileName - self.aChunk.webAdd( self.theWeb ) + self.aChunk.filePath = self.filePath + self.aChunk.webAdd(self.theWeb) # capture a NamedChunk up to @} or @] .. .. class:: small - |loz| *start a NamedChunk or NamedDocumentChunk, adding it to the web (118)*. Used by: major commands... (`116`_) + |loz| *start a NamedChunk or NamedDocumentChunk, adding it to the web (119)*. Used by: WebReader handle a command... (`117`_) An import command has the unusual form of ``@i`` *name*, with no trailing @@ -5962,49 +6398,45 @@ can be set to permit failure; this allows a ``.w`` to include a file that does not yet exist. The primary use case for this feature is when weaving test output. -The first pass of **pyWeb** tangles the program source files; they are -then run to create test output; the second pass of **pyWeb** weaves this +The first pass of **py-web-tool** tangles the program source files; they are +then run to create test output; the second pass of **py-web-tool** weaves this test output into the final document via the ``@i`` command. -.. _`119`: -.. rubric:: import another file (119) = +.. _`120`: +.. rubric:: include another file (120) = .. parsed-literal:: :class: code - incFile= next(self.tokenizer).strip() + incPath = Path(next(self.tokenizer).strip()) try: - self.logger.info( "Including {!r}".format(incFile) ) - include= WebReader( parent=self ) - include.load( self.theWeb, incFile ) + include = WebReader(parent=self) + if not incPath.is\_absolute(): + incPath = self.base\_path / incPath + self.logger.info("Including '%s'", incPath) + include.load(self.theWeb, incPath) self.totalLines += include.tokenizer.lineNumber self.totalFiles += include.totalFiles if include.errors: self.errors += include.errors - self.logger.error( - "Errors in included file {!s}, output is incomplete.".format( - incFile) ) + self.logger.error("Errors in included file '%s', output is incomplete.", incPath) except Error as e: - self.logger.error( - "Problems with included file {!s}, output is incomplete.".format( - incFile) ) + self.logger.error("Problems with included file '%s', output is incomplete.", incPath) self.errors += 1 except IOError as e: - self.logger.error( - "Problems with included file {!s}, output is incomplete.".format( - incFile) ) + self.logger.error("Problems finding included file '%s', output is incomplete.", incPath) # Discretionary -- sometimes we want to continue if self.cmdi in self.permitList: pass - else: raise # TODO: Seems heavy-handed - self.aChunk= Chunk() - self.aChunk.webAdd( self.theWeb ) + else: raise # Seems heavy-handed, but, the file wasn't found! + self.aChunk = Chunk() + self.aChunk.webAdd(self.theWeb) .. .. class:: small - |loz| *import another file (119)*. Used by: major commands... (`116`_) + |loz| *include another file (120)*. Used by: WebReader handle a command... (`117`_) When a ``@}`` or ``@]`` are found, this finishes a named chunk. The next @@ -6020,55 +6452,25 @@ For the base ``Chunk`` class, this would be false, but for all other subclasses -.. _`120`: -.. rubric:: finish a chunk, start a new Chunk adding it to the web (120) = +.. _`121`: +.. rubric:: finish a chunk, start a new Chunk adding it to the web (121) = .. parsed-literal:: :class: code - self.aChunk= Chunk() - self.aChunk.webAdd( self.theWeb ) + self.aChunk = Chunk() + self.aChunk.webAdd(self.theWeb) .. .. class:: small - |loz| *finish a chunk, start a new Chunk adding it to the web (120)*. Used by: major commands... (`116`_) + |loz| *finish a chunk, start a new Chunk adding it to the web (121)*. Used by: WebReader handle a command... (`117`_) The following sequence of ``elif`` statements identifies the minor commands that add ``Command`` instances to the current open ``Chunk``. - - -.. _`121`: -.. rubric:: minor commands add Commands to the current Chunk (121) = -.. parsed-literal:: - :class: code - - - elif token[:2] == self.cmdpipe: - |srarr|\ assign user identifiers to the current chunk (`122`_) - elif token[:2] == self.cmdf: - self.aChunk.append( FileXrefCommand(self.tokenizer.lineNumber) ) - elif token[:2] == self.cmdm: - self.aChunk.append( MacroXrefCommand(self.tokenizer.lineNumber) ) - elif token[:2] == self.cmdu: - self.aChunk.append( UserIdXrefCommand(self.tokenizer.lineNumber) ) - elif token[:2] == self.cmdlangl: - |srarr|\ add a reference command to the current chunk (`123`_) - elif token[:2] == self.cmdlexpr: - |srarr|\ add an expression command to the current chunk (`125`_) - elif token[:2] == self.cmdcmd: - |srarr|\ double at-sign replacement, append this character to previous TextCommand (`126`_) - -.. - - .. class:: small - - |loz| *minor commands add Commands to the current Chunk (121)*. Used by: WebReader handle a command... (`115`_) - - User identifiers occur after a ``@|`` in a ``NamedChunk``. Note that no check is made to assure that the previous ``Chunk`` was indeed a named @@ -6089,17 +6491,17 @@ These are accumulated and expanded by ``@u`` reference try: - self.aChunk.setUserIDRefs( next(self.tokenizer).strip() ) + self.aChunk.setUserIDRefs(next(self.tokenizer).strip()) except AttributeError: # Out of place @\| user identifier command - self.logger.error( "Unexpected references near {!s}: {!s}".format(self.location(),token) ) + self.logger.error("Unexpected references near %r: %r", self.location(), token) self.errors += 1 .. .. class:: small - |loz| *assign user identifiers to the current chunk (122)*. Used by: minor commands... (`121`_) + |loz| *assign user identifiers to the current chunk (122)*. Used by: WebReader handle a command... (`117`_) A reference command has the form ``@<``\ *name*\ ``@>``. We accept three @@ -6114,18 +6516,18 @@ tokens from the input, the middle token is the referenced name. # get the name, introduce into the named Chunk dictionary - expand= next(self.tokenizer).strip() - closing= self.expect( (self.cmdrangl,) ) - self.theWeb.addDefName( expand ) - self.aChunk.append( ReferenceCommand( expand, self.tokenizer.lineNumber ) ) - self.aChunk.appendText( "", self.tokenizer.lineNumber ) # to collect following text - self.logger.debug( "Reading {!r} {!r}".format(expand, closing) ) + expand = next(self.tokenizer).strip() + closing = self.expect((self.cmdrangl,)) + self.theWeb.addDefName(expand) + self.aChunk.append(ReferenceCommand(expand, self.tokenizer.lineNumber)) + self.aChunk.appendText("", self.tokenizer.lineNumber) # to collect following text + self.logger.debug("Reading %r %r", expand, closing) .. .. class:: small - |loz| *add a reference command to the current chunk (123)*. Used by: minor commands... (`121`_) + |loz| *add a reference command to the current chunk (123)*. Used by: WebReader handle a command... (`117`_) An expression command has the form ``@(``\ *Python Expression*\ ``@)``. @@ -6137,7 +6539,7 @@ There are two alternative semantics for an embedded expression. - **Deferred Execution**. This requires definition of a new subclass of ``Command``, ``ExpressionCommand``, and appends it into the current ``Chunk``. At weave and tangle time, this expression is evaluated. The insert might look something like this: - ``aChunk.append( ExpressionCommand(expression, self.tokenizer.lineNumber) )``. + ``aChunk.append(ExpressionCommand(expression, self.tokenizer.lineNumber))``. - **Immediate Execution**. This simply creates a context and evaluates the Python expression. The output from the expression becomes a ``TextCommand``, and @@ -6145,8 +6547,8 @@ There are two alternative semantics for an embedded expression. We use the **Immediate Execution** semantics. -Note that we've removed the blanket ``os``. We only provide ``os.path``. -An ``os.getcwd()`` must be changed to ``os.path.realpath('.')``. +Note that we've removed the blanket ``os``. We provide ``os.path`` library. +An ``os.getcwd()`` could be changed to ``os.path.realpath('.')``. .. _`124`: @@ -6164,7 +6566,7 @@ An ``os.getcwd()`` must be changed to ``os.path.realpath('.')``. .. class:: small - |loz| *Imports (124)*. Used by: pyweb.py (`153`_) + |loz| *Imports (124)*. Used by: pyweb.py (`155`_) @@ -6175,37 +6577,46 @@ An ``os.getcwd()`` must be changed to ``os.path.realpath('.')``. # get the Python expression, create the expression result - expression= next(self.tokenizer) - self.expect( (self.cmdrexpr,) ) + expression = next(self.tokenizer) + self.expect((self.cmdrexpr,)) try: # Build Context - safe= types.SimpleNamespace( \*\*dict( (name,obj) + # \*\*TODO:\*\* Parts of this are static. + dangerous = { + 'breakpoint', 'compile', 'eval', 'exec', 'execfile', 'globals', 'help', 'input', + 'memoryview', 'open', 'print', 'super', '\_\_import\_\_' + } + safe = types.SimpleNamespace(\*\*dict( + (name, obj) for name,obj in builtins.\_\_dict\_\_.items() - if name not in ('eval', 'exec', 'open', '\_\_import\_\_'))) - globals= dict( - \_\_builtins\_\_= safe, - os= types.SimpleNamespace(path=os.path), - datetime= datetime, - platform= platform, - theLocation= self.location(), - theWebReader= self, - theFile= self.theWeb.webFileName, - thisApplication= sys.argv[0], - \_\_version\_\_= \_\_version\_\_, + if name not in dangerous + )) + globals = dict( + \_\_builtins\_\_=safe, + os=types.SimpleNamespace(path=os.path, getcwd=os.getcwd, name=os.name), + time=time, + datetime=datetime, + platform=platform, + theLocation=str(self.location()), + theWebReader=self, + theFile=self.theWeb.web\_path, + thisApplication=sys.argv[0], + \_\_version\_\_=\_\_version\_\_, # Legacy compatibility. Deprecated. + version=\_\_version\_\_, ) # Evaluate - result= str(eval(expression, globals)) - except Exception as e: - self.logger.error( 'Failure to process {!r}: result is {!r}'.format(expression, e) ) + result = str(eval(expression, globals)) + except Exception as exc: + self.logger.error('Failure to process %r: result is %r', expression, exc) self.errors += 1 - result= "@({!r}: Error {!r}@)".format(expression, e) - self.aChunk.appendText( result, self.tokenizer.lineNumber ) + result = f"@({expression!r}: Error {exc!r}@)" + self.aChunk.appendText(result, self.tokenizer.lineNumber) .. .. class:: small - |loz| *add an expression command to the current chunk (125)*. Used by: minor commands... (`121`_) + |loz| *add an expression command to the current chunk (125)*. Used by: WebReader handle a command... (`117`_) A double command sequence (``'@@'``, when the command is an ``'@'``) has the @@ -6225,13 +6636,13 @@ largely seamless. :class: code - self.aChunk.appendText( self.command, self.tokenizer.lineNumber ) + self.aChunk.appendText(self.command, self.tokenizer.lineNumber) .. .. class:: small - |loz| *double at-sign replacement, append this character to previous TextCommand (126)*. Used by: minor commands... (`121`_) + |loz| *double at-sign replacement, append this character to previous TextCommand (126)*. Used by: WebReader handle a command... (`117`_) The ``expect()`` method examines the @@ -6246,19 +6657,19 @@ This is used by ``handleCommand()``. :class: code - def expect( self, tokens ): + def expect(self, tokens: Iterable[str]) -> str \| None: try: - t= next(self.tokenizer) + t = next(self.tokenizer) while t == '\\n': - t= next(self.tokenizer) + t = next(self.tokenizer) except StopIteration: - self.logger.error( "At {!r}: end of input, {!r} not found".format(self.location(),tokens) ) + self.logger.error("At %r: end of input, %r not found", self.location(), tokens) self.errors += 1 - return + return None if t not in tokens: - self.logger.error( "At {!r}: expected {!r}, found {!r}".format(self.location(),tokens,t) ) + self.logger.error("At %r: expected %r, found %r", self.location(), tokens, t) self.errors += 1 - return + return None return t @@ -6266,7 +6677,7 @@ This is used by ``handleCommand()``. .. class:: small - |loz| *WebReader handle a command string (127)*. Used by: WebReader class... (`114`_) + |loz| *WebReader handle a command string (127)*. Used by: WebReader class... (`116`_) The ``location()`` provides the file name and line number. @@ -6280,15 +6691,15 @@ to correctly reference the original input files. :class: code - def location( self ): - return (self.fileName, self.tokenizer.lineNumber+1) + def location(self) -> tuple[str, int]: + return (str(self.filePath), self.tokenizer.lineNumber+1) .. .. class:: small - |loz| *WebReader location in the input stream (128)*. Used by: WebReader class... (`114`_) + |loz| *WebReader location in the input stream (128)*. Used by: WebReader class... (`116`_) The ``load()`` method reads the entire input file as a sequence @@ -6303,54 +6714,74 @@ is that it's always loading a single top-level web. .. _`129`: -.. rubric:: WebReader load the web (129) = +.. rubric:: Imports (129) += .. parsed-literal:: :class: code - - def load( self, web, filename, source=None ): - self.theWeb= web - self.fileName= filename - - # Only set the a web filename once using the first file. - # This should be a setter property of the web. - if self.theWeb.webFileName is None: - self.theWeb.webFileName= self.fileName - - if source: - self.\_source= source - self.parse\_source() - else: - with open( self.fileName, "r" ) as self.\_source: - self.parse\_source() - - def parse\_source( self ): - self.tokenizer= Tokenizer( self.\_source, self.command ) - self.totalFiles += 1 - - self.aChunk= Chunk() # Initial anonymous chunk of text. - self.aChunk.webAdd( self.theWeb ) - - for token in self.tokenizer: - if len(token) >= 2 and token.startswith(self.command): - if self.handleCommand( token ): - continue - else: - self.logger.warn( 'Unknown @-command in input: {!r}'.format(token) ) - self.aChunk.appendText( token, self.tokenizer.lineNumber ) - elif token: - # Accumulate a non-empty block of text in the current chunk. - self.aChunk.appendText( token, self.tokenizer.lineNumber ) - + from typing import TextIO .. .. class:: small - |loz| *WebReader load the web (129)*. Used by: WebReader class... (`114`_) + |loz| *Imports (129)*. Used by: pyweb.py (`155`_) -The command character can be changed to permit + +.. _`130`: +.. rubric:: WebReader load the web (130) = +.. parsed-literal:: + :class: code + + + def load(self, web: "Web", filepath: Path, source: TextIO \| None = None) -> "WebReader": + self.theWeb = web + self.filePath = filepath + self.base\_path = self.filePath.parent + + # Only set the a web's filename once using the first file. + # \*\*TODO:\*\* this should be a setter property of the web. + if self.theWeb.web\_path is None: + self.theWeb.web\_path = self.filePath + + if source: + self.\_source = source + self.parse\_source() + else: + with self.filePath.open() as self.\_source: + self.parse\_source() + return self + + def parse\_source(self) -> None: + self.tokenizer = Tokenizer(self.\_source, self.command) + self.totalFiles += 1 + + self.aChunk = Chunk() # Initial anonymous chunk of text. + self.aChunk.webAdd(self.theWeb) + + for token in self.tokenizer: + if len(token) >= 2 and token.startswith(self.command): + if self.handleCommand(token): + continue + else: + self.logger.error('Unknown @-command in input: %r', token) + self.aChunk.appendText(token, self.tokenizer.lineNumber) + elif token: + # Accumulate a non-empty block of text in the current chunk. + self.aChunk.appendText(token, self.tokenizer.lineNumber) + else: + # Whitespace + pass + + +.. + + .. class:: small + + |loz| *WebReader load the web (130)*. Used by: WebReader class... (`116`_) + + +The command character can be changed to permit some flexibility when working with languages that make extensive use of the ``@`` symbol, i.e., PERL. The initialization of the ``WebReader`` is based on the selected @@ -6358,39 +6789,39 @@ command character. -.. _`130`: -.. rubric:: WebReader command literals (130) = +.. _`131`: +.. rubric:: WebReader command literals (131) = .. parsed-literal:: :class: code # Structural ("major") commands - self.cmdo= self.command+'o' - self.cmdd= self.command+'d' - self.cmdlcurl= self.command+'{' - self.cmdrcurl= self.command+'}' - self.cmdlbrak= self.command+'[' - self.cmdrbrak= self.command+']' - self.cmdi= self.command+'i' + self.cmdo = self.command+'o' + self.cmdd = self.command+'d' + self.cmdlcurl = self.command+'{' + self.cmdrcurl = self.command+'}' + self.cmdlbrak = self.command+'[' + self.cmdrbrak = self.command+']' + self.cmdi = self.command+'i' # Inline ("minor") commands - self.cmdlangl= self.command+'<' - self.cmdrangl= self.command+'>' - self.cmdpipe= self.command+'\|' - self.cmdlexpr= self.command+'(' - self.cmdrexpr= self.command+')' - self.cmdcmd= self.command+self.command + self.cmdlangl = self.command+'<' + self.cmdrangl = self.command+'>' + self.cmdpipe = self.command+'\|' + self.cmdlexpr = self.command+'(' + self.cmdrexpr = self.command+')' + self.cmdcmd = self.command+self.command # Content "minor" commands - self.cmdf= self.command+'f' - self.cmdm= self.command+'m' - self.cmdu= self.command+'u' + self.cmdf = self.command+'f' + self.cmdm = self.command+'m' + self.cmdu = self.command+'u' .. .. class:: small - |loz| *WebReader command literals (130)*. Used by: WebReader class... (`114`_) + |loz| *WebReader command literals (131)*. Used by: WebReader class... (`116`_) @@ -6432,45 +6863,48 @@ We can safely filter these via a generator expression. The tokenizer counts newline characters for us, so that error messages can include a line number. Also, we can tangle comments into the file that include line numbers. -Since the tokenizer is a proper iterator, we can use ``tokens= iter(Tokenizer(source))`` +Since the tokenizer is a proper iterator, we can use ``tokens = iter(Tokenizer(source))`` and ``next(tokens)`` to step through the sequence of tokens until we raise a ``StopIteration`` exception. -.. _`131`: -.. rubric:: Imports (131) += +.. _`132`: +.. rubric:: Imports (132) += .. parsed-literal:: :class: code import re + from collections.abc import Iterator, Iterable .. .. class:: small - |loz| *Imports (131)*. Used by: pyweb.py (`153`_) + |loz| *Imports (132)*. Used by: pyweb.py (`155`_) -.. _`132`: -.. rubric:: Tokenizer class - breaks input into tokens (132) = +.. _`133`: +.. rubric:: Tokenizer class - breaks input into tokens (133) = .. parsed-literal:: :class: code - class Tokenizer: - def \_\_init\_\_( self, stream, command\_char='@' ): - self.command= command\_char - self.parsePat= re.compile( r'({!s}.\|\\n)'.format(self.command) ) - self.token\_iter= (t for t in self.parsePat.split( stream.read() ) if len(t) != 0) - self.lineNumber= 0 - def \_\_next\_\_( self ): - token= next(self.token\_iter) + class Tokenizer(Iterator[str]): + def \_\_init\_\_(self, stream: TextIO, command\_char: str='@') -> None: + self.command = command\_char + self.parsePat = re.compile(f'({self.command}.\|\\\\n)') + self.token\_iter = (t for t in self.parsePat.split(stream.read()) if len(t) != 0) + self.lineNumber = 0 + + def \_\_next\_\_(self) -> str: + token = next(self.token\_iter) self.lineNumber += token.count('\\n') return token - def \_\_iter\_\_( self ): + + def \_\_iter\_\_(self) -> Iterator[str]: return self @@ -6478,7 +6912,7 @@ exception. .. class:: small - |loz| *Tokenizer class - breaks input into tokens (132)*. Used by: Base Class Definitions (`1`_) + |loz| *Tokenizer class - breaks input into tokens (133)*. Used by: Base Class Definitions (`1`_) The Option Parser Class @@ -6505,8 +6939,8 @@ To handle this, we have a separate lexical scanner and parser for these two commands. -.. _`133`: -.. rubric:: Imports (133) += +.. _`134`: +.. rubric:: Imports (134) += .. parsed-literal:: :class: code @@ -6518,7 +6952,7 @@ two commands. .. class:: small - |loz| *Imports (133)*. Used by: pyweb.py (`153`_) + |loz| *Imports (134)*. Used by: pyweb.py (`155`_) Here's how we can define an option. @@ -6526,32 +6960,48 @@ Here's how we can define an option. .. parsed-literal:: OptionParser( - OptionDef( "-start", nargs=1, default=None ), - OptionDef( "-end", nargs=1, default="" ), - OptionDef( "-indent", nargs=0 ), # A default - OptionDef( "-noindent", nargs=0 ), - OptionDef( "argument", nargs='*' ), + OptionDef("-start", nargs=1, default=None), + OptionDef("-end", nargs=1, default=""), + OptionDef("-indent", nargs=0), # A default + OptionDef("-noindent", nargs=0), + OptionDef("argument", nargs='*'), ) The idea is to parallel ``argparse.add_argument()`` syntax. -.. _`134`: -.. rubric:: Option Parser class - locates optional values on commands (134) = +.. _`135`: +.. rubric:: Option Parser class - locates optional values on commands (135) = +.. parsed-literal:: + :class: code + + + class ParseError(Exception): pass + +.. + + .. class:: small + + |loz| *Option Parser class - locates optional values on commands (135)*. Used by: Base Class Definitions (`1`_) + + + +.. _`136`: +.. rubric:: Option Parser class - locates optional values on commands (136) += .. parsed-literal:: :class: code class OptionDef: - def \_\_init\_\_( self, name, \*\*kw ): - self.name= name - self.\_\_dict\_\_.update( kw ) + def \_\_init\_\_(self, name: str, \*\*kw: Any) -> None: + self.name = name + self.\_\_dict\_\_.update(kw) .. .. class:: small - |loz| *Option Parser class - locates optional values on commands (134)*. Used by: Base Class Definitions (`1`_) + |loz| *Option Parser class - locates optional values on commands (136)*. Used by: Base Class Definitions (`1`_) The parser breaks the text into words using ``shelex`` rules. @@ -6559,33 +7009,38 @@ It then steps through the words, accumulating the options and the final argument value. -.. _`135`: -.. rubric:: Option Parser class - locates optional values on commands (135) += +.. _`137`: +.. rubric:: Option Parser class - locates optional values on commands (137) += .. parsed-literal:: :class: code class OptionParser: - def \_\_init\_\_( self, \*arg\_defs ): - self.args= dict( (arg.name,arg) for arg in arg\_defs ) - self.trailers= [k for k in self.args.keys() if not k.startswith('-')] - def parse( self, text ): + def \_\_init\_\_(self, \*arg\_defs: Any) -> None: + self.args = dict((arg.name, arg) for arg in arg\_defs) + self.trailers = [k for k in self.args.keys() if not k.startswith('-')] + + def parse(self, text: str) -> dict[str, list[str]]: try: - word\_iter= iter(shlex.split(text)) + word\_iter = iter(shlex.split(text)) except ValueError as e: - raise Error( "Error parsing options in {!r}".format(text) ) - options = dict( s for s in self.\_group( word\_iter ) ) + raise Error(f"Error parsing options in {text!r}") + options = dict(self.\_group(word\_iter)) return options - def \_group( self, word\_iter ): - option, value, final= None, [], [] + + def \_group(self, word\_iter: Iterator[str]) -> Iterator[tuple[str, list[str]]]: + option: str \| None + value: list[str] + final: list[str] + option, value, final = None, [], [] for word in word\_iter: if word == '--': if option: yield option, value try: - final= [next(word\_iter)] + final = [next(word\_iter)] except StopIteration: - final= [] # Special case of '--' at the end. + final = [] # Special case of '--' at the end. break elif word.startswith('-'): if word in self.args: @@ -6593,28 +7048,28 @@ final argument value. yield option, value option, value = word, [] else: - raise ParseError( "Unknown option {0}".format(word) ) + raise ParseError(f"Unknown option {word!r}") else: if option: if self.args[option].nargs == len(value): yield option, value - final= [word] + final = [word] break else: - value.append( word ) + value.append(word) else: - final= [word] + final = [word] break # In principle, we step through the trailers based on nargs counts. for word in word\_iter: - final.append( word ) - yield self.trailers[0], " ".join(final) + final.append(word) + yield self.trailers[0], final .. .. class:: small - |loz| *Option Parser class - locates optional values on commands (135)*. Used by: Base Class Definitions (`1`_) + |loz| *Option Parser class - locates optional values on commands (137)*. Used by: Base Class Definitions (`1`_) In principle, we step through the trailers based on nargs counts. @@ -6627,11 +7082,11 @@ Then we'd have a loop something like this. (Untested, incomplete, just hand-wavi .. parsed-literal:: - trailers= self.trailers[:] # Stateful shallow copy + trailers = self.trailers[:] # Stateful shallow copy for word in word_iter: - if len(final) == trailers[-1].nargs: # nargs=='*' vs. nargs=int?? + if len(final) == trailers[-1].nargs: # nargs=='*' vs. nargs=int?? yield trailers[0], " ".join(final) - final= 0 + final = 0 trailers.pop(0) yield trailers[0], " ".join(final) @@ -6652,24 +7107,23 @@ This two pass action might be embedded in the following type of Python program. .. parsed-literal:: - import pyweb, os, runpy, sys - pyweb.tangle( "source.w" ) - with open("source.log", "w") as target: - sys.stdout= target - runpy.run_path( 'source.py' ) - sys.stdout= sys.__stdout__ - pyweb.weave( "source.w" ) + import pyweb, os, runpy, sys, pathlib, contextlib + log = pathlib.Path("source.log") + pyweb.tangle("source.w") + with log.open("w") as target: + with contextlib.redirect_stdout(target): + runpy.run_path('source.py') + pyweb.weave("source.w") -The first step runs **pyWeb**, excluding the final weaving pass. The second +The first step runs **py-web-tool** , excluding the final weaving pass. The second step runs the tangled program, ``source.py``, and produces test results in -some log file, ``source.log``. The third step runs pyWeb excluding the +some log file, ``source.log``. The third step runs **py-web-tool** excluding the tangle pass. This produces a final document that includes the ``source.log`` test results. - To accomplish this, we provide a class hierarchy that defines the various -actions of the pyWeb application. This class hierarchy defines an extensible set of +actions of the **py-web-tool** application. This class hierarchy defines an extensible set of fundamental actions. This gives us the flexibility to create a simple sequence of actions and execute any combination of these. It eliminates the need for a forest of ``if``-statements to determine precisely what will be done. @@ -6679,32 +7133,32 @@ application. A partner with this command hierarchy is the Application class that defines the application options, inputs and results. -.. _`136`: -.. rubric:: Action class hierarchy - used to describe basic actions of the application (136) = +.. _`138`: +.. rubric:: Action class hierarchy - used to describe actions of the application (138) = .. parsed-literal:: :class: code - |srarr|\ Action superclass has common features of all actions (`137`_) - |srarr|\ ActionSequence subclass that holds a sequence of other actions (`140`_) - |srarr|\ WeaveAction subclass initiates the weave action (`144`_) - |srarr|\ TangleAction subclass initiates the tangle action (`147`_) - |srarr|\ LoadAction subclass loads the document web (`150`_) + |srarr|\ Action superclass has common features of all actions (`139`_) + |srarr|\ ActionSequence subclass that holds a sequence of other actions (`142`_) + |srarr|\ WeaveAction subclass initiates the weave action (`146`_) + |srarr|\ TangleAction subclass initiates the tangle action (`149`_) + |srarr|\ LoadAction subclass loads the document web (`152`_) .. .. class:: small - |loz| *Action class hierarchy - used to describe basic actions of the application (136)*. Used by: Base Class Definitions (`1`_) + |loz| *Action class hierarchy - used to describe actions of the application (138)*. Used by: Base Class Definitions (`1`_) Action Class ~~~~~~~~~~~~~ -The ``Action`` class embodies the basic operations of pyWeb. +The ``Action`` class embodies the basic operations of **py-web-tool** . The intent of this hierarchy is to both provide an easily expanded method of adding new actions, but an easily specified list of actions for a particular -run of **pyWeb**. +run of **py-web-tool** . The overall process of the application is defined by an instance of ``Action``. This instance may be the ``WeaveAction`` instance, the ``TangleAction`` instance @@ -6718,8 +7172,8 @@ and an instance that excludes weaving. These correspond to the command-line opt .. parsed-literal:: - anOp= SomeAction( *parameters* ) - anOp.options= *argparse.Namespace* + anOp = SomeAction(*parameters*) + anOp.options = *argparse.Namespace* anOp.web = *Current web* anOp() @@ -6742,31 +7196,34 @@ An ``Action`` has a number of common attributes. -.. _`137`: -.. rubric:: Action superclass has common features of all actions (137) = +.. _`139`: +.. rubric:: Action superclass has common features of all actions (139) = .. parsed-literal:: :class: code class Action: """An action performed by pyWeb.""" - def \_\_init\_\_( self, name ): - self.name= name - self.web= None - self.options= None - self.start= None - self.logger= logging.getLogger( self.\_\_class\_\_.\_\_qualname\_\_ ) - def \_\_str\_\_( self ): - return "{!s} [{!s}]".format( self.name, self.web ) - |srarr|\ Action call method actually does the real work (`138`_) - |srarr|\ Action final summary of what was done (`139`_) + options : argparse.Namespace + web : "Web" + def \_\_init\_\_(self, name: str) -> None: + self.name = name + self.start: float \| None = None + self.logger = logging.getLogger(self.\_\_class\_\_.\_\_qualname\_\_) + + def \_\_str\_\_(self) -> str: + return f"{self.name!s} [{self.web!s}]" + + |srarr|\ Action call method actually does the real work (`140`_) + + |srarr|\ Action final summary of what was done (`141`_) .. .. class:: small - |loz| *Action superclass has common features of all actions (137)*. Used by: Action class hierarchy... (`136`_) + |loz| *Action superclass has common features of all actions (139)*. Used by: Action class hierarchy... (`138`_) The ``__call__()`` method does the real work of the action. @@ -6774,47 +7231,47 @@ For the superclass, it merely logs a message. This is overridden by a subclass. -.. _`138`: -.. rubric:: Action call method actually does the real work (138) = +.. _`140`: +.. rubric:: Action call method actually does the real work (140) = .. parsed-literal:: :class: code - def \_\_call\_\_( self ): - self.logger.info( "Starting {!s}".format(self.name) ) - self.start= time.process\_time() + def \_\_call\_\_(self) -> None: + self.logger.info("Starting %s", self.name) + self.start = time.process\_time() .. .. class:: small - |loz| *Action call method actually does the real work (138)*. Used by: Action superclass... (`137`_) + |loz| *Action call method actually does the real work (140)*. Used by: Action superclass... (`139`_) The ``summary()`` method returns some basic processing statistics for this action. - -.. _`139`: -.. rubric:: Action final summary of what was done (139) = +.. _`141`: +.. rubric:: Action final summary of what was done (141) = .. parsed-literal:: :class: code - def duration( self ): + def duration(self) -> float: """Return duration of the action.""" return (self.start and time.process\_time()-self.start) or 0 - def summary( self ): - return "{!s} in {:0.2f} sec.".format( self.name, self.duration() ) + + def summary(self) -> str: + return f"{self.name!s} in {self.duration():0.3f} sec." .. .. class:: small - |loz| *Action final summary of what was done (139)*. Used by: Action superclass... (`137`_) + |loz| *Action final summary of what was done (141)*. Used by: Action superclass... (`139`_) ActionSequence Class @@ -6834,30 +7291,34 @@ an ``append()`` method that is used to construct the sequence of actions. -.. _`140`: -.. rubric:: ActionSequence subclass that holds a sequence of other actions (140) = +.. _`142`: +.. rubric:: ActionSequence subclass that holds a sequence of other actions (142) = .. parsed-literal:: :class: code - class ActionSequence( Action ): + class ActionSequence(Action): """An action composed of a sequence of other actions.""" - def \_\_init\_\_( self, name, opSequence=None ): - super().\_\_init\_\_( name ) - if opSequence: self.opSequence= opSequence - else: self.opSequence= [] - def \_\_str\_\_( self ): - return "; ".join( [ str(x) for x in self.opSequence ] ) - |srarr|\ ActionSequence call method delegates the sequence of ations (`141`_) - |srarr|\ ActionSequence append adds a new action to the sequence (`142`_) - |srarr|\ ActionSequence summary summarizes each step (`143`_) + def \_\_init\_\_(self, name: str, opSequence: list[Action] \| None = None) -> None: + super().\_\_init\_\_(name) + if opSequence: self.opSequence = opSequence + else: self.opSequence = [] + + def \_\_str\_\_(self) -> str: + return "; ".join([str(x) for x in self.opSequence]) + + |srarr|\ ActionSequence call method delegates the sequence of ations (`143`_) + + |srarr|\ ActionSequence append adds a new action to the sequence (`144`_) + + |srarr|\ ActionSequence summary summarizes each step (`145`_) .. .. class:: small - |loz| *ActionSequence subclass that holds a sequence of other actions (140)*. Used by: Action class hierarchy... (`136`_) + |loz| *ActionSequence subclass that holds a sequence of other actions (142)*. Used by: Action class hierarchy... (`138`_) Since the macro ``__call__()`` method delegates to other Actions, @@ -6866,16 +7327,17 @@ it is possible to short-cut argument processing by using the Python sub-action. -.. _`141`: -.. rubric:: ActionSequence call method delegates the sequence of ations (141) = +.. _`143`: +.. rubric:: ActionSequence call method delegates the sequence of ations (143) = .. parsed-literal:: :class: code - def \_\_call\_\_( self ): + def \_\_call\_\_(self) -> None: + super().\_\_call\_\_() for o in self.opSequence: - o.web= self.web - o.options= self.options + o.web = self.web + o.options = self.options o() @@ -6883,49 +7345,49 @@ sub-action. .. class:: small - |loz| *ActionSequence call method delegates the sequence of ations (141)*. Used by: ActionSequence subclass... (`140`_) + |loz| *ActionSequence call method delegates the sequence of ations (143)*. Used by: ActionSequence subclass... (`142`_) Since this class is essentially a wrapper around the built-in sequence type, we delegate sequence related actions directly to the underlying sequence. -.. _`142`: -.. rubric:: ActionSequence append adds a new action to the sequence (142) = +.. _`144`: +.. rubric:: ActionSequence append adds a new action to the sequence (144) = .. parsed-literal:: :class: code - def append( self, anAction ): - self.opSequence.append( anAction ) + def append(self, anAction: Action) -> None: + self.opSequence.append(anAction) .. .. class:: small - |loz| *ActionSequence append adds a new action to the sequence (142)*. Used by: ActionSequence subclass... (`140`_) + |loz| *ActionSequence append adds a new action to the sequence (144)*. Used by: ActionSequence subclass... (`142`_) The ``summary()`` method returns some basic processing statistics for each step of this action. -.. _`143`: -.. rubric:: ActionSequence summary summarizes each step (143) = +.. _`145`: +.. rubric:: ActionSequence summary summarizes each step (145) = .. parsed-literal:: :class: code - def summary( self ): - return ", ".join( [ o.summary() for o in self.opSequence ] ) + def summary(self) -> str: + return ", ".join([o.summary() for o in self.opSequence]) .. .. class:: small - |loz| *ActionSequence summary summarizes each step (143)*. Used by: ActionSequence subclass... (`140`_) + |loz| *ActionSequence summary summarizes each step (145)*. Used by: ActionSequence subclass... (`142`_) WeaveAction Class @@ -6943,28 +7405,30 @@ If the options include ``theWeaver``, that ``Weaver`` instance will be used. Otherwise, the ``web.language()`` method function is used to guess what weaver to use. -.. _`144`: -.. rubric:: WeaveAction subclass initiates the weave action (144) = +.. _`146`: +.. rubric:: WeaveAction subclass initiates the weave action (146) = .. parsed-literal:: :class: code - class WeaveAction( Action ): + class WeaveAction(Action): """Weave the final document.""" - def \_\_init\_\_( self ): - super().\_\_init\_\_( "Weave" ) - def \_\_str\_\_( self ): - return "{!s} [{!s}, {!s}]".format( self.name, self.web, self.theWeaver ) + def \_\_init\_\_(self) -> None: + super().\_\_init\_\_("Weave") + + def \_\_str\_\_(self) -> str: + return f"{self.name!s} [{self.web!s}, {self.options.theWeaver!s}]" - |srarr|\ WeaveAction call method to pick the language (`145`_) - |srarr|\ WeaveAction summary of language choice (`146`_) + |srarr|\ WeaveAction call method to pick the language (`147`_) + + |srarr|\ WeaveAction summary of language choice (`148`_) .. .. class:: small - |loz| *WeaveAction subclass initiates the weave action (144)*. Used by: Action class hierarchy... (`136`_) + |loz| *WeaveAction subclass initiates the weave action (146)*. Used by: Action class hierarchy... (`138`_) The language is picked just prior to weaving. It is either (1) the language @@ -6975,26 +7439,25 @@ Weaving can only raise an exception when there is a reference to a chunk that is never defined. -.. _`145`: -.. rubric:: WeaveAction call method to pick the language (145) = +.. _`147`: +.. rubric:: WeaveAction call method to pick the language (147) = .. parsed-literal:: :class: code - def \_\_call\_\_( self ): + def \_\_call\_\_(self) -> None: super().\_\_call\_\_() if not self.options.theWeaver: # Examine first few chars of first chunk of web to determine language - self.options.theWeaver= self.web.language() - self.logger.info( "Using {0}".format(self.options.theWeaver.\_\_class\_\_.\_\_name\_\_) ) - self.options.theWeaver.reference\_style= self.options.reference\_style + self.options.theWeaver = self.web.language() + self.logger.info("Using %s", self.options.theWeaver.\_\_class\_\_.\_\_name\_\_) + self.options.theWeaver.reference\_style = self.options.reference\_style + self.options.theWeaver.output = self.options.output try: - self.web.weave( self.options.theWeaver ) - self.logger.info( "Finished Normally" ) + self.web.weave(self.options.theWeaver) + self.logger.info("Finished Normally") except Error as e: - self.logger.error( - "Problems weaving document from {!s} (weave file is faulty).".format( - self.web.webFileName) ) + self.logger.error("Problems weaving document from %r (weave file is faulty).", self.web.web\_path) #raise @@ -7002,7 +7465,7 @@ is never defined. .. class:: small - |loz| *WeaveAction call method to pick the language (145)*. Used by: WeaveAction subclass... (`144`_) + |loz| *WeaveAction call method to pick the language (147)*. Used by: WeaveAction subclass... (`146`_) The ``summary()`` method returns some basic processing @@ -7010,24 +7473,25 @@ statistics for the weave action. -.. _`146`: -.. rubric:: WeaveAction summary of language choice (146) = +.. _`148`: +.. rubric:: WeaveAction summary of language choice (148) = .. parsed-literal:: :class: code - def summary( self ): + def summary(self) -> str: if self.options.theWeaver and self.options.theWeaver.linesWritten > 0: - return "{!s} {:d} lines in {:0.2f} sec.".format( self.name, - self.options.theWeaver.linesWritten, self.duration() ) - return "did not {!s}".format( self.name, ) + return ( + f"{self.name!s} {self.options.theWeaver.linesWritten:d} lines in {self.duration():0.3f} sec." + ) + return f"did not {self.name!s}" .. .. class:: small - |loz| *WeaveAction summary of language choice (146)*. Used by: WeaveAction subclass... (`144`_) + |loz| *WeaveAction summary of language choice (148)*. Used by: WeaveAction subclass... (`146`_) TangleAction Class @@ -7044,25 +7508,27 @@ This class overrides the ``__call__()`` method of the superclass. The options **must** include ``theTangler``, with the ``Tangler`` instance to be used. -.. _`147`: -.. rubric:: TangleAction subclass initiates the tangle action (147) = +.. _`149`: +.. rubric:: TangleAction subclass initiates the tangle action (149) = .. parsed-literal:: :class: code - class TangleAction( Action ): + class TangleAction(Action): """Tangle source files.""" - def \_\_init\_\_( self ): - super().\_\_init\_\_( "Tangle" ) - |srarr|\ TangleAction call method does tangling of the output files (`148`_) - |srarr|\ TangleAction summary method provides total lines tangled (`149`_) + def \_\_init\_\_(self) -> None: + super().\_\_init\_\_("Tangle") + + |srarr|\ TangleAction call method does tangling of the output files (`150`_) + + |srarr|\ TangleAction summary method provides total lines tangled (`151`_) .. .. class:: small - |loz| *TangleAction subclass initiates the tangle action (147)*. Used by: Action class hierarchy... (`136`_) + |loz| *TangleAction subclass initiates the tangle action (149)*. Used by: Action class hierarchy... (`138`_) Tangling can only raise an exception when a cross reference request (``@f``, ``@m`` or ``@u``) @@ -7071,21 +7537,20 @@ with any of ``@d`` or ``@o`` and use ``@{`` ``@}`` brackets. -.. _`148`: -.. rubric:: TangleAction call method does tangling of the output files (148) = +.. _`150`: +.. rubric:: TangleAction call method does tangling of the output files (150) = .. parsed-literal:: :class: code - def \_\_call\_\_( self ): + def \_\_call\_\_(self) -> None: super().\_\_call\_\_() - self.options.theTangler.include\_line\_numbers= self.options.tangler\_line\_numbers + self.options.theTangler.include\_line\_numbers = self.options.tangler\_line\_numbers + self.options.theTangler.output = self.options.output try: - self.web.tangle( self.options.theTangler ) + self.web.tangle(self.options.theTangler) except Error as e: - self.logger.error( - "Problems tangling outputs from {!r} (tangle files are faulty).".format( - self.web.webFileName) ) + self.logger.error("Problems tangling outputs from %r (tangle files are faulty).", self.web.web\_path) #raise @@ -7093,31 +7558,32 @@ with any of ``@d`` or ``@o`` and use ``@{`` ``@}`` brackets. .. class:: small - |loz| *TangleAction call method does tangling of the output files (148)*. Used by: TangleAction subclass... (`147`_) + |loz| *TangleAction call method does tangling of the output files (150)*. Used by: TangleAction subclass... (`149`_) The ``summary()`` method returns some basic processing statistics for the tangle action. -.. _`149`: -.. rubric:: TangleAction summary method provides total lines tangled (149) = +.. _`151`: +.. rubric:: TangleAction summary method provides total lines tangled (151) = .. parsed-literal:: :class: code - def summary( self ): + def summary(self) -> str: if self.options.theTangler and self.options.theTangler.linesWritten > 0: - return "{!s} {:d} lines in {:0.2f} sec.".format( self.name, - self.options.theTangler.totalLines, self.duration() ) - return "did not {!r}".format( self.name, ) + return ( + f"{self.name!s} {self.options.theTangler.totalLines:d} lines in {self.duration():0.3f} sec." + ) + return f"did not {self.name!r}" .. .. class:: small - |loz| *TangleAction summary method provides total lines tangled (149)*. Used by: TangleAction subclass... (`147`_) + |loz| *TangleAction summary method provides total lines tangled (151)*. Used by: TangleAction subclass... (`149`_) @@ -7136,27 +7602,29 @@ The options **must** include ``webReader``, with the ``WebReader`` instance to b -.. _`150`: -.. rubric:: LoadAction subclass loads the document web (150) = +.. _`152`: +.. rubric:: LoadAction subclass loads the document web (152) = .. parsed-literal:: :class: code - class LoadAction( Action ): + class LoadAction(Action): """Load the source web.""" - def \_\_init\_\_( self ): - super().\_\_init\_\_( "Load" ) - def \_\_str\_\_( self ): - return "Load [{!s}, {!s}]".format( self.webReader, self.web ) - |srarr|\ LoadAction call method loads the input files (`151`_) - |srarr|\ LoadAction summary provides lines read (`152`_) + def \_\_init\_\_(self) -> None: + super().\_\_init\_\_("Load") + def \_\_str\_\_(self) -> str: + return f"Load [{self.webReader!s}, {self.web!s}]" + + |srarr|\ LoadAction call method loads the input files (`153`_) + + |srarr|\ LoadAction summary provides lines read (`154`_) .. .. class:: small - |loz| *LoadAction subclass loads the document web (150)*. Used by: Action class hierarchy... (`136`_) + |loz| *LoadAction subclass loads the document web (152)*. Used by: Action class hierarchy... (`138`_) Trying to load the web involves two steps, either of which can raise @@ -7179,29 +7647,28 @@ exceptions due to incorrect inputs. chunk reference cannot be resolved to a named chunk. -.. _`151`: -.. rubric:: LoadAction call method loads the input files (151) = +.. _`153`: +.. rubric:: LoadAction call method loads the input files (153) = .. parsed-literal:: :class: code - def \_\_call\_\_( self ): + def \_\_call\_\_(self) -> None: super().\_\_call\_\_() - self.webReader= self.options.webReader - self.webReader.command= self.options.command - self.webReader.permitList= self.options.permitList - self.web.webFileName= self.options.webFileName - error= "Problems with source file {!r}, no output produced.".format( - self.options.webFileName) + self.webReader = self.options.webReader + self.webReader.command = self.options.command + self.webReader.permitList = self.options.permitList + self.web.web\_path = self.options.source\_path + error = f"Problems with source file {self.options.source\_path!r}, no output produced." try: - self.webReader.load( self.web, self.options.webFileName ) + self.webReader.load(self.web, self.options.source\_path) if self.webReader.errors != 0: - self.logger.error( error ) - raise Error( "Syntax Errors in the Web" ) + self.logger.error(error) + raise Error("Syntax Errors in the Web") self.web.createUsedBy() if self.webReader.errors != 0: - self.logger.error( error ) - raise Error( "Internal Reference Errors in the Web" ) + self.logger.error(error) + raise Error("Internal Reference Errors in the Web") except Error as e: self.logger.error(error) raise # Older design. @@ -7214,30 +7681,30 @@ exceptions due to incorrect inputs. .. class:: small - |loz| *LoadAction call method loads the input files (151)*. Used by: LoadAction subclass... (`150`_) + |loz| *LoadAction call method loads the input files (153)*. Used by: LoadAction subclass... (`152`_) The ``summary()`` method returns some basic processing statistics for the load action. -.. _`152`: -.. rubric:: LoadAction summary provides lines read (152) = +.. _`154`: +.. rubric:: LoadAction summary provides lines read (154) = .. parsed-literal:: :class: code - def summary( self ): - return "{!s} {:d} lines from {:d} files in {:0.2f} sec.".format( - self.name, self.webReader.totalLines, - self.webReader.totalFiles, self.duration() ) + def summary(self) -> str: + return ( + f"{self.name!s} {self.webReader.totalLines:d} lines from {self.webReader.totalFiles:d} files in {self.duration():0.3f} sec." + ) .. .. class:: small - |loz| *LoadAction summary provides lines read (152)*. Used by: LoadAction subclass... (`150`_) + |loz| *LoadAction summary provides lines read (154)*. Used by: LoadAction subclass... (`152`_) @@ -7247,23 +7714,23 @@ statistics for the load action. The **pyWeb** application file is shown below: -.. _`153`: -.. rubric:: pyweb.py (153) = +.. _`155`: +.. rubric:: pyweb.py (155) = .. parsed-literal:: :class: code - |srarr|\ Overheads (`155`_), |srarr|\ (`156`_), |srarr|\ (`157`_) - |srarr|\ Imports (`11`_), |srarr|\ (`47`_), |srarr|\ (`96`_), |srarr|\ (`124`_), |srarr|\ (`131`_), |srarr|\ (`133`_), |srarr|\ (`154`_), |srarr|\ (`158`_), |srarr|\ (`164`_) + |srarr|\ Overheads (`157`_), |srarr|\ (`158`_), |srarr|\ (`159`_) + |srarr|\ Imports (`3`_), |srarr|\ (`12`_), |srarr|\ (`48`_), |srarr|\ (`58`_), |srarr|\ (`98`_), |srarr|\ (`124`_), |srarr|\ (`129`_), |srarr|\ (`132`_), |srarr|\ (`134`_), |srarr|\ (`156`_), |srarr|\ (`160`_), |srarr|\ (`166`_) |srarr|\ Base Class Definitions (`1`_) - |srarr|\ Application Class (`159`_), |srarr|\ (`160`_) - |srarr|\ Logging Setup (`165`_), |srarr|\ (`166`_) - |srarr|\ Interface Functions (`167`_) + |srarr|\ Application Class (`161`_), |srarr|\ (`162`_) + |srarr|\ Logging Setup (`167`_), |srarr|\ (`168`_) + |srarr|\ Interface Functions (`169`_) .. .. class:: small - |loz| *pyweb.py (153)*. + |loz| *pyweb.py (155)*. The `Overheads`_ are described below, they include things like: @@ -7309,8 +7776,8 @@ closer to where they're referenced. -.. _`154`: -.. rubric:: Imports (154) += +.. _`156`: +.. rubric:: Imports (156) += .. parsed-literal:: :class: code @@ -7325,7 +7792,7 @@ closer to where they're referenced. .. class:: small - |loz| *Imports (154)*. Used by: pyweb.py (`153`_) + |loz| *Imports (156)*. Used by: pyweb.py (`155`_) Note that ``os.path``, ``time``, ``datetime`` and ``platform``` @@ -7343,8 +7810,8 @@ file as standard input. -.. _`155`: -.. rubric:: Overheads (155) = +.. _`157`: +.. rubric:: Overheads (157) = .. parsed-literal:: :class: code @@ -7354,7 +7821,7 @@ file as standard input. .. class:: small - |loz| *Overheads (155)*. Used by: pyweb.py (`153`_) + |loz| *Overheads (157)*. Used by: pyweb.py (`155`_) A Python ``__doc__`` string provides a standard vehicle for documenting @@ -7364,44 +7831,24 @@ detailed usage information. -.. _`156`: -.. rubric:: Overheads (156) += -.. parsed-literal:: - :class: code - - """pyWeb Literate Programming - tangle and weave tool. - - Yet another simple literate programming tool derived from nuweb, - implemented entirely in Python. - This produces any markup for any programming language. - - Usage: - pyweb.py [-dvs] [-c x] [-w format] file.w - - Options: - -v verbose output (the default) - -s silent output - -d debugging output - -c x change the command character from '@' to x - -w format Use the given weaver for the final document. - Choices are rst, html, latex and htmlshort. - Additionally, a \`module.class\` name can be used. - -xw Exclude weaving - -xt Exclude tangling - -pi Permit include-command errors - -rt Transitive references - -rs Simple references (default) - -n Include line number comments in the tangled source; requires - comment start and stop on the @o commands. - - file.w The input file, with @o, @d, @i, @[, @{, @\|, @<, @f, @m, @u commands. +.. _`158`: +.. rubric:: Overheads (158) += +.. parsed-literal:: + :class: code + + """py-web-tool Literate Programming. + + Yet another simple literate programming tool derived from \*\*nuweb\*\*, + implemented entirely in Python. + With a suitable configuration, this weaves documents with any markup language, + and tangles source files for any programming language. """ .. .. class:: small - |loz| *Overheads (156)*. Used by: pyweb.py (`153`_) + |loz| *Overheads (158)*. Used by: pyweb.py (`155`_) The keyword cruft is a standard way of placing version control information into @@ -7413,23 +7860,23 @@ We also sneak in a "DO NOT EDIT" warning that belongs in all generated applicati source files. -.. _`157`: -.. rubric:: Overheads (157) += +.. _`159`: +.. rubric:: Overheads (159) += .. parsed-literal:: :class: code - \_\_version\_\_ = """3.0""" + \_\_version\_\_ = """3.1""" ### DO NOT EDIT THIS FILE! - ### It was created by /Users/slott/Documents/Projects/PyWebTool-3/pyweb/pyweb.py, \_\_version\_\_='3.0'. - ### From source pyweb.w modified Sat Jun 16 08:10:37 2018. - ### In working directory '/Users/slott/Documents/Projects/PyWebTool-3/pyweb'. + ### It was created by /Users/slott/Documents/Projects/py-web-tool/bootstrap/pyweb.py, \_\_version\_\_='3.0'. + ### From source pyweb.w modified Mon Jun 13 08:52:05 2022. + ### In working directory '/Users/slott/Documents/Projects/py-web-tool/src'. .. .. class:: small - |loz| *Overheads (157)*. Used by: pyweb.py (`153`_) + |loz| *Overheads (159)*. Used by: pyweb.py (`155`_) @@ -7465,13 +7912,13 @@ For example: import pyweb, argparse - p= argparse.ArgumentParser() + p = argparse.ArgumentParser() *argument definition* config = p.parse_args() - a= pyweb.Application() + a = pyweb.Application() *Configure the Application based on options* - a.process( config ) + a.process(config) The ``main()`` function creates an ``Application`` instance and @@ -7483,8 +7930,8 @@ The configuration can be either a ``types.SimpleNamespace`` or an -.. _`158`: -.. rubric:: Imports (158) += +.. _`160`: +.. rubric:: Imports (160) += .. parsed-literal:: :class: code @@ -7495,29 +7942,30 @@ The configuration can be either a ``types.SimpleNamespace`` or an .. class:: small - |loz| *Imports (158)*. Used by: pyweb.py (`153`_) + |loz| *Imports (160)*. Used by: pyweb.py (`155`_) -.. _`159`: -.. rubric:: Application Class (159) = +.. _`161`: +.. rubric:: Application Class (161) = .. parsed-literal:: :class: code class Application: - def \_\_init\_\_( self ): - self.logger= logging.getLogger( self.\_\_class\_\_.\_\_qualname\_\_ ) - |srarr|\ Application default options (`161`_) - |srarr|\ Application parse command line (`162`_) - |srarr|\ Application class process all files (`163`_) + def \_\_init\_\_(self) -> None: + self.logger = logging.getLogger(self.\_\_class\_\_.\_\_qualname\_\_) + |srarr|\ Application default options (`163`_) + + |srarr|\ Application parse command line (`164`_) + |srarr|\ Application class process all files (`165`_) .. .. class:: small - |loz| *Application Class (159)*. Used by: pyweb.py (`153`_) + |loz| *Application Class (161)*. Used by: pyweb.py (`155`_) The first part of parsing the command line is @@ -7595,8 +8043,8 @@ Rather than automate this, and potentially expose elements of the class hierarch that aren't really meant to be used, we provide a manually-developed list. -.. _`160`: -.. rubric:: Application Class (160) += +.. _`162`: +.. rubric:: Application Class (162) += .. parsed-literal:: :class: code @@ -7613,45 +8061,46 @@ that aren't really meant to be used, we provide a manually-developed list. .. class:: small - |loz| *Application Class (160)*. Used by: pyweb.py (`153`_) + |loz| *Application Class (162)*. Used by: pyweb.py (`155`_) The defaults used for application configuration. The ``expand()`` method expands on these simple text values to create more useful objects. -.. _`161`: -.. rubric:: Application default options (161) = +.. _`163`: +.. rubric:: Application default options (163) = .. parsed-literal:: :class: code - self.defaults= argparse.Namespace( - verbosity= logging.INFO, - command= '@', - weaver= 'rst', - skip= '', # Don't skip any steps - permit= '', # Don't tolerate missing includes - reference= 's', # Simple references - tangler\_line\_numbers= False, + self.defaults = argparse.Namespace( + verbosity=logging.INFO, + command='@', + weaver='rst', + skip='', # Don't skip any steps + permit='', # Don't tolerate missing includes + reference='s', # Simple references + tangler\_line\_numbers=False, + output=Path.cwd(), ) - self.expand( self.defaults ) + # self.expand(self.defaults) # Primitive Actions - self.loadOp= LoadAction() - self.weaveOp= WeaveAction() - self.tangleOp= TangleAction() + self.loadOp = LoadAction() + self.weaveOp = WeaveAction() + self.tangleOp = TangleAction() # Composite Actions - self.doWeave= ActionSequence( "load and weave", [self.loadOp, self.weaveOp] ) - self.doTangle= ActionSequence( "load and tangle", [self.loadOp, self.tangleOp] ) - self.theAction= ActionSequence( "load, tangle and weave", [self.loadOp, self.tangleOp, self.weaveOp] ) + self.doWeave = ActionSequence("load and weave", [self.loadOp, self.weaveOp]) + self.doTangle = ActionSequence("load and tangle", [self.loadOp, self.tangleOp]) + self.theAction = ActionSequence("load, tangle and weave", [self.loadOp, self.tangleOp, self.weaveOp]) .. .. class:: small - |loz| *Application default options (161)*. Used by: Application Class... (`159`_) + |loz| *Application default options (163)*. Used by: Application Class... (`161`_) The algorithm for parsing the command line parameters uses the built in @@ -7663,68 +8112,72 @@ instances. -.. _`162`: -.. rubric:: Application parse command line (162) = +.. _`164`: +.. rubric:: Application parse command line (164) = .. parsed-literal:: :class: code - def parseArgs( self ): + def parseArgs(self, argv: list[str]) -> argparse.Namespace: p = argparse.ArgumentParser() - p.add\_argument( "-v", "--verbose", dest="verbosity", action="store\_const", const=logging.INFO ) - p.add\_argument( "-s", "--silent", dest="verbosity", action="store\_const", const=logging.WARN ) - p.add\_argument( "-d", "--debug", dest="verbosity", action="store\_const", const=logging.DEBUG ) - p.add\_argument( "-c", "--command", dest="command", action="store" ) - p.add\_argument( "-w", "--weaver", dest="weaver", action="store" ) - p.add\_argument( "-x", "--except", dest="skip", action="store", choices=('w','t') ) - p.add\_argument( "-p", "--permit", dest="permit", action="store" ) - p.add\_argument( "-r", "--reference", dest="reference", action="store", choices=('t', 's') ) - p.add\_argument( "-n", "--linenumbers", dest="tangler\_line\_numbers", action="store\_true" ) - p.add\_argument( "files", nargs='+' ) - config= p.parse\_args( namespace=self.defaults ) - self.expand( config ) + p.add\_argument("-v", "--verbose", dest="verbosity", action="store\_const", const=logging.INFO) + p.add\_argument("-s", "--silent", dest="verbosity", action="store\_const", const=logging.WARN) + p.add\_argument("-d", "--debug", dest="verbosity", action="store\_const", const=logging.DEBUG) + p.add\_argument("-c", "--command", dest="command", action="store") + p.add\_argument("-w", "--weaver", dest="weaver", action="store") + p.add\_argument("-x", "--except", dest="skip", action="store", choices=('w', 't')) + p.add\_argument("-p", "--permit", dest="permit", action="store") + p.add\_argument("-r", "--reference", dest="reference", action="store", choices=('t', 's')) + p.add\_argument("-n", "--linenumbers", dest="tangler\_line\_numbers", action="store\_true") + p.add\_argument("-o", "--output", dest="output", action="store", type=Path) + p.add\_argument("-V", "--Version", action='version', version=f"py-web-tool pyweb.py {\_\_version\_\_}") + p.add\_argument("files", nargs='+', type=Path) + config = p.parse\_args(argv, namespace=self.defaults) + self.expand(config) return config - def expand( self, config ): + def expand(self, config: argparse.Namespace) -> argparse.Namespace: """Translate the argument values from simple text to useful objects. Weaver. Tangler. WebReader. """ - if config.reference == 't': - config.reference\_style = TransitiveReference() - elif config.reference == 's': - config.reference\_style = SimpleReference() - else: - raise Error( "Improper configuration" ) - + match config.reference: + case 't': + config.reference\_style = TransitiveReference() + case 's': + config.reference\_style = SimpleReference() + case \_: + raise Error("Improper configuration") + + # Weaver try: - weaver\_class= weavers[config.weaver.lower()] + weaver\_class = weavers[config.weaver.lower()] except KeyError: module\_name, \_, class\_name = config.weaver.partition('.') weaver\_module = \_\_import\_\_(module\_name) weaver\_class = weaver\_module.\_\_dict\_\_[class\_name] if not issubclass(weaver\_class, Weaver): - raise TypeError( "{0!r} not a subclass of Weaver".format(weaver\_class) ) - config.theWeaver= weaver\_class() + raise TypeError(f"{weaver\_class!r} not a subclass of Weaver") + config.theWeaver = weaver\_class() - config.theTangler= TanglerMake() + # Tangler + config.theTangler = TanglerMake() if config.permit: # save permitted errors, usual case is \`\`-pi\`\` to permit \`\`@i\`\` include errors - config.permitList= [ '{!s}{!s}'.format( config.command, c ) for c in config.permit ] + config.permitList = [f'{config.command!s}{c!s}' for c in config.permit] else: - config.permitList= [] + config.permitList = [] - config.webReader= WebReader() + config.webReader = WebReader() return config - .. .. class:: small - |loz| *Application parse command line (162)*. Used by: Application Class... (`159`_) + |loz| *Application parse command line (164)*. Used by: Application Class... (`161`_) The ``process()`` function uses the current ``Application`` settings @@ -7734,7 +8187,7 @@ to process each file as follows: the parameters required to process the input file. 2. Create a ``Web`` instance, *w* - and set the Web's *sourceFileName* from the WebReader's *fileName*. + and set the Web's *sourceFileName* from the WebReader's *filePath*. 3. Perform the given command, typically a ``ActionSequence``, which does some combination of load, tangle the output files and @@ -7750,46 +8203,45 @@ The re-raising is done so that all exceptions are handled by the outermost main program. -.. _`163`: -.. rubric:: Application class process all files (163) = +.. _`165`: +.. rubric:: Application class process all files (165) = .. parsed-literal:: :class: code - def process( self, config ): - root= logging.getLogger() - root.setLevel( config.verbosity ) - self.logger.debug( "Setting root log level to {!r}".format( - logging.getLevelName(root.getEffectiveLevel()) ) ) + def process(self, config: argparse.Namespace) -> None: + root = logging.getLogger() + root.setLevel(config.verbosity) + self.logger.debug("Setting root log level to %r", logging.getLevelName(root.getEffectiveLevel())) if config.command: - self.logger.debug( "Command character {!r}".format(config.command) ) + self.logger.debug("Command character %r", config.command) if config.skip: - if config.skip.lower().startswith('w'): # not weaving == tangling - self.theAction= self.doTangle - elif config.skip.lower().startswith('t'): # not tangling == weaving - self.theAction= self.doWeave + if config.skip.lower().startswith('w'): # not weaving == tangling + self.theAction = self.doTangle + elif config.skip.lower().startswith('t'): # not tangling == weaving + self.theAction = self.doWeave else: - raise Exception( "Unknown -x option {!r}".format(config.skip) ) + raise Exception(f"Unknown -x option {config.skip!r}") - self.logger.info( "Weaver {!s}".format(config.theWeaver) ) + self.logger.info("Weaver %s", config.theWeaver) for f in config.files: - w= Web() # New, empty web to load and process. - self.logger.info( "{!s} {!r}".format(self.theAction.name, f) ) - config.webFileName= f - self.theAction.web= w - self.theAction.options= config + w = Web() # New, empty web to load and process. + self.logger.info("%s %r", self.theAction.name, f) + config.source\_path = f + self.theAction.web = w + self.theAction.options = config self.theAction() - self.logger.info( self.theAction.summary() ) + self.logger.info(self.theAction.summary()) .. .. class:: small - |loz| *Application class process all files (163)*. Used by: Application Class... (`159`_) + |loz| *Application class process all files (165)*. Used by: Application Class... (`161`_) Logging Setup @@ -7800,8 +8252,8 @@ function in an explicit ``with`` statement that assures that logging is configured and cleaned up politely. -.. _`164`: -.. rubric:: Imports (164) += +.. _`166`: +.. rubric:: Imports (166) += .. parsed-literal:: :class: code @@ -7814,7 +8266,7 @@ configured and cleaned up politely. .. class:: small - |loz| *Imports (164)*. Used by: pyweb.py (`153`_) + |loz| *Imports (166)*. Used by: pyweb.py (`155`_) This has two configuration approaches. If a positional argument is given, @@ -7825,23 +8277,25 @@ A subclass might properly load a dictionary encoded in YAML and use that with ``logging.config.dictConfig``. -.. _`165`: -.. rubric:: Logging Setup (165) = +.. _`167`: +.. rubric:: Logging Setup (167) = .. parsed-literal:: :class: code class Logger: - def \_\_init\_\_( self, dict\_config=None, \*\*kw\_config ): - self.dict\_config= dict\_config - self.kw\_config= kw\_config - def \_\_enter\_\_( self ): + def \_\_init\_\_(self, dict\_config: dict[str, Any] \| None = None, \*\*kw\_config: Any) -> None: + self.dict\_config = dict\_config + self.kw\_config = kw\_config + + def \_\_enter\_\_(self) -> "Logger": if self.dict\_config: - logging.config.dictConfig( self.dict\_config ) + logging.config.dictConfig(self.dict\_config) else: - logging.basicConfig( \*\*self.kw\_config ) + logging.basicConfig(\*\*self.kw\_config) return self - def \_\_exit\_\_( self, \*args ): + + def \_\_exit\_\_(self, \*args: Any) -> Literal[False]: logging.shutdown() return False @@ -7849,7 +8303,7 @@ encoded in YAML and use that with ``logging.config.dictConfig``. .. class:: small - |loz| *Logging Setup (165)*. Used by: pyweb.py (`153`_) + |loz| *Logging Setup (167)*. Used by: pyweb.py (`155`_) Here's a sample logging setup. This creates a simple console handler and @@ -7859,44 +8313,45 @@ It defines the root logger plus two overrides for class loggers that might be used to gather additional information. -.. _`166`: -.. rubric:: Logging Setup (166) += +.. _`168`: +.. rubric:: Logging Setup (168) += .. parsed-literal:: :class: code - log\_config= dict( - version= 1, - disable\_existing\_loggers= False, # Allow pre-existing loggers to work. - handlers= { + log\_config = { + 'version': 1, + 'disable\_existing\_loggers': False, # Allow pre-existing loggers to work. + 'style': '{', + 'handlers': { 'console': { 'class': 'logging.StreamHandler', 'stream': 'ext://sys.stderr', 'formatter': 'basic', }, }, - formatters = { + 'formatters': { 'basic': { 'format': "{levelname}:{name}:{message}", 'style': "{", } }, - root= { 'handlers': ['console'], 'level': logging.INFO, }, + 'root': {'handlers': ['console'], 'level': logging.INFO,}, #For specific debugging support... - loggers= { - # 'RST': { 'level': logging.DEBUG }, - # 'TanglerMake': { 'level': logging.DEBUG }, - # 'WebReader': { 'level': logging.DEBUG }, + 'loggers': { + # 'RST': {'level': logging.DEBUG}, + # 'TanglerMake': {'level': logging.DEBUG}, + # 'WebReader': {'level': logging.DEBUG}, }, - ) + } .. .. class:: small - |loz| *Logging Setup (166)*. Used by: pyweb.py (`153`_) + |loz| *Logging Setup (168)*. Used by: pyweb.py (`155`_) This seems a bit verbose; a separate configuration file might be better. @@ -7918,26 +8373,26 @@ We might also want to parse a logging configuration file, as well as a weaver template configuration file. -.. _`167`: -.. rubric:: Interface Functions (167) = +.. _`169`: +.. rubric:: Interface Functions (169) = .. parsed-literal:: :class: code - def main(): - a= Application() - config= a.parseArgs() + def main(argv: list[str] = sys.argv[1:]) -> None: + a = Application() + config = a.parseArgs(argv) a.process(config) if \_\_name\_\_ == "\_\_main\_\_": - with Logger( log\_config ): - main( ) + with Logger(log\_config): + main() .. .. class:: small - |loz| *Interface Functions (167)*. Used by: pyweb.py (`153`_) + |loz| *Interface Functions (169)*. Used by: pyweb.py (`155`_) This can be extended by doing something like the following. @@ -7953,58 +8408,51 @@ This can be extended by doing something like the following. .. parsed-literal:: import pyweb - class MyWeaver( HTML ): + class MyWeaver(HTML): *Any template changes* pyweb.weavers['myweaver']= MyWeaver() pyweb.main() -This will create a variant on **pyWeb** that will handle a different +This will create a variant on **py-web-tool** that will handle a different weaver via the command-line option ``-w myweaver``. - -.. pyweb/test.w +.. py-web-tool/src/test.w Unit Tests =========== -The ``test`` directory includes ``pyweb_test.w``, which will create a +The ``tests`` directory includes ``pyweb_test.w``, which will create a complete test suite. -This source will weaves a ``pyweb_test.html`` file. See file:test/pyweb_test.html +This source will weaves a ``pyweb_test.html`` file. See `tests/pyweb_test.html `_. This source will tangle several test modules: ``test.py``, ``test_tangler.py``, ``test_weaver.py``, -``test_loader.py`` and ``test_unit.py``. Running the ``test.py`` module will include and -execute all 78 tests. +``test_loader.py``, ``test_unit.py``, and ``test_scripts.py``. + +Use **pytest** to discover and run all 80+ test cases. Here's a script that works out well for running this without disturbing the development environment. The ``PYTHONPATH`` setting is essential to support importing ``pyweb``. .. parsed-literal:: - cd test - python ../pyweb.py pyweb_test.w - PYTHONPATH=.. python test.py + python pyweb.py -o tests tests/pyweb_test.w + PYTHONPATH=$(PWD) pytest Note that the last line really does set an environment variable and run -a program on a single line. +the ``pytest`` tool on a single line. -.. pyweb/additional.w +.. py-web-tool/src/scripts.w -Additional Files -================ +Handy Scripts and Other Files +================================================= Two aditional scripts, ``tangle.py`` and ``weave.py``, are provided as examples -which an be customized. - -The ``README`` and ``setup.py`` files are also an important part of the -distribution as are a ``.nojekyll`` and ``index.html`` that are part of -publishing from GitHub. - -The ``.CSS`` file and ``.conf`` file for RST production are also provided here. +which can be customized and extended. ``tangle.py`` Script --------------------- @@ -8027,45 +8475,50 @@ Note the general flow of this top-level script. a summary. -.. _`168`: -.. rubric:: tangle.py (168) = +.. _`170`: +.. rubric:: tangle.py (170) = .. parsed-literal:: :class: code #!/usr/bin/env python3 """Sample tangle.py script.""" - import pyweb - import logging import argparse - - with pyweb.Logger( pyweb.log\_config ): - logger= logging.getLogger(\_\_file\_\_) - - options = argparse.Namespace( - webFileName= "pyweb.w", - verbosity= logging.INFO, - command= '@', - permitList= ['@i'], - tangler\_line\_numbers= False, - reference\_style = pyweb.SimpleReference(), - theTangler= pyweb.TanglerMake(), - webReader= pyweb.WebReader(), - ) + import logging + from pathlib import Path + import pyweb - w= pyweb.Web() - - for action in LoadAction(), TangleAction(): - action.web= w - action.options= options - action() - logger.info( action.summary() ) + def main(source: Path) -> None: + with pyweb.Logger(pyweb.log\_config): + logger = logging.getLogger(\_\_file\_\_) + + options = argparse.Namespace( + source\_path=source, + output=source.parent, + verbosity=logging.INFO, + command='@', + permitList=['@i'], + tangler\_line\_numbers=False, + reference\_style=pyweb.SimpleReference(), + theTangler=pyweb.TanglerMake(), + webReader=pyweb.WebReader(), + ) + + w = pyweb.Web() + + for action in pyweb.LoadAction(), pyweb.TangleAction(): + action.web = w + action.options = options + action() + logger.info(action.summary()) + if \_\_name\_\_ == "\_\_main\_\_": + main(Path("examples/test\_rst.w")) .. .. class:: small - |loz| *tangle.py (168)*. + |loz| *tangle.py (170)*. ``weave.py`` Script @@ -8078,70 +8531,73 @@ to define a customized set of templates for a different markup language. A customized weaver generally has three parts. -.. _`169`: -.. rubric:: weave.py (169) = +.. _`171`: +.. rubric:: weave.py (171) = .. parsed-literal:: :class: code - |srarr|\ weave.py overheads for correct operation of a script (`170`_) - |srarr|\ weave.py custom weaver definition to customize the Weaver being used (`171`_) - |srarr|\ weaver.py processing: load and weave the document (`172`_) + |srarr|\ weave.py overheads for correct operation of a script (`172`_) + + |srarr|\ weave.py custom weaver definition to customize the Weaver being used (`173`_) + + |srarr|\ weaver.py processing: load and weave the document (`174`_) .. .. class:: small - |loz| *weave.py (169)*. + |loz| *weave.py (171)*. -.. _`170`: -.. rubric:: weave.py overheads for correct operation of a script (170) = +.. _`172`: +.. rubric:: weave.py overheads for correct operation of a script (172) = .. parsed-literal:: :class: code #!/usr/bin/env python3 """Sample weave.py script.""" - import pyweb - import logging import argparse + import logging import string + from pathlib import Path + import pyweb .. .. class:: small - |loz| *weave.py overheads for correct operation of a script (170)*. Used by: weave.py (`169`_) + |loz| *weave.py overheads for correct operation of a script (172)*. Used by: weave.py (`171`_) -.. _`171`: -.. rubric:: weave.py custom weaver definition to customize the Weaver being used (171) = +.. _`173`: +.. rubric:: weave.py custom weaver definition to customize the Weaver being used (173) = .. parsed-literal:: :class: code - class MyHTML( pyweb.HTML ): + class MyHTML(pyweb.HTML): """HTML formatting templates.""" - extension= ".html" + extension = ".html" - cb\_template= string.Template(""" + cb\_template = string.Template("""

    ${fullName} (${seq}) ${concat}

    \\n""")
         
    -        ce\_template= string.Template("""
    +        ce\_template = string.Template("""
             

    ${fullName} (${seq}). ${references}

    \\n""") - fb\_template= string.Template(""" + fb\_template = string.Template("""

    \`\`${fullName}\`\` (${seq}) ${concat}

    \\n""") # Prevent indent
                 
    -        fe\_template= string.Template( """
    + fe\_template = string.Template( """

    ◊ \`\`${fullName}\`\` (${seq}). ${references}

    \\n""") @@ -8150,651 +8606,162 @@ A customized weaver generally has three parts. '${fullName} (${seq})' ) - ref\_template = string.Template( ' Used by ${refList}.' ) + ref\_template = string.Template(' Used by ${refList}.' ) refto\_name\_template = string.Template( '${fullName} (${seq})' ) - refto\_seq\_template = string.Template( '(${seq})' ) + refto\_seq\_template = string.Template('(${seq})') - xref\_head\_template = string.Template( "
    \\n" ) - xref\_foot\_template = string.Template( "
    \\n" ) - xref\_item\_template = string.Template( "
    ${fullName}
    ${refList}
    \\n" ) + xref\_head\_template = string.Template("
    \\n") + xref\_foot\_template = string.Template("
    \\n") + xref\_item\_template = string.Template("
    ${fullName}
    ${refList}
    \\n") - name\_def\_template = string.Template( '•${seq}' ) - name\_ref\_template = string.Template( '${seq}' ) + name\_def\_template = string.Template('•${seq}') + name\_ref\_template = string.Template('${seq}') .. .. class:: small - |loz| *weave.py custom weaver definition to customize the Weaver being used (171)*. Used by: weave.py (`169`_) + |loz| *weave.py custom weaver definition to customize the Weaver being used (173)*. Used by: weave.py (`171`_) -.. _`172`: -.. rubric:: weaver.py processing: load and weave the document (172) = +.. _`174`: +.. rubric:: weaver.py processing: load and weave the document (174) = .. parsed-literal:: :class: code - with pyweb.Logger( pyweb.log\_config ): - logger= logging.getLogger(\_\_file\_\_) - - options = argparse.Namespace( - webFileName= "pyweb.w", - verbosity= logging.INFO, - command= '@', - theWeaver= MyHTML(), - permitList= [], - tangler\_line\_numbers= False, - reference\_style = pyweb.SimpleReference(), - theTangler= pyweb.TanglerMake(), - webReader= pyweb.WebReader(), - ) - - w= pyweb.Web() + def main(source: Path) -> None: + with pyweb.Logger(pyweb.log\_config): + logger = logging.getLogger(\_\_file\_\_) + + options = argparse.Namespace( + source\_path=source, + output=source.parent, + verbosity=logging.INFO, + command='@', + permitList=[], + tangler\_line\_numbers=False, + reference\_style=pyweb.SimpleReference(), + theWeaver=MyHTML(), + webReader=pyweb.WebReader(), + ) + + w = pyweb.Web() + + for action in pyweb.LoadAction(), pyweb.WeaveAction(): + action.web = w + action.options = options + action() + logger.info(action.summary()) - for action in LoadAction(), WeaveAction(): - action.web= w - action.options= options - action() - logger.info( action.summary() ) + if \_\_name\_\_ == "\_\_main\_\_": + main(Path("examples/test\_rst.w")) .. .. class:: small - |loz| *weaver.py processing: load and weave the document (172)*. Used by: weave.py (`169`_) + |loz| *weaver.py processing: load and weave the document (174)*. Used by: weave.py (`171`_) -The ``setup.py`` and ``MANIFEST.in`` files --------------------------------------------- -In order to support a pleasant installation, the ``setup.py`` file is helpful. +.. py-web-tool/src/todo.w + +To Do +======= -.. _`173`: -.. rubric:: setup.py (173) = -.. parsed-literal:: - :class: code +1. Rename the module from ``pyweb`` to ``pylpweb`` to avoid name squatting issues. + Rename the project from ``py-web-tool`` to ``py-lpweb``. + +2. Switch to jinja templates. - #!/usr/bin/env python3 - """Setup for pyWeb.""" + - See the ``weave.py`` example. + Defining templates in the source removes any need for a command-line option. A silly optimization. + Setting the "command character" to something other than ``@`` can be done in the configuration, too. + + - With Jinjda templates can be provided via + a Jinja configuration (there are many choices.) By stepping away from the ``string.Template``, + we can incorporate list-processing ``{%for%}...{%endfor%}`` construct that + pushes some processing into the template. + +#. Separate TOML-based logging configuration file would be helpful. + Must be separate from template configuration. + +#. Rethink the presentation. Are |loz| and |srarr| REALLY necessary? + Can we use ◊ and → now that Unicode is more universal? + And why ``'\N{LOZENGE}'``? There's a nice ``'\N{END OF PROOF}'`` symbol we could use. + Remove the unused ``header``, ``docBegin()``, and ``docEnd()``. + +#. Tangling can include non-woven content. More usefully, Weaving can exclude some chunks. + The use case is a book chapter with test cases that are **not** woven into the text. + Add an option to define tangle-only chunks that are NOT woven into the final document. + +#. Update the ``-indent`` option on @d chunks to accept a numeric argument with the + specific indentation value. This becomes a kind of "noindent" with a given + value. The ``-noindent`` would then be the same as ``-indent 0``. + Currently, `-indent` and `-noindent` are true/false flags. - from distutils.core import setup +#. We might want to decompose the ``impl.w`` file: it's huge. - setup(name='pyweb', - version='3.0', - description='pyWeb 3.0: Yet Another Literate Programming Tool', - author='S. Lott', - author\_email='s\_lott@yahoo.com', - url='http://slott-softwarearchitect.blogspot.com/', - py\_modules=['pyweb'], - classifiers=[ - 'Intended Audience :: Developers', - 'Topic :: Documentation', - 'Topic :: Software Development :: Documentation', - 'Topic :: Text Processing :: Markup', - ] - ) +#. We might want to interleave code and test into a document that presents both + side-by-side. We can route to multiple files. + It's a little awkward to create tangled files in multiple directories; + We'd have to use ``../tests/whatever.py``, **assuming** we were always using ``-o src``. -.. +#. Fix name definition order. There's no **good** reason why a full name must + be first and elided names defined later. - .. class:: small +#. Offer a basic XHTML template that uses ``CDATA`` sections instead of quoting. + Does require the standard quoting for the ``CDATA`` end tag. - |loz| *setup.py (173)*. +#. The ``createUsedBy()`` method can be done incrementally by + accumulating a list of forward references to chunks; as each + new chunk is added, any references to the chunk are removed from + the forward references list, and a call is made to the Web's + setUsage method. References backward to already existing chunks + are easily resolved with a simple lookup. + +#. Note that the overall ``Web`` is a bit like a ``NamedChunk`` that contains ``Chunks``. + This similarity could be factored out. + While this will create a more proper **Composition** pattern implementation, it + leads to the question of why nest ``@d`` or ``@o`` chunks in the first place? -In order build a source distribution kit the ``python3 setup.py sdist`` requires a -``MANIFEST``. We can either list all files or provide a ``MANIFEST.in`` -that specifies additional rules. -We use a simple inclusion to augment the default manifest rules. +.. py-web-tool/src/done.w +Change Log +=========== -.. _`174`: -.. rubric:: MANIFEST.in (174) = -.. parsed-literal:: - :class: code +Changes for 3.1 - include \*.w \*.css \*.html \*.conf \*.rst - include test/\*.w test/\*.css test/\*.html test/\*.conf test/\*.py - include jedit/\*.xml +- Change to Python 3.10 as the supported version. -.. +- Add type hints, f-strings, ``pathlib``, ``abc.ABC``. - .. class:: small +- Replace some complex ``elif`` blocks with ``match`` statements. - |loz| *MANIFEST.in (174)*. +- Use **pytest** as a test runner. +- Add a ``Makefile``, ``pyproject.toml``, ``requirements.txt`` and ``requirements-dev.txt``. -The ``README`` file ---------------------- +- Implement ``-o dir`` option to write output to a directory of choice, simplifying **tox** setup. -Here's the README file. +- Add ``bootstrap`` directory with a snapshot of a previous working release to simplify development. +- Add Test cases for ``weave.py`` and ``tangle.py`` -.. _`175`: -.. rubric:: README (175) = -.. parsed-literal:: - :class: code +- Replace hand-build mock classes with ``unittest.mock.Mock`` objects - pyWeb 3.0: In Python, Yet Another Literate Programming Tool - - Literate programming is an attempt to reconcile the opposing needs - of clear presentation to people with the technical issues of - creating code that will work with our current set of tools. - - Presentation to people requires extensive and sophisticated typesetting - techniques. Further, the "narrative arc" of a presentation may not - follow the source code as layed out for the compiler. - - pyWeb is a literate programming tool based on Knuth's Web to combine the actions - of weaving a document with tangling source files. - It is independent of any particular document markup or source language. - Is uses a simple set of markup tags to define chunks of code and - documentation. - - The \`\`pyweb.w\`\` file is the source for the various pyweb module and script files. - The various source code files are created by applying a - tangle operation to the \`\`.w\`\` file. The final documentation is created by - applying a weave operation to the \`\`.w\`\` file. - - Installation - ------------- - - :: - - python3 setup.py install - - This will install the pyweb module. - - Document production - -------------------- - - The supplied documentation uses RST markup and requires docutils. - - :: - - python3 -m pyweb pyweb.w - rst2html.py pyweb.rst pyweb.html - - Authoring - --------- - - The pyweb document describes the simple markup used to define code chunks - and assemble those code chunks into a coherent document as well as working code. - - If you're a JEdit user, the \`\`jedit\`\` directory can be used - to configure syntax highlighting that includes PyWeb and RST. - - Operation - --------- - - You can then run pyweb with - - :: - - python3 -m pyweb pyweb.w - - This will create the various output files from the source .w file. - - - \`\`pyweb.html\`\` is the final woven document. - - - \`\`pyweb.py\`\`, \`\`tangle.py\`\`, \`\`weave.py\`\`, \`\`README\`\`, \`\`setup.py\`\` and \`\`MANIFEST.in\`\` - \`\`.nojekyll\`\` and \`\`index.html\`\` are tangled output files. - - Testing - ------- - - The test directory includes \`\`pyweb\_test.w\`\`, which will create a - complete test suite. - - This weaves a \`\`pyweb\_test.html\`\` file. - - This tangles several test modules: \`\`test.py\`\`, \`\`test\_tangler.py\`\`, \`\`test\_weaver.py\`\`, - \`\`test\_loader.py\`\` and \`\`test\_unit.py\`\`. Running the \`\`test.py\`\` module will include and - execute all tests. - - :: - - cd test - python3 -m pyweb pyweb\_test.w - PYTHONPATH=.. python3 test.py - rst2html.py pyweb\_test.rst pyweb\_test.html - - - -.. +- Separate the projec into ``src``, ``tests``, ``examples``. Cleanup ``Makefile``, ``pyproject.toml``, etc. - .. class:: small - - |loz| *README (175)*. - - -The HTML Support Files ----------------------- - -To get the RST to look good, there are some additional files. - -``docutils.conf`` defines the CSS files to use. -The default CSS file (stylesheet-path) may need to be customized for your -installation of docutils. - - -.. _`176`: -.. rubric:: docutils.conf (176) = -.. parsed-literal:: - :class: code - - # docutils.conf - - [html4css1 writer] - stylesheet-path: /Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages/docutils/writers/html4css1/html4css1.css, - page-layout.css - syntax-highlight: long +- Silence the ERROR-level logging during testing. -.. - - .. class:: small - - |loz| *docutils.conf (176)*. - - -``page-layout.css`` This tweaks one CSS to be sure that -the resulting HTML pages are easier to read. - - -.. _`177`: -.. rubric:: page-layout.css (177) = -.. parsed-literal:: - :class: code - - /\* Page layout tweaks \*/ - div.document { width: 7in; } - .small { font-size: smaller; } - .code - { - color: #101080; - display: block; - border-color: black; - border-width: thin; - border-style: solid; - background-color: #E0FFFF; - /\*#99FFFF\*/ - padding: 0 0 0 1%; - margin: 0 6% 0 6%; - text-align: left; - font-size: smaller; - } - -.. - - .. class:: small - - |loz| *page-layout.css (177)*. - - -Yes, this creates a (nearly) empty file for use by GitHub. There's a small -bug in ``NamedChunk.tangle()`` that prevents handling zero-length text. - - -.. _`178`: -.. rubric:: .nojekyll (178) = -.. parsed-literal:: - :class: code - - - -.. - - .. class:: small - - |loz| *.nojekyll (178)*. - - -Finally, an ``index.html`` to redirect GitHub to the ``pyweb.html`` file. - - -.. _`179`: -.. rubric:: index.html (179) = -.. parsed-literal:: - :class: code - - - - - Redirect - - - Sorry, you should have been redirected pyweb.html. - - -.. - - .. class:: small - - |loz| *index.html (179)*. - - -.. pyweb/jedit.w - -JEdit Configuration -==================== - -Here's the ``pyweb.xml`` file that you'll need to configure -JEdit so that it properly highlights your PyWeb commands. - -We'll define the overall properties plus two sets of rules. - -.. _`180`: -.. rubric:: jedit/pyweb.xml (180) = -.. parsed-literal:: - :class: code - - - - - - |srarr|\ props for JEdit mode (`181`_) - |srarr|\ rules for JEdit PyWeb and RST (`182`_) - |srarr|\ rules for JEdit PyWeb XML-Like Constructs (`183`_) - - -.. - - .. class:: small - - |loz| *jedit/pyweb.xml (180)*. - - -Here are some properties to define RST constructs to JEdit - -.. _`181`: -.. rubric:: props for JEdit mode (181) = -.. parsed-literal:: - :class: code - - - - - - - - - -.. - - .. class:: small - - |loz| *props for JEdit mode (181)*. Used by: jedit/pyweb.xml (`180`_) - - -Here are some rules to define PyWeb and RST constructs to JEdit. - - -.. _`182`: -.. rubric:: rules for JEdit PyWeb and RST (182) = -.. parsed-literal:: - :class: code - - - - - - \_\_ - .. \_ - - - ={3,} - -{3,} - ~{3,} - #{3,} - "{3,} - \\^{3,} - \\+{3,} - \\\*{3,} - - - \\.\\.\\s\\\|[^\|]+\\\| - - - \\\|[^\|]+\\\| - - - \\.\\.\\s[A-z][A-z0-9-\_]+:: - - - \\\*\\\*[^\*]+\\\*\\\* - - - \\\*[^\\s\*][^\*]\*\\\* - - - .. - - - \`[A-z0-9]+[^\`]+\`\_{1,2} - - - \\[[0-9]+\\]\_ - - - \\[#[A-z0-9\_]\*\\]\_ - - - [\*]\_ - - - \\[[A-z][A-z0-9\_-]\*\\]\_ - - - - - \`\` - \`\` - - - - - @d - @o - - - @{ - @} - - - - \` - \` - - - \`{3,} - - - :[A-z][A-z0-9 =\\s\\t\_]\*: - - - \\+-[+-]+ - \\+=[+=]+ - - - -.. - - .. class:: small - - |loz| *rules for JEdit PyWeb and RST (182)*. Used by: jedit/pyweb.xml (`180`_) - - -Here are some additional rules to define PyWeb constructs to JEdit -that look like XML. - - -.. _`183`: -.. rubric:: rules for JEdit PyWeb XML-Like Constructs (183) = -.. parsed-literal:: - :class: code - - - - - @< - @> - - - -.. - - .. class:: small - - |loz| *rules for JEdit PyWeb XML-Like Constructs (183)*. Used by: jedit/pyweb.xml (`180`_) - - -Additionally, you'll want to update the JEdit catalog. - -.. parsed-literal:: - - - - - - - - - - -.. End - - -.. pyweb/todo.w - -To Do -======= - -1. Fix name definition order. There's no good reason why a full name should - be first and elided names defined later. - -2. Silence the logging during testing. - -#. Add a JSON-based configuration file to configure templates. - - - See the ``weave.py`` example. - This removes any need for a weaver command-line option; its defined within the source. - Also, setting the command character can be done in this configuration, too. - - - An alternative is to get markup templates from a "header" section in the ``.w`` file. - - To support reuse over multiple projects, a header could be included with ``@i``. - The downside is that we have a lot of variable = value syntax that makes it - more like a properties file than a ``.w`` syntax file. It seems needless to invent - a lot of new syntax just for configuration. - -#. JSON-based logging configuration file would be helpful. - Should be separate from template configuration. - -#. We might want to decompose the ``impl.w`` file: it's huge. - -#. We might want to interleave code and test into a document that presents both - side-by-side. They get routed to different output files. - -#. Add a ``@h`` "header goes here" command to allow weaving any **pyWeb** required addons to - a LaTeX header, HTML header or RST header. - These are extra ``.. include::``, ``\\usepackage{fancyvrb}`` or maybe an HTML CSS reference - that come from **pyWeb** and need to be folded into otherwise boilerplate documents. - -#. Update the ``-indent`` option to accept a numeric argument with the - specific indentation value. This becomes a kind of "noindent" with a given - value. The ``-noindent`` would then be the same as ``-indent 0``. - -#. Offer a basic XHTML template that uses ``CDATA`` sections instead of quoting. - Does require the standard quoting for the ``CDATA`` end tag. - -#. The ``createUsedBy()`` method can be done incrementally by - accumulating a list of forward references to chunks; as each - new chunk is added, any references to the chunk are removed from - the forward references list, and a call is made to the Web's - setUsage method. References backward to already existing chunks - are easily resolved with a simple lookup. - -#. Note that the overall ``Web`` is a bit like a ``NamedChunk`` that contains ``Chunks``. - This similarity could be factored out. - While this will create a more proper **Composition** pattern implementation, it - leads to the question of why nest ``@d`` or ``@o`` chunks in the first place? - -Other Thoughts ----------------- - -There are two possible projects that might prove useful. - -- Jinja2 for better templates. - -- pyYAML for slightly cleaner encoding of logging configuration - or other configuration. - -There are advantages and disadvantages to depending on other projects. -The disadvantage is a (very low, but still present) barrier to adoption. -The advantage of adding these two projects might be some simplification. - - -.. pyweb/done.w - -Change Log -=========== +- Clean up the examples Changes for 3.0 @@ -8933,28 +8900,12 @@ Files ------ -:.nojekyll: - |srarr|\ (`178`_) -:MANIFEST.in: - |srarr|\ (`174`_) -:README: - |srarr|\ (`175`_) -:docutils.conf: - |srarr|\ (`176`_) -:index.html: - |srarr|\ (`179`_) -:jedit/pyweb.xml: - |srarr|\ (`180`_) -:page-layout.css: - |srarr|\ (`177`_) :pyweb.py: - |srarr|\ (`153`_) -:setup.py: - |srarr|\ (`173`_) + |srarr|\ (`155`_) :tangle.py: - |srarr|\ (`168`_) + |srarr|\ (`170`_) :weave.py: - |srarr|\ (`169`_) + |srarr|\ (`171`_) @@ -8963,263 +8914,263 @@ Macros :Action call method actually does the real work: + |srarr|\ (`140`_) +:Action class hierarchy - used to describe actions of the application: |srarr|\ (`138`_) -:Action class hierarchy - used to describe basic actions of the application: - |srarr|\ (`136`_) :Action final summary of what was done: - |srarr|\ (`139`_) + |srarr|\ (`141`_) :Action superclass has common features of all actions: - |srarr|\ (`137`_) + |srarr|\ (`139`_) :ActionSequence append adds a new action to the sequence: - |srarr|\ (`142`_) + |srarr|\ (`144`_) :ActionSequence call method delegates the sequence of ations: - |srarr|\ (`141`_) + |srarr|\ (`143`_) :ActionSequence subclass that holds a sequence of other actions: - |srarr|\ (`140`_) + |srarr|\ (`142`_) :ActionSequence summary summarizes each step: - |srarr|\ (`143`_) + |srarr|\ (`145`_) :Application Class: - |srarr|\ (`159`_) |srarr|\ (`160`_) + |srarr|\ (`161`_) |srarr|\ (`162`_) :Application class process all files: - |srarr|\ (`163`_) + |srarr|\ (`165`_) :Application default options: - |srarr|\ (`161`_) + |srarr|\ (`163`_) :Application parse command line: - |srarr|\ (`162`_) + |srarr|\ (`164`_) :Base Class Definitions: |srarr|\ (`1`_) :Chunk add to the web: - |srarr|\ (`55`_) + |srarr|\ (`56`_) :Chunk append a command: - |srarr|\ (`53`_) -:Chunk append text: |srarr|\ (`54`_) -:Chunk class: - |srarr|\ (`52`_) +:Chunk append text: + |srarr|\ (`55`_) +:Chunk base class for anonymous chunks of the file: + |srarr|\ (`53`_) :Chunk class hierarchy - used to describe input chunks: - |srarr|\ (`51`_) + |srarr|\ (`52`_) :Chunk examination: starts with, matches pattern: - |srarr|\ (`57`_) + |srarr|\ (`59`_) :Chunk generate references from this Chunk: - |srarr|\ (`58`_) + |srarr|\ (`60`_) :Chunk indent adjustments: - |srarr|\ (`62`_) + |srarr|\ (`64`_) :Chunk references to this Chunk: - |srarr|\ (`59`_) + |srarr|\ (`61`_) :Chunk superclass make Content definition: - |srarr|\ (`56`_) + |srarr|\ (`57`_) :Chunk tangle this Chunk into a code file: - |srarr|\ (`61`_) + |srarr|\ (`63`_) :Chunk weave this Chunk into the documentation: - |srarr|\ (`60`_) + |srarr|\ (`62`_) :CodeCommand class to contain a program source code block: - |srarr|\ (`81`_) + |srarr|\ (`83`_) :Command analysis features: starts-with and Regular Expression search: - |srarr|\ (`78`_) + |srarr|\ (`80`_) :Command class hierarchy - used to describe individual commands: - |srarr|\ (`76`_) + |srarr|\ (`78`_) :Command superclass: - |srarr|\ (`77`_) -:Command tangle and weave functions: |srarr|\ (`79`_) +:Command tangle and weave functions: + |srarr|\ (`81`_) :Emitter class hierarchy - used to control output files: |srarr|\ (`2`_) :Emitter core open, close and write: - |srarr|\ (`4`_) + |srarr|\ (`5`_) :Emitter doClose, to be overridden by subclasses: - |srarr|\ (`6`_) + |srarr|\ (`7`_) :Emitter doOpen, to be overridden by subclasses: - |srarr|\ (`5`_) + |srarr|\ (`6`_) :Emitter indent control: set, clear and reset: - |srarr|\ (`10`_) + |srarr|\ (`11`_) :Emitter superclass: - |srarr|\ (`3`_) + |srarr|\ (`4`_) :Emitter write a block of code: - |srarr|\ (`7`_) |srarr|\ (`8`_) |srarr|\ (`9`_) + |srarr|\ (`8`_) |srarr|\ (`9`_) |srarr|\ (`10`_) :Error class - defines the errors raised: - |srarr|\ (`94`_) + |srarr|\ (`96`_) :FileXrefCommand class for an output file cross-reference: - |srarr|\ (`83`_) + |srarr|\ (`85`_) :HTML code chunk begin: - |srarr|\ (`33`_) -:HTML code chunk end: |srarr|\ (`34`_) -:HTML output file begin: +:HTML code chunk end: |srarr|\ (`35`_) -:HTML output file end: +:HTML output file begin: |srarr|\ (`36`_) +:HTML output file end: + |srarr|\ (`37`_) :HTML reference to a chunk: - |srarr|\ (`39`_) + |srarr|\ (`40`_) :HTML references summary at the end of a chunk: - |srarr|\ (`37`_) + |srarr|\ (`38`_) :HTML short references summary at the end of a chunk: - |srarr|\ (`42`_) + |srarr|\ (`43`_) :HTML simple cross reference markup: - |srarr|\ (`40`_) + |srarr|\ (`41`_) :HTML subclass of Weaver: - |srarr|\ (`31`_) |srarr|\ (`32`_) + |srarr|\ (`32`_) |srarr|\ (`33`_) :HTML write a line of code: - |srarr|\ (`38`_) + |srarr|\ (`39`_) :HTML write user id cross reference line: - |srarr|\ (`41`_) + |srarr|\ (`42`_) :Imports: - |srarr|\ (`11`_) |srarr|\ (`47`_) |srarr|\ (`96`_) |srarr|\ (`124`_) |srarr|\ (`131`_) |srarr|\ (`133`_) |srarr|\ (`154`_) |srarr|\ (`158`_) |srarr|\ (`164`_) + |srarr|\ (`3`_) |srarr|\ (`12`_) |srarr|\ (`48`_) |srarr|\ (`58`_) |srarr|\ (`98`_) |srarr|\ (`124`_) |srarr|\ (`129`_) |srarr|\ (`132`_) |srarr|\ (`134`_) |srarr|\ (`156`_) |srarr|\ (`160`_) |srarr|\ (`166`_) :Interface Functions: - |srarr|\ (`167`_) + |srarr|\ (`169`_) :LaTeX code chunk begin: - |srarr|\ (`24`_) -:LaTeX code chunk end: |srarr|\ (`25`_) -:LaTeX file output begin: +:LaTeX code chunk end: |srarr|\ (`26`_) -:LaTeX file output end: +:LaTeX file output begin: |srarr|\ (`27`_) +:LaTeX file output end: + |srarr|\ (`28`_) :LaTeX reference to a chunk: - |srarr|\ (`30`_) + |srarr|\ (`31`_) :LaTeX references summary at the end of a chunk: - |srarr|\ (`28`_) + |srarr|\ (`29`_) :LaTeX subclass of Weaver: - |srarr|\ (`23`_) + |srarr|\ (`24`_) :LaTeX write a line of code: - |srarr|\ (`29`_) + |srarr|\ (`30`_) :LoadAction call method loads the input files: - |srarr|\ (`151`_) + |srarr|\ (`153`_) :LoadAction subclass loads the document web: - |srarr|\ (`150`_) -:LoadAction summary provides lines read: |srarr|\ (`152`_) +:LoadAction summary provides lines read: + |srarr|\ (`154`_) :Logging Setup: - |srarr|\ (`165`_) |srarr|\ (`166`_) + |srarr|\ (`167`_) |srarr|\ (`168`_) :MacroXrefCommand class for a named chunk cross-reference: - |srarr|\ (`84`_) + |srarr|\ (`86`_) :NamedChunk add to the web: - |srarr|\ (`65`_) -:NamedChunk class: - |srarr|\ (`63`_) |srarr|\ (`68`_) -:NamedChunk tangle into the source file: |srarr|\ (`67`_) +:NamedChunk class for defined names: + |srarr|\ (`65`_) |srarr|\ (`70`_) +:NamedChunk tangle into the source file: + |srarr|\ (`69`_) :NamedChunk user identifiers set and get: - |srarr|\ (`64`_) -:NamedChunk weave into the documentation: |srarr|\ (`66`_) +:NamedChunk weave into the documentation: + |srarr|\ (`68`_) :NamedDocumentChunk class: - |srarr|\ (`73`_) -:NamedDocumentChunk tangle: |srarr|\ (`75`_) +:NamedDocumentChunk tangle: + |srarr|\ (`77`_) :NamedDocumentChunk weave: - |srarr|\ (`74`_) + |srarr|\ (`76`_) :Option Parser class - locates optional values on commands: - |srarr|\ (`134`_) |srarr|\ (`135`_) + |srarr|\ (`135`_) |srarr|\ (`136`_) |srarr|\ (`137`_) :OutputChunk add to the web: - |srarr|\ (`70`_) + |srarr|\ (`72`_) :OutputChunk class: - |srarr|\ (`69`_) + |srarr|\ (`71`_) :OutputChunk tangle: - |srarr|\ (`72`_) + |srarr|\ (`74`_) :OutputChunk weave: - |srarr|\ (`71`_) + |srarr|\ (`73`_) :Overheads: - |srarr|\ (`155`_) |srarr|\ (`156`_) |srarr|\ (`157`_) + |srarr|\ (`157`_) |srarr|\ (`158`_) |srarr|\ (`159`_) :RST subclass of Weaver: - |srarr|\ (`22`_) + |srarr|\ (`23`_) :Reference class hierarchy - strategies for references to a chunk: - |srarr|\ (`91`_) |srarr|\ (`92`_) |srarr|\ (`93`_) + |srarr|\ (`93`_) |srarr|\ (`94`_) |srarr|\ (`95`_) :ReferenceCommand class for chunk references: - |srarr|\ (`86`_) -:ReferenceCommand refers to a chunk: |srarr|\ (`88`_) +:ReferenceCommand refers to a chunk: + |srarr|\ (`90`_) :ReferenceCommand resolve a referenced chunk name: - |srarr|\ (`87`_) + |srarr|\ (`89`_) :ReferenceCommand tangle a referenced chunk: - |srarr|\ (`90`_) + |srarr|\ (`92`_) :ReferenceCommand weave a reference to a chunk: - |srarr|\ (`89`_) + |srarr|\ (`91`_) :TangleAction call method does tangling of the output files: - |srarr|\ (`148`_) + |srarr|\ (`150`_) :TangleAction subclass initiates the tangle action: - |srarr|\ (`147`_) -:TangleAction summary method provides total lines tangled: |srarr|\ (`149`_) +:TangleAction summary method provides total lines tangled: + |srarr|\ (`151`_) :Tangler code chunk begin: - |srarr|\ (`45`_) -:Tangler code chunk end: |srarr|\ (`46`_) +:Tangler code chunk end: + |srarr|\ (`47`_) :Tangler doOpen, and doClose overrides: - |srarr|\ (`44`_) + |srarr|\ (`45`_) :Tangler subclass of Emitter to create source files with no markup: - |srarr|\ (`43`_) + |srarr|\ (`44`_) :TanglerMake doClose override, comparing temporary to original: - |srarr|\ (`50`_) + |srarr|\ (`51`_) :TanglerMake doOpen override, using a temporary file: - |srarr|\ (`49`_) + |srarr|\ (`50`_) :TanglerMake subclass which is make-sensitive: - |srarr|\ (`48`_) + |srarr|\ (`49`_) :TextCommand class to contain a document text block: - |srarr|\ (`80`_) + |srarr|\ (`82`_) :Tokenizer class - breaks input into tokens: - |srarr|\ (`132`_) + |srarr|\ (`133`_) :UserIdXrefCommand class for a user identifier cross-reference: - |srarr|\ (`85`_) + |srarr|\ (`87`_) :WeaveAction call method to pick the language: - |srarr|\ (`145`_) + |srarr|\ (`147`_) :WeaveAction subclass initiates the weave action: - |srarr|\ (`144`_) -:WeaveAction summary of language choice: |srarr|\ (`146`_) +:WeaveAction summary of language choice: + |srarr|\ (`148`_) :Weaver code chunk begin-end: - |srarr|\ (`17`_) + |srarr|\ (`18`_) :Weaver cross reference output methods: - |srarr|\ (`20`_) |srarr|\ (`21`_) + |srarr|\ (`21`_) |srarr|\ (`22`_) :Weaver doOpen, doClose and addIndent overrides: - |srarr|\ (`13`_) + |srarr|\ (`14`_) :Weaver document chunk begin-end: - |srarr|\ (`15`_) + |srarr|\ (`16`_) :Weaver file chunk begin-end: - |srarr|\ (`18`_) + |srarr|\ (`19`_) :Weaver quoted characters: - |srarr|\ (`14`_) + |srarr|\ (`15`_) :Weaver reference command output: - |srarr|\ (`19`_) + |srarr|\ (`20`_) :Weaver reference summary, used by code chunk and file chunk: - |srarr|\ (`16`_) + |srarr|\ (`17`_) :Weaver subclass of Emitter to create documentation: - |srarr|\ (`12`_) + |srarr|\ (`13`_) :Web Chunk check reference counts are all one: - |srarr|\ (`105`_) + |srarr|\ (`107`_) :Web Chunk cross reference methods: - |srarr|\ (`104`_) |srarr|\ (`106`_) |srarr|\ (`107`_) |srarr|\ (`108`_) + |srarr|\ (`106`_) |srarr|\ (`108`_) |srarr|\ (`109`_) |srarr|\ (`110`_) :Web Chunk name resolution methods: - |srarr|\ (`102`_) |srarr|\ (`103`_) + |srarr|\ (`104`_) |srarr|\ (`105`_) :Web add a named macro chunk: - |srarr|\ (`100`_) + |srarr|\ (`102`_) :Web add an anonymous chunk: - |srarr|\ (`99`_) -:Web add an output file definition chunk: |srarr|\ (`101`_) +:Web add an output file definition chunk: + |srarr|\ (`103`_) :Web add full chunk names, ignoring abbreviated names: - |srarr|\ (`98`_) + |srarr|\ (`100`_) :Web class - describes the overall "web" of chunks: - |srarr|\ (`95`_) -:Web construction methods used by Chunks and WebReader: |srarr|\ (`97`_) +:Web construction methods used by Chunks and WebReader: + |srarr|\ (`99`_) :Web determination of the language from the first chunk: - |srarr|\ (`111`_) + |srarr|\ (`113`_) :Web tangle the output files: - |srarr|\ (`112`_) + |srarr|\ (`114`_) :Web weave the output document: - |srarr|\ (`113`_) + |srarr|\ (`115`_) :WebReader class - parses the input file, building the Web structure: - |srarr|\ (`114`_) + |srarr|\ (`116`_) :WebReader command literals: - |srarr|\ (`130`_) + |srarr|\ (`131`_) :WebReader handle a command string: - |srarr|\ (`115`_) |srarr|\ (`127`_) + |srarr|\ (`117`_) |srarr|\ (`127`_) :WebReader load the web: - |srarr|\ (`129`_) + |srarr|\ (`130`_) :WebReader location in the input stream: |srarr|\ (`128`_) :XrefCommand superclass for all cross-reference commands: - |srarr|\ (`82`_) + |srarr|\ (`84`_) :add a reference command to the current chunk: |srarr|\ (`123`_) :add an expression command to the current chunk: @@ -9227,35 +9178,25 @@ Macros :assign user identifiers to the current chunk: |srarr|\ (`122`_) :collect all user identifiers from a given map into ux: - |srarr|\ (`109`_) + |srarr|\ (`111`_) :double at-sign replacement, append this character to previous TextCommand: |srarr|\ (`126`_) :find user identifier usage and update ux from the given map: - |srarr|\ (`110`_) + |srarr|\ (`112`_) :finish a chunk, start a new Chunk adding it to the web: - |srarr|\ (`120`_) -:import another file: - |srarr|\ (`119`_) -:major commands segment the input into separate Chunks: - |srarr|\ (`116`_) -:minor commands add Commands to the current Chunk: |srarr|\ (`121`_) -:props for JEdit mode: - |srarr|\ (`181`_) -:rules for JEdit PyWeb XML-Like Constructs: - |srarr|\ (`183`_) -:rules for JEdit PyWeb and RST: - |srarr|\ (`182`_) +:include another file: + |srarr|\ (`120`_) :start a NamedChunk or NamedDocumentChunk, adding it to the web: - |srarr|\ (`118`_) + |srarr|\ (`119`_) :start an OutputChunk, adding it to the web: - |srarr|\ (`117`_) + |srarr|\ (`118`_) :weave.py custom weaver definition to customize the Weaver being used: - |srarr|\ (`171`_) + |srarr|\ (`173`_) :weave.py overheads for correct operation of a script: - |srarr|\ (`170`_) -:weaver.py processing: load and weave the document: |srarr|\ (`172`_) +:weaver.py processing: load and weave the document: + |srarr|\ (`174`_) @@ -9264,239 +9205,247 @@ User Identifiers :Action: - [`137`_] `140`_ `144`_ `147`_ `150`_ + [`139`_] `142`_ `144`_ `146`_ `149`_ `152`_ :ActionSequence: - [`140`_] `161`_ + [`142`_] `163`_ :Application: - [`159`_] `167`_ + [`161`_] `169`_ :Chunk: - [`52`_] `58`_ `63`_ `90`_ `95`_ `104`_ `119`_ `120`_ `123`_ `129`_ + `16`_ `17`_ `18`_ `19`_ `46`_ `47`_ [`53`_] `59`_ `60`_ `65`_ `79`_ `88`_ `92`_ `93`_ `94`_ `95`_ `97`_ `101`_ `102`_ `103`_ `105`_ `106`_ `110`_ `116`_ `120`_ `121`_ `123`_ `130`_ :CodeCommand: - `63`_ [`81`_] + `65`_ [`83`_] :Command: - `53`_ [`77`_] `80`_ `82`_ `86`_ `163`_ + `53`_ `54`_ `55`_ `57`_ `65`_ `75`_ [`79`_] `82`_ `84`_ `88`_ `165`_ :Emitter: - [`3`_] `12`_ `43`_ + [`4`_] `5`_ `13`_ `44`_ :Error: - `58`_ `61`_ `67`_ `75`_ `82`_ `90`_ [`94`_] `100`_ `102`_ `103`_ `113`_ `118`_ `119`_ `125`_ `135`_ `145`_ `148`_ `151`_ `162`_ + `60`_ `63`_ `69`_ `77`_ `84`_ `92`_ [`96`_] `102`_ `104`_ `105`_ `115`_ `119`_ `120`_ `125`_ `137`_ `147`_ `150`_ `153`_ `164`_ :FileXrefCommand: - [`83`_] `121`_ + [`85`_] `117`_ :HTML: - `31`_ [`32`_] `111`_ `160`_ `171`_ + `32`_ [`33`_] `113`_ `162`_ `173`_ :LaTeX: - [`23`_] `111`_ `160`_ + [`24`_] `113`_ `162`_ :LoadAction: - [`150`_] `161`_ `168`_ `172`_ + [`152`_] `163`_ `170`_ `174`_ :MacroXrefCommand: - [`84`_] `121`_ + [`86`_] `117`_ :NamedChunk: - [`63`_] `68`_ `69`_ `73`_ `118`_ + `59`_ [`65`_] `70`_ `71`_ `75`_ `119`_ :NamedDocumentChunk: - [`73`_] `118`_ + [`75`_] `119`_ :OutputChunk: - [`69`_] `117`_ + [`71`_] `118`_ +:Path: + [`3`_] `4`_ `5`_ `53`_ `97`_ `114`_ `116`_ `120`_ `130`_ `163`_ `164`_ `170`_ `172`_ `174`_ :ReferenceCommand: - [`86`_] `123`_ + [`88`_] `123`_ :TangleAction: - [`147`_] `161`_ `168`_ + [`149`_] `163`_ `170`_ :Tangler: - `3`_ [`43`_] `48`_ `162`_ + `4`_ [`44`_] `49`_ `63`_ `64`_ `69`_ `70`_ `74`_ `77`_ `81`_ `82`_ `83`_ `84`_ `92`_ `114`_ `164`_ :TanglerMake: - [`48`_] `162`_ `166`_ `168`_ `172`_ + [`49`_] `164`_ `168`_ `170`_ :TextCommand: - `54`_ `56`_ `67`_ `73`_ [`80`_] `81`_ + `55`_ `57`_ `69`_ `75`_ [`82`_] `83`_ :Tokenizer: - `129`_ [`132`_] + `116`_ `130`_ [`133`_] :UserIdXrefCommand: - [`85`_] `121`_ + [`87`_] `117`_ :WeaveAction: - [`144`_] `161`_ `172`_ + [`146`_] `163`_ `174`_ :Weaver: - [`12`_] `22`_ `23`_ `31`_ `162`_ `163`_ + [`13`_] `23`_ `24`_ `32`_ `61`_ `62`_ `68`_ `73`_ `76`_ `81`_ `82`_ `83`_ `84`_ `85`_ `86`_ `87`_ `91`_ `113`_ `115`_ `164`_ `165`_ :Web: - `45`_ `55`_ `65`_ `70`_ [`95`_] `151`_ `163`_ `168`_ `172`_ `175`_ + `46`_ `53`_ `56`_ `60`_ `62`_ `63`_ `64`_ `67`_ `68`_ `69`_ `70`_ `72`_ `73`_ `74`_ `76`_ `77`_ `81`_ `82`_ `83`_ `84`_ `85`_ `86`_ `87`_ `89`_ `90`_ `91`_ `92`_ [`97`_] `116`_ `130`_ `139`_ `153`_ `165`_ `170`_ `174`_ :WebReader: - [`114`_] `119`_ `162`_ `166`_ `168`_ `172`_ + [`116`_] `120`_ `130`_ `164`_ `168`_ `170`_ `174`_ :XrefCommand: - [`82`_] `83`_ `84`_ `85`_ + [`84`_] `85`_ `86`_ `87`_ +:__enter__: + [`5`_] `167`_ +:__exit__: + [`5`_] `167`_ :__version__: - `125`_ [`157`_] + `125`_ [`159`_] `164`_ :_gatherUserId: - [`108`_] + [`110`_] :_updateUserId: - [`108`_] + [`110`_] :add: - `55`_ [`99`_] + `56`_ [`101`_] :addDefName: - [`98`_] `100`_ `123`_ + [`100`_] `102`_ `123`_ :addIndent: - `10`_ [`13`_] `62`_ `66`_ + `11`_ [`14`_] `64`_ `68`_ :addNamed: - `65`_ [`100`_] + `67`_ [`102`_] :addOutput: - `70`_ [`101`_] + `72`_ [`103`_] :append: - `10`_ `13`_ `53`_ `54`_ `93`_ `99`_ `100`_ `101`_ `104`_ `110`_ `121`_ `123`_ `135`_ [`142`_] + `11`_ `14`_ `54`_ `55`_ `95`_ `101`_ `102`_ `103`_ `106`_ `112`_ `117`_ `123`_ `137`_ [`144`_] :appendText: - [`54`_] `123`_ `125`_ `126`_ `129`_ + [`55`_] `123`_ `125`_ `126`_ `130`_ :argparse: - [`158`_] `161`_ `162`_ `168`_ `170`_ `172`_ + `139`_ [`160`_] `163`_ `164`_ `165`_ `170`_ `172`_ `174`_ :builtins: [`124`_] `125`_ :chunkXref: - `84`_ [`107`_] + `86`_ [`109`_] :close: - [`4`_] `13`_ `44`_ `50`_ + [`5`_] `14`_ `45`_ `51`_ :clrIndent: - [`10`_] `62`_ `66`_ `68`_ + [`11`_] `64`_ `68`_ `70`_ :codeBegin: - `17`_ [`45`_] `66`_ `67`_ + `18`_ [`46`_] `68`_ `69`_ :codeBlock: - [`7`_] `66`_ `81`_ + [`8`_] `68`_ `83`_ :codeEnd: - `17`_ [`46`_] `66`_ `67`_ + `18`_ [`47`_] `68`_ `69`_ :codeFinish: - `4`_ `9`_ [`13`_] + `5`_ `10`_ [`14`_] :createUsedBy: - [`104`_] `151`_ + [`106`_] `153`_ :datetime: - `125`_ [`154`_] + `125`_ [`156`_] :doClose: - `4`_ `6`_ `13`_ `44`_ [`50`_] + `5`_ `7`_ `14`_ `45`_ [`51`_] :doOpen: - `4`_ `5`_ `13`_ `44`_ [`49`_] + `5`_ `6`_ `14`_ `45`_ [`50`_] :docBegin: - [`15`_] `60`_ + [`16`_] `62`_ :docEnd: - [`15`_] `60`_ + [`16`_] `62`_ :duration: - [`139`_] `146`_ `149`_ `152`_ + [`141`_] `148`_ `151`_ `154`_ :expand: - `74`_ `123`_ `161`_ [`162`_] + `76`_ `123`_ `163`_ [`164`_] :expect: - `117`_ `118`_ `123`_ `125`_ [`127`_] + `118`_ `119`_ `123`_ `125`_ [`127`_] :fileBegin: - `18`_ [`35`_] `71`_ + `19`_ [`36`_] `73`_ :fileEnd: - `18`_ [`36`_] `71`_ + `19`_ [`37`_] `73`_ :fileXref: - `83`_ [`107`_] + `85`_ [`109`_] :filecmp: - [`47`_] `50`_ + [`48`_] `51`_ :formatXref: - [`82`_] `83`_ `84`_ + [`84`_] `85`_ `86`_ :fullNameFor: - `66`_ `71`_ `87`_ `98`_ [`102`_] `103`_ `104`_ + `68`_ `73`_ `89`_ `100`_ [`104`_] `105`_ `106`_ :genReferences: - [`58`_] `104`_ + [`60`_] `106`_ :getUserIDRefs: - `57`_ [`64`_] `109`_ + `59`_ [`66`_] `111`_ :getchunk: - `87`_ [`103`_] `104`_ `113`_ + `89`_ [`105`_] `106`_ `115`_ :handleCommand: - [`115`_] `129`_ + [`117`_] `130`_ :language: - [`111`_] `145`_ `156`_ `175`_ + [`113`_] `147`_ `158`_ :lineNumber: - `17`_ `18`_ `33`_ `35`_ `45`_ `54`_ `56`_ [`57`_] `63`_ `67`_ `73`_ `77`_ `80`_ `82`_ `86`_ `119`_ `121`_ `123`_ `125`_ `126`_ `128`_ `129`_ `132`_ `171`_ + `18`_ `19`_ `34`_ `36`_ `46`_ `55`_ `57`_ [`59`_] `65`_ `69`_ `75`_ `79`_ `82`_ `84`_ `88`_ `117`_ `120`_ `123`_ `125`_ `126`_ `128`_ `130`_ `133`_ `173`_ :load: - `119`_ [`129`_] `151`_ `161`_ `163`_ + `120`_ [`130`_] `153`_ `163`_ `165`_ :location: - `115`_ `122`_ `125`_ `127`_ [`128`_] + `117`_ `122`_ `125`_ `127`_ [`128`_] :logging: - `3`_ `77`_ `91`_ `95`_ `114`_ `137`_ `159`_ `161`_ `162`_ `163`_ [`164`_] `165`_ `166`_ `168`_ `170`_ `172`_ + `4`_ `53`_ `79`_ `93`_ `97`_ `116`_ `139`_ `161`_ `163`_ `164`_ `165`_ [`166`_] `167`_ `168`_ `170`_ `172`_ `174`_ :logging.config: - [`164`_] `165`_ + [`166`_] `167`_ :main: - [`167`_] + [`169`_] `170`_ `174`_ :makeContent: - `54`_ [`56`_] `63`_ `73`_ + `55`_ [`57`_] `65`_ `75`_ :multi_reference: - `105`_ [`106`_] + `107`_ [`108`_] :no_definition: - `105`_ [`106`_] + `107`_ [`108`_] :no_reference: - `105`_ [`106`_] + `107`_ [`108`_] :open: - [`4`_] `13`_ `44`_ `112`_ `113`_ `125`_ `129`_ + [`5`_] `14`_ `45`_ `114`_ `115`_ `125`_ `130`_ :os: - `44`_ `49`_ `50`_ `113`_ `125`_ [`154`_] + `50`_ `51`_ `125`_ [`156`_] :parse: - `117`_ `118`_ [`129`_] `135`_ + `118`_ `119`_ [`130`_] `137`_ :parseArgs: - [`162`_] `167`_ + [`164`_] `169`_ :perform: - [`151`_] + [`153`_] :platform: [`124`_] `125`_ :process: - `125`_ [`163`_] `167`_ + `125`_ [`165`_] `169`_ :quote: - [`8`_] `81`_ + [`9`_] `83`_ :quoted_chars: - `8`_ `14`_ `29`_ [`38`_] + `9`_ `15`_ `30`_ [`39`_] :re: - `110`_ [`131`_] `132`_ `175`_ + `112`_ [`132`_] `133`_ :readdIndent: - `3`_ [`10`_] `13`_ + `4`_ [`11`_] `14`_ :ref: - `28`_ `58`_ [`79`_] `88`_ `99`_ `100`_ `101`_ + `29`_ `60`_ [`81`_] `90`_ `101`_ `102`_ `103`_ :referenceSep: - [`19`_] `113`_ + [`20`_] `115`_ :referenceTo: - `19`_ `20`_ [`39`_] `66`_ + `20`_ `21`_ [`40`_] `68`_ :references: - `16`_ `17`_ `18`_ `19`_ `25`_ `32`_ `34`_ `36`_ [`42`_] `52`_ `58`_ `105`_ `122`_ `156`_ `161`_ `171`_ + `17`_ `18`_ `19`_ `20`_ `26`_ `33`_ `35`_ `37`_ [`43`_] `53`_ `60`_ `61`_ `107`_ `122`_ `163`_ `173`_ :resolve: - `67`_ [`87`_] `88`_ `89`_ `90`_ `103`_ + `69`_ [`89`_] `90`_ `91`_ `92`_ `105`_ :searchForRE: - `57`_ [`78`_] `80`_ `110`_ + `59`_ [`80`_] `82`_ `112`_ +:setIndent: + [`11`_] `70`_ :setUserIDRefs: - [`64`_] `122`_ + `59`_ [`66`_] `122`_ :shlex: - [`133`_] `135`_ + [`134`_] `137`_ :startswith: - `57`_ [`78`_] `80`_ `102`_ `111`_ `129`_ `135`_ `163`_ + `59`_ [`80`_] `82`_ `104`_ `113`_ `130`_ `137`_ `165`_ :string: - [`11`_] `16`_ `17`_ `18`_ `19`_ `20`_ `21`_ `24`_ `25`_ `28`_ `30`_ `33`_ `34`_ `35`_ `36`_ `37`_ `39`_ `40`_ `41`_ `42`_ `170`_ `171`_ + [`12`_] `17`_ `18`_ `19`_ `20`_ `21`_ `22`_ `25`_ `26`_ `29`_ `31`_ `34`_ `35`_ `36`_ `37`_ `38`_ `40`_ `41`_ `42`_ `43`_ `55`_ `172`_ `173`_ :summary: - `139`_ `143`_ `146`_ `149`_ [`152`_] `163`_ `168`_ `172`_ + `141`_ `145`_ `148`_ `151`_ [`154`_] `165`_ `170`_ `174`_ :sys: - [`124`_] `125`_ `166`_ + [`124`_] `125`_ `168`_ `169`_ :tangle: - `45`_ `61`_ `67`_ `69`_ `72`_ `73`_ `75`_ `79`_ `80`_ `81`_ `82`_ `90`_ [`112`_] `148`_ `156`_ `161`_ `168`_ `175`_ + `46`_ `63`_ `69`_ `71`_ `74`_ `75`_ `77`_ `81`_ `82`_ `83`_ `84`_ `92`_ [`114`_] `150`_ `163`_ `170`_ :tempfile: - [`47`_] `49`_ + [`48`_] `50`_ :time: - `138`_ `139`_ [`154`_] + `125`_ `140`_ `141`_ [`156`_] :types: - `12`_ `125`_ [`154`_] + `13`_ `125`_ [`156`_] :usedBy: - [`88`_] + [`90`_] :userNamesXref: - `85`_ [`108`_] + `87`_ [`110`_] :weakref: - [`96`_] `99`_ `100`_ `101`_ + `53`_ [`98`_] `101`_ `102`_ `103`_ :weave: - `60`_ `66`_ `71`_ `74`_ `79`_ `80`_ `81`_ `83`_ `84`_ `85`_ `89`_ [`113`_] `145`_ `156`_ `161`_ `170`_ `175`_ + `62`_ `68`_ `73`_ `76`_ `81`_ `82`_ `83`_ `85`_ `86`_ `87`_ `91`_ [`115`_] `147`_ `163`_ `172`_ :weaveChunk: - `89`_ [`113`_] + `91`_ [`115`_] :weaveReferenceTo: - `60`_ `66`_ [`74`_] `113`_ + `62`_ `68`_ [`76`_] `115`_ :weaveShortReferenceTo: - `60`_ `66`_ [`74`_] `113`_ + `62`_ `68`_ [`76`_] `115`_ :webAdd: - `55`_ `65`_ [`70`_] `117`_ `118`_ `119`_ `120`_ `129`_ + `56`_ `67`_ [`72`_] `118`_ `119`_ `120`_ `121`_ `130`_ :write: - [`4`_] `7`_ `9`_ `17`_ `18`_ `20`_ `21`_ `45`_ `80`_ `113`_ + `4`_ [`5`_] `8`_ `10`_ `18`_ `19`_ `21`_ `22`_ `46`_ `82`_ `115`_ :xrefDefLine: - `21`_ [`41`_] `85`_ + `22`_ [`42`_] `87`_ :xrefFoot: - `20`_ [`40`_] `82`_ `85`_ + `21`_ [`41`_] `84`_ `87`_ :xrefHead: - `20`_ [`40`_] `82`_ `85`_ + `21`_ [`41`_] `84`_ `87`_ :xrefLine: - `20`_ [`40`_] `82`_ + `21`_ [`41`_] `84`_ @@ -9505,11 +9454,10 @@ User Identifiers .. class:: small - Created by /Users/slott/Documents/Projects/PyWebTool-3/pyweb/pyweb.py at Sat Jun 16 08:11:27 2018. + Created by /Users/slott/Documents/Projects/py-web-tool/bootstrap/pyweb.py at Mon Jun 13 13:20:24 2022. - Source pyweb.w modified Sat Jun 16 08:10:37 2018. + Source pyweb.w modified Mon Jun 13 08:52:05 2022. pyweb.__version__ '3.0'. - Working directory '/Users/slott/Documents/Projects/PyWebTool-3/pyweb'. - + Working directory '/Users/slott/Documents/Projects/py-web-tool/src'. diff --git a/pyweb.w b/src/pyweb.w similarity index 91% rename from pyweb.w rename to src/pyweb.w index 31686b5..156310a 100755 --- a/pyweb.w +++ b/src/pyweb.w @@ -1,5 +1,5 @@ ############################## -pyWeb Literate Programming 3.0 +pyWeb Literate Programming 3.1 ############################## ================================================= @@ -14,15 +14,17 @@ Yet Another Literate Programming Tool @i intro.w +@i usage.w + +@i language.w + @i overview.w @i impl.w @i tests.w -@i additional.w - -@i jedit.w +@i scripts.w @i todo.w @@ -59,4 +61,3 @@ User Identifiers pyweb.__version__ '@(__version__@)'. Working directory '@(os.path.realpath('.')@)'. - diff --git a/src/scripts.w b/src/scripts.w new file mode 100644 index 0000000..42e3a24 --- /dev/null +++ b/src/scripts.w @@ -0,0 +1,166 @@ +.. py-web-tool/src/scripts.w + +Handy Scripts and Other Files +================================================= + +Two aditional scripts, ``tangle.py`` and ``weave.py``, are provided as examples +which can be customized and extended. + +``tangle.py`` Script +--------------------- + +This script shows a simple version of Tangling. This has a permitted +error for '@@i' commands to allow an include file (for example test results) +to be omitted from the tangle operation. + +Note the general flow of this top-level script. + +1. Create the logging context. + +2. Create the options. This hard-coded object is a stand-in for + parsing command-line options. + +3. Create the web object. + +4. For each action (``LoadAction`` and ``TangleAction`` in this example) + Set the web, set the options, execute the callable action, and write + a summary. + +@o tangle.py +@{#!/usr/bin/env python3 +"""Sample tangle.py script.""" +import argparse +import logging +from pathlib import Path +import pyweb + +def main(source: Path) -> None: + with pyweb.Logger(pyweb.log_config): + logger = logging.getLogger(__file__) + + options = argparse.Namespace( + source_path=source, + output=source.parent, + verbosity=logging.INFO, + command='@@', + permitList=['@@i'], + tangler_line_numbers=False, + reference_style=pyweb.SimpleReference(), + theTangler=pyweb.TanglerMake(), + webReader=pyweb.WebReader(), + ) + + w = pyweb.Web() + + for action in pyweb.LoadAction(), pyweb.TangleAction(): + action.web = w + action.options = options + action() + logger.info(action.summary()) + +if __name__ == "__main__": + main(Path("examples/test_rst.w")) +@} + +``weave.py`` Script +--------------------- + +This script shows a simple version of Weaving. This shows how +to define a customized set of templates for a different markup language. + + +A customized weaver generally has three parts. + +@o weave.py +@{@ + +@ + +@ +@} + +@d weave.py overheads... +@{#!/usr/bin/env python3 +"""Sample weave.py script.""" +import argparse +import logging +import string +from pathlib import Path +import pyweb +@} + +@d weave.py custom weaver definition... +@{ +class MyHTML(pyweb.HTML): + """HTML formatting templates.""" + extension = ".html" + + cb_template = string.Template(""" + +

    ${fullName} (${seq}) ${concat}

    +
    \n""")
    +
    +    ce_template = string.Template("""
    +    
    +

    ${fullName} (${seq}). + ${references} +

    \n""") + + fb_template = string.Template(""" + +

    ``${fullName}`` (${seq}) ${concat}

    +
    \n""") # Prevent indent
    +        
    +    fe_template = string.Template( """
    +

    ◊ ``${fullName}`` (${seq}). + ${references} +

    \n""") + + ref_item_template = string.Template( + '${fullName} (${seq})' + ) + + ref_template = string.Template(' Used by ${refList}.' ) + + refto_name_template = string.Template( + '${fullName} (${seq})' + ) + refto_seq_template = string.Template('(${seq})') + + xref_head_template = string.Template("
    \n") + xref_foot_template = string.Template("
    \n") + xref_item_template = string.Template("
    ${fullName}
    ${refList}
    \n") + + name_def_template = string.Template('•${seq}') + name_ref_template = string.Template('${seq}') +@} + +@d weaver.py processing... +@{ +def main(source: Path) -> None: + with pyweb.Logger(pyweb.log_config): + logger = logging.getLogger(__file__) + + options = argparse.Namespace( + source_path=source, + output=source.parent, + verbosity=logging.INFO, + command='@@', + permitList=[], + tangler_line_numbers=False, + reference_style=pyweb.SimpleReference(), + theWeaver=MyHTML(), + webReader=pyweb.WebReader(), + ) + + w = pyweb.Web() + + for action in pyweb.LoadAction(), pyweb.WeaveAction(): + action.web = w + action.options = options + action() + logger.info(action.summary()) + +if __name__ == "__main__": + main(Path("examples/test_rst.w")) +@} diff --git a/src/tangle.py b/src/tangle.py new file mode 100644 index 0000000..da25538 --- /dev/null +++ b/src/tangle.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 +"""Sample tangle.py script.""" +import argparse +import logging +from pathlib import Path +import pyweb + +def main(source: Path) -> None: + with pyweb.Logger(pyweb.log_config): + logger = logging.getLogger(__file__) + + options = argparse.Namespace( + source_path=source, + output=source.parent, + verbosity=logging.INFO, + command='@', + permitList=['@i'], + tangler_line_numbers=False, + reference_style=pyweb.SimpleReference(), + theTangler=pyweb.TanglerMake(), + webReader=pyweb.WebReader(), + ) + + w = pyweb.Web() + + for action in pyweb.LoadAction(), pyweb.TangleAction(): + action.web = w + action.options = options + action() + logger.info(action.summary()) + +if __name__ == "__main__": + main(Path("examples/test_rst.w")) diff --git a/src/tests.w b/src/tests.w new file mode 100644 index 0000000..cdf1806 --- /dev/null +++ b/src/tests.w @@ -0,0 +1,25 @@ +.. py-web-tool/src/test.w + +Unit Tests +=========== + +The ``tests`` directory includes ``pyweb_test.w``, which will create a +complete test suite. + +This source will weaves a ``pyweb_test.html`` file. See `tests/pyweb_test.html `_. + +This source will tangle several test modules: ``test.py``, ``test_tangler.py``, ``test_weaver.py``, +``test_loader.py``, ``test_unit.py``, and ``test_scripts.py``. + +Use **pytest** to discover and run all 80+ test cases. + +Here's a script that works out well for running this without disturbing the development +environment. The ``PYTHONPATH`` setting is essential to support importing ``pyweb``. + +.. parsed-literal:: + + python pyweb.py -o tests tests/pyweb_test.w + PYTHONPATH=$(PWD) pytest + +Note that the last line really does set an environment variable and run +the ``pytest`` tool on a single line. diff --git a/src/todo.w b/src/todo.w new file mode 100644 index 0000000..c29da58 --- /dev/null +++ b/src/todo.w @@ -0,0 +1,61 @@ +.. py-web-tool/src/todo.w + + +To Do +======= + +1. Rename the module from ``pyweb`` to ``pylpweb`` to avoid name squatting issues. + Rename the project from ``py-web-tool`` to ``py-lpweb``. + +2. Switch to jinja templates. + + - See the ``weave.py`` example. + Defining templates in the source removes any need for a command-line option. A silly optimization. + Setting the "command character" to something other than ``@@`` can be done in the configuration, too. + + - With Jinjda templates can be provided via + a Jinja configuration (there are many choices.) By stepping away from the ``string.Template``, + we can incorporate list-processing ``{%for%}...{%endfor%}`` construct that + pushes some processing into the template. + +#. Separate TOML-based logging configuration file would be helpful. + Must be separate from template configuration. + +#. Rethink the presentation. Are |loz| and |srarr| REALLY necessary? + Can we use ◊ and → now that Unicode is more universal? + And why ``'\N{LOZENGE}'``? There's a nice ``'\N{END OF PROOF}'`` symbol we could use. + Remove the unused ``header``, ``docBegin()``, and ``docEnd()``. + +#. Tangling can include non-woven content. More usefully, Weaving can exclude some chunks. + The use case is a book chapter with test cases that are **not** woven into the text. + Add an option to define tangle-only chunks that are NOT woven into the final document. + +#. Update the ``-indent`` option on @@d chunks to accept a numeric argument with the + specific indentation value. This becomes a kind of "noindent" with a given + value. The ``-noindent`` would then be the same as ``-indent 0``. + Currently, `-indent` and `-noindent` are true/false flags. + +#. We might want to decompose the ``impl.w`` file: it's huge. + +#. We might want to interleave code and test into a document that presents both + side-by-side. We can route to multiple files. + It's a little awkward to create tangled files in multiple directories; + We'd have to use ``../tests/whatever.py``, **assuming** we were always using ``-o src``. + +#. Fix name definition order. There's no **good** reason why a full name must + be first and elided names defined later. + +#. Offer a basic XHTML template that uses ``CDATA`` sections instead of quoting. + Does require the standard quoting for the ``CDATA`` end tag. + +#. The ``createUsedBy()`` method can be done incrementally by + accumulating a list of forward references to chunks; as each + new chunk is added, any references to the chunk are removed from + the forward references list, and a call is made to the Web's + setUsage method. References backward to already existing chunks + are easily resolved with a simple lookup. + +#. Note that the overall ``Web`` is a bit like a ``NamedChunk`` that contains ``Chunks``. + This similarity could be factored out. + While this will create a more proper **Composition** pattern implementation, it + leads to the question of why nest ``@@d`` or ``@@o`` chunks in the first place? diff --git a/src/usage.w b/src/usage.w new file mode 100644 index 0000000..b2dc559 --- /dev/null +++ b/src/usage.w @@ -0,0 +1,188 @@ +.. py-web-tool/src/usage.w + +Installing +========== + +This requires Python 3.10. + +This is not (currently) hosted in PyPI. Instead of installing it with PIP, +clone the GitHub repository or download the distribution kit. + +Install pyweb "manually" using the provided ``setup.py``. + +:: + + python setup.py install + +This will install the ``pyweb`` module. + +Using +===== + +**py-web-tool** supports two use cases, `Tangle Source Files`_ and `Weave Documentation`_. +These are often combined to both tangle and weave an application and it's documentation. + +Tangle Source Files +------------------- + +A user initiates this process when they have a complete ``.w`` file that contains +a description of source files. These source files are described with ``@@o`` commands +in the ``.w`` file. + +The use case is successful when the source files are produced. + +Outside this use case, the user will debug those source files, possibly updating the +``.w`` file. This will lead to a need to restart this use case. + +The use case is a failure when the source files cannot be produced, due to +errors in the ``.w`` file. These must be corrected based on information in log messages. + +A typical command to tangle (without weaving) is: + +.. parsed-literal:: + + python -m pyweb -xw *theFile*.w + +The outputs will be defined by the ``@@o`` commands in the source. + +Weave Documentation +------------------- + +A user initiates this process when they have a ``.w`` file that contains +a description of a document to produce. The document is described by the entire +``.w`` file. The default is to use ReSTructured Text (RST) markup. +The output file will have the ``.rst`` suffix. + +The use case is successful when the documentation file is produced. + +Outside this use case, the user will edit the documentation file, possibly updating the +``.w`` file. This will lead to a need to restart this use case. + +The use case is a failure when the documentation file cannot be produced, due to +errors in the ``.w`` file. These must be corrected based on information in log messages. + +A typical command to weave (without tangling) is: + +.. parsed-literal:: + + python -m pyweb -xt *theFile*\ .w + +The output will be the *theFile*\ ``.rst``. + +Tangle, Test, and Weave with Test Results +----------------------------------------- + +A user initiates this process when they have a ``.w`` file that contains +a description of a document to produce. The document is described by the entire +``.w`` file. Further, their final document should include test output +from the source files created by the tangle operation. + +The use case is successful when the documentation file is produced, including +current test output. + +Outside this use case, the user will edit the documentation file, possibly updating the +``.w`` file. This will lead to a need to restart this use case. + +The use case is a failure when the documentation file cannot be produced, due to +errors in the ``.w`` file. These must be corrected based on information in log messages. + +The use case is a failure when the documentation file does not include current +test output. + +The sequence is as follows: + +.. parsed-literal:: + + python -m pyweb -xw -pi *theFile*\ .w + pytest >\ *aLog* + python -m pyweb -xt *theFile*\ .w + +The first step excludes weaving and permits errors on the ``@@i`` command. The ``-pi`` option +is necessary in the event that the log file does not yet exist. The second step +runs the test, creating a log file. The third step weaves the final document, +including the test output. + +Running **py-web-tool** to Tangle and Weave +------------------------------------------- + +Assuming that you have marked ``pyweb.py`` as executable, +you do the following: + +.. parsed-literal:: + + python -m pyweb *theFile*\ .w + +This will tangle the ``@@o`` commands in each *theFile*. +It will also weave the output, and create *theFile*.rst. + +Command Line Options +~~~~~~~~~~~~~~~~~~~~~ + +Currently, the following command line options are accepted. + + +:-v: + Verbose logging. + +:-s: + Silent operation. + +:-c\ *x*: + Change the command character from ``@@`` to ``*x*``. + +:-w\ *weaver*: + Choose a particular documentation weaver template. Currently the choices + are RST and HTML. + +:-xw: + Exclude weaving. This does tangling of source program files only. + +:-xt: + Exclude tangling. This does weaving of the document file only. + +:-p\ *command*: + Permit errors in the given list of commands. The most common + version is ``-pi`` to permit errors in locating an include file. + This is done in the following scenario: pass 1 uses ``-xw -pi`` to exclude + weaving and permit include-file errors; + the tangled program is run to create test results; pass 2 uses + ``-xt`` to exclude tangling and include the test results. + +:-o\ *directory*: + The directory to which to write output files. + +Bootstrapping +-------------- + +**py-web-tool** is written using **py-web-tool**. The distribution includes the original ``.w`` +files as well as a ``.py`` module. + +The bootstrap procedure is to run a "known good" ``pyweb`` to transform +a working copy into a new version of ``pyweb``. We provide the previous release in the ``bootstrap`` +directory. + +.. parsed-literal:: + + python bootstrap/pyweb.py pyweb.w + rst2html.py pyweb.rst pyweb.html + +The resulting ``pyweb.html`` file is the updated documentation. +The ``pyweb.py`` is the updated candidate release of **py-web-tool**. + +Similarly, the tests built from a ``.w`` files. + +.. parsed-literal:: + + python pyweb.py tests/pyweb_test.w -o tests + PYTHONPATH=.. pytest + rst2html.py tests/pyweb_test.rst tests/pyweb_test.html + +Dependencies +------------- + +**py-web-tool** requires Python 3.10 or newer. + +If you create RST output, you'll want to use ``docutils`` to translate +the RST to HTML or LaTeX or any of the other formats supported by docutils. + +Tools like ``pytest`` and ``tox`` are also used for development. diff --git a/src/weave.py b/src/weave.py new file mode 100644 index 0000000..fff4141 --- /dev/null +++ b/src/weave.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +"""Sample weave.py script.""" +import argparse +import logging +import string +from pathlib import Path +import pyweb + + + +class MyHTML(pyweb.HTML): + """HTML formatting templates.""" + extension = ".html" + + cb_template = string.Template(""" + +

    ${fullName} (${seq}) ${concat}

    +
    \n""")
    +
    +    ce_template = string.Template("""
    +    
    +

    ${fullName} (${seq}). + ${references} +

    \n""") + + fb_template = string.Template(""" + +

    ``${fullName}`` (${seq}) ${concat}

    +
    \n""") # Prevent indent
    +        
    +    fe_template = string.Template( """
    +

    ◊ ``${fullName}`` (${seq}). + ${references} +

    \n""") + + ref_item_template = string.Template( + '${fullName} (${seq})' + ) + + ref_template = string.Template(' Used by ${refList}.' ) + + refto_name_template = string.Template( + '${fullName} (${seq})' + ) + refto_seq_template = string.Template('(${seq})') + + xref_head_template = string.Template("
    \n") + xref_foot_template = string.Template("
    \n") + xref_item_template = string.Template("
    ${fullName}
    ${refList}
    \n") + + name_def_template = string.Template('•${seq}') + name_ref_template = string.Template('${seq}') + + + +def main(source: Path) -> None: + with pyweb.Logger(pyweb.log_config): + logger = logging.getLogger(__file__) + + options = argparse.Namespace( + source_path=source, + output=source.parent, + verbosity=logging.INFO, + command='@', + permitList=[], + tangler_line_numbers=False, + reference_style=pyweb.SimpleReference(), + theWeaver=MyHTML(), + webReader=pyweb.WebReader(), + ) + + w = pyweb.Web() + + for action in pyweb.LoadAction(), pyweb.WeaveAction(): + action.web = w + action.options = options + action() + logger.info(action.summary()) + +if __name__ == "__main__": + main(Path("examples/test_rst.w")) + diff --git a/tangle.py b/tangle.py deleted file mode 100644 index eccebc8..0000000 --- a/tangle.py +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env python3 -"""Sample tangle.py script.""" -import pyweb -import logging -import argparse - -with pyweb.Logger( pyweb.log_config ): - logger= logging.getLogger(__file__) - - options = argparse.Namespace( - webFileName= "pyweb.w", - verbosity= logging.INFO, - command= '@', - permitList= ['@i'], - tangler_line_numbers= False, - reference_style = pyweb.SimpleReference(), - theTangler= pyweb.TanglerMake(), - webReader= pyweb.WebReader(), - ) - - w= pyweb.Web() - - for action in LoadAction(), TangleAction(): - action.web= w - action.options= options - action() - logger.info( action.summary() ) - diff --git a/test/combined.w b/test/combined.w deleted file mode 100644 index ebb00a0..0000000 --- a/test/combined.w +++ /dev/null @@ -1,50 +0,0 @@ - - -

    The combined test script runs all tests in all test modules.

    - -@o test.py -@{@ -@ -@ -@} - -

    The overheads import unittest and logging, because those are essential -infrastructure. Additionally, each of the test modules is also imported. -

    - -@d Combined Test overheads... -@{from __future__ import print_function -"""Combined tests.""" -import unittest -import test_loader -import test_tangler -import test_weaver -import test_unit -import logging -@} - -

    The test suite is built from each of the individual test modules.

    - -@d Combined Test suite... -@{ -def suite(): - s= unittest.TestSuite() - for m in ( test_loader, test_tangler, test_weaver, test_unit ): - s.addTests( unittest.defaultTestLoader.loadTestsFromModule( m ) ) - return s -@} - -

    The main script initializes logging and then executes the -unittest.TextTestRunner on the test suite. -

    - -@d Combined Test main... -@{ -if __name__ == "__main__": - import sys - logging.basicConfig( stream=sys.stdout, level=logging.CRITICAL ) - tr= unittest.TextTestRunner() - result= tr.run( suite() ) - logging.shutdown() - sys.exit( len(result.failures) + len(result.errors) ) -@} \ No newline at end of file diff --git a/test/func.w b/test/func.w deleted file mode 100644 index b189002..0000000 --- a/test/func.w +++ /dev/null @@ -1,548 +0,0 @@ - - -

    There are three broad areas of functional testing.

    - -
      -
    • Loading
    • -
    • Tanging
    • -
    • Weaving
    • -
    - -

    There are a total of 11 test cases.

    - -

    Tests for Loading

    - -

    We need to be able to load a web from one or more source files.

    - -@o test_loader.py -@{@ -@ -@ -@ -@ -@} - -

    Parsing test cases have a common setup shown in this superclass.

    - -

    By using some class-level variables text, -file_name, we can simply provide a file-like -input object to the WebReader instance. -

    - -@d Load Test superclass... -@{ -class ParseTestcase( unittest.TestCase ): - text= "" - file_name= "" - def setUp( self ): - source= StringIO.StringIO( self.text ) - self.web= pyweb.Web( self.file_name ) - self.rdr= pyweb.WebReader() - self.rdr.source( self.file_name, source ).web( self.web ) -@} - -

    There are a lot of specific parsing exceptions which can be thrown. -We'll cover most of the cases with a quick check for a failure to -find an expected next token. -

    - -@d Load Test error handling... -@{ -@ - -class Test_ParseErrors( ParseTestcase ): - text= test1_w - file_name= "test1.w" - def test_should_raise_syntax( self ): - try: - self.rdr.load() - self.fail( "Should not parse" ) - except pyweb.Error, e: - self.assertEquals( "At ('test1.w', 8, 8): expected ('@@{',), found '@@o'", e.args[0] ) -@} - -@d Sample Document 1... -@{ -test1_w= """Some anonymous chunk -@@o test1.tmp -@@{@@ -@@ -@@}@@@@ -@@d part1 @@{This is part 1.@@} -Okay, now for an error. -@@o show how @@o commands work -@@{ @@{ @@] @@] -""" -@} - -

    All of the parsing exceptions should be correctly identified with -any included file. -We'll cover most of the cases with a quick check for a failure to -find an expected next token. -

    - -

    In order to handle the include file processing, we have to actually -create a temporary file. It's hard to mock the include processing. -

    - -@d Load Test include... -@{ -@ - -class Test_IncludeParseErrors( ParseTestcase ): - text= test8_w - file_name= "test8.w" - def setUp( self ): - with open('test8_inc.tmp','w') as temp: - temp.write( test8_inc_w ) - super( Test_IncludeParseErrors, self ).setUp() - def test_should_raise_include_syntax( self ): - try: - self.rdr.load() - self.fail( "Should not parse" ) - except pyweb.Error, e: - self.assertEquals( "At ('test8_inc.tmp', 3, 4): end of input, ('@@{', '@@[') not found", e.args[0] ) - def tearDown( self ): - os.remove( 'test8_inc.tmp' ) - super( Test_IncludeParseErrors, self ).tearDown() -@} - -

    The sample document must reference the correct name that will -be given to the included document by setUp. -

    - -@d Sample Document 8... -@{ -test8_w= """Some anonymous chunk. -@@d title @@[the title of this document, defined with @@@@[ and @@@@]@@] -A reference to @@. -@@i test8_inc.tmp -A final anonymous chunk from test8.w -""" - -test8_inc_w="""A chunk from test8a.w -And now for an error - incorrect syntax in an included file! -@@d yap -""" -@} - -

    The overheads for a Python unittest.

    - -@d Load Test overheads... -@{from __future__ import print_function -"""Loader and parsing tests.""" -import pyweb -import unittest -import logging -import StringIO -import os -@} - -

    A main program that configures logging and then runs the test.

    - -@d Load Test main program... -@{ -if __name__ == "__main__": - import sys - logging.basicConfig( stream=sys.stdout, level= logging.WARN ) - unittest.main() -@} - -

    Tests for Tangling

    - -

    We need to be able to tangle a web.

    - -@o test_tangler.py -@{@ -@ -@ -@ -@ -@ -@ -@ -@ -@} - -

    Tangling test cases have a common setup and teardown shown in this superclass. -Since tangling must produce a file, it's helpful to remove the file that gets created. -The essential test case is to load and attempt to tangle, checking the -exceptions raised. -

    - -@d Tangle Test superclass... -@{ -class TangleTestcase( unittest.TestCase ): - text= "" - file_name= "" - error= "" - def setUp( self ): - source= StringIO.StringIO( self.text ) - self.web= pyweb.Web( self.file_name ) - self.rdr= pyweb.WebReader() - self.rdr.source( self.file_name, source ).web( self.web ) - self.tangler= pyweb.Tangler() - def tangle_and_check_exception( self, exception_text ): - try: - self.rdr.load() - self.web.tangle( self.tangler ) - self.web.createUsedBy() - self.fail( "Should not tangle" ) - except pyweb.Error, e: - self.assertEquals( exception_text, e.args[0] ) - def tearDown( self ): - name, _ = os.path.splitext( self.file_name ) - try: - os.remove( name + ".tmp" ) - except OSError: - pass -@} - -@d Tangle Test semantic error 2... -@{ -@ - -class Test_SemanticError_2( TangleTestcase ): - text= test2_w - file_name= "test2.w" - def test_should_raise_undefined( self ): - self.tangle_and_check_exception( "Attempt to tangle an undefined Chunk, part2." ) -@} - -@d Sample Document 2... @{ -test2_w= """Some anonymous chunk -@@o test2.tmp -@@{@@ -@@ -@@}@@@@ -@@d part1 @@{This is part 1.@@} -Okay, now for some errors: no part2! -""" -@} - -@d Tangle Test semantic error 3... -@{ -@ - -class Test_SemanticError_3( TangleTestcase ): - text= test3_w - file_name= "test3.w" - def test_should_raise_bad_xref( self ): - self.tangle_and_check_exception( "Illegal tangling of a cross reference command." ) -@} - -@d Sample Document 3... @{ -test3_w= """Some anonymous chunk -@@o test3.tmp -@@{@@ -@@ -@@}@@@@ -@@d part1 @@{This is part 1.@@} -@@d part2 @@{This is part 2, with an illegal: @@f.@@} -Okay, now for some errors: attempt to tangle a cross-reference! -""" -@} - - -@d Tangle Test semantic error 4... -@{ -@ - -class Test_SemanticError_4( TangleTestcase ): - text= test4_w - file_name= "test4.w" - def test_should_raise_noFullName( self ): - self.tangle_and_check_exception( "No full name for 'part1...'" ) -@} - -@d Sample Document 4... @{ -test4_w= """Some anonymous chunk -@@o test4.tmp -@@{@@ -@@ -@@}@@@@ -@@d part1... @@{This is part 1.@@} -@@d part2 @@{This is part 2.@@} -Okay, now for some errors: attempt to weave but no full name for part1.... -""" -@} - -@d Tangle Test semantic error 5... -@{ -@ - -class Test_SemanticError_5( TangleTestcase ): - text= test5_w - file_name= "test5.w" - def test_should_raise_ambiguous( self ): - self.tangle_and_check_exception( "Ambiguous abbreviation 'part1...', matches ['part1b', 'part1a']" ) -@} - -@d Sample Document 5... @{ -test5_w= """ -Some anonymous chunk -@@o test5.tmp -@@{@@ -@@ -@@}@@@@ -@@d part1a @@{This is part 1 a.@@} -@@d part1b @@{This is part 1 b.@@} -@@d part2 @@{This is part 2.@@} -Okay, now for some errors: part1... is ambiguous -""" -@} - -@d Tangle Test semantic error 6... -@{ -@ - -class Test_SemanticError_6( TangleTestcase ): - text= test6_w - file_name= "test6.w" - def test_should_warn( self ): - self.rdr.load() - self.web.tangle( self.tangler ) - self.web.createUsedBy() - self.assertEquals( 1, len( self.web.no_reference() ) ) - self.assertEquals( 1, len( self.web.multi_reference() ) ) - self.assertEquals( 0, len( self.web.no_definition() ) ) -@} - -@d Sample Document 6... @{ -test6_w= """Some anonymous chunk -@@o test6.tmp -@@{@@ -@@ -@@}@@@@ -@@d part1a @@{This is part 1 a.@@} -@@d part2 @@{This is part 2.@@} -Okay, now for some warnings: -- part1 has multiple references. -- part2 is unreferenced. -""" -@} - -@d Tangle Test include error 7... -@{ -@ - -class Test_IncludeError_7( TangleTestcase ): - text= test7_w - file_name= "test7.w" - def setUp( self ): - with open('test7_inc.tmp','w') as temp: - temp.write( test7_inc_w ) - super( Test_IncludeError_7, self ).setUp() - def test_should_include( self ): - self.rdr.load() - self.web.tangle( self.tangler ) - self.web.createUsedBy() - self.assertEquals( 5, len(self.web.chunkSeq) ) - self.assertEquals( test7_inc_w, self.web.chunkSeq[3].commands[0].text ) - def tearDown( self ): - os.remove( 'test7_inc.tmp' ) - super( Test_IncludeError_7, self ).tearDown() -@} - -@d Sample Document 7... @{ -test7_w= """ -Some anonymous chunk. -@@d title @@[the title of this document, defined with @@@@[ and @@@@]@@] -A reference to @@. -@@i test7_inc.tmp -A final anonymous chunk from test7.w -""" - -test7_inc_w= """The test7a.tmp chunk for test7.w -""" -@} - -@d Tangle Test overheads... -@{from __future__ import print_function -"""Tangler tests exercise various semantic features.""" -import pyweb -import unittest -import logging -import StringIO -import os -@} - -@d Tangle Test main program... -@{ -if __name__ == "__main__": - import sys - logging.basicConfig( stream=sys.stdout, level= logging.WARN ) - unittest.main() -@} - - -

    Tests for Weaving

    - -

    We need to be able to weave a document from one or more source files.

    - -@o test_weaver.py -@{@ -@ -@ -@ -@ -@} - -

    Weaving test cases have a common setup shown in this superclass.

    - -@d Weave Test superclass... @{ -class WeaveTestcase( unittest.TestCase ): - text= "" - file_name= "" - error= "" - def setUp( self ): - source= StringIO.StringIO( self.text ) - self.web= pyweb.Web( self.file_name ) - self.rdr= pyweb.WebReader() - self.rdr.source( self.file_name, source ).web( self.web ) - self.rdr.load() - def tangle_and_check_exception( self, exception_text ): - try: - self.rdr.load() - self.web.tangle( self.tangler ) - self.web.createUsedBy() - self.fail( "Should not tangle" ) - except pyweb.Error, e: - self.assertEquals( exception_text, e.args[0] ) - def tearDown( self ): - name, _ = os.path.splitext( self.file_name ) - try: - os.remove( name + ".html" ) - except OSError: - pass -@} - -@d Weave Test references... @{ -@ -@ - -class Test_RefDefWeave( WeaveTestcase ): - text= test0_w - file_name = "test0.w" - def test_load_should_createChunks( self ): - self.assertEquals( 3, len( self.web.chunkSeq ) ) - def test_weave_should_createFile( self ): - doc= pyweb.HTML() - self.web.weave( doc ) - with open("test0.html","r") as source: - actual= source.read() - m= difflib.SequenceMatcher( lambda x: x in string.whitespace, expected, actual ) - for tag, i1, i2, j1, j2 in m.get_opcodes(): - if tag == "equal": continue - self.fail( "At %d %s: expected %r, actual %r" % ( j1, tag, repr(expected[i1:i2]), repr(actual[j1:j2]) ) ) - -@} - -@d Sample Document 0... -@{ -test0_w= """ - - - - -@@ - -@@d some code -@@{ -def fastExp( n, p ): - r= 1 - while p > 0: - if p%2 == 1: return n*fastExp(n,p-1) - return n*n*fastExp(n,p/2) - -for i in range(24): - fastExp(2,i) -@@} - - -""" -@} - -@d Expected Output 0... @{ -expected= """ - - - - - some code (1) - - - - -

    some code (1) =

    -
    
    -
    -def fastExp( n, p ):
    -    r= 1
    -    while p > 0:
    -        if p%2 == 1: return n*fastExp(n,p-1)
    -	return n*n*fastExp(n,p/2)
    -
    -for i in range(24):
    -    fastExp(2,i)
    -
    -    
    -

    some code (1). - -

    - - - -""" -@} - -@d Weave Test evaluation... @{ -@ - -class TestEvaluations( WeaveTestcase ): - text= test9_w - file_name = "test9.w" - def test_should_evaluate( self ): - doc= pyweb.HTML() - self.web.weave( doc ) - with open("test9.html","r") as source: - actual= source.readlines() - #print( actual ) - self.assertEquals( "An anonymous chunk.\n", actual[0] ) - self.assertTrue( actual[1].startswith( "Time =" ) ) - self.assertEquals( "File = ('test9.w', 3, 3)\n", actual[2] ) - self.assertEquals( 'Version = $Revision$\n', actual[3] ) - self.assertEquals( 'OS = %s\n' % os.name, actual[4] ) - self.assertEquals( 'CWD = %s\n' % os.getcwd(), actual[5] ) -@} - -@d Sample Document 9... -@{ -test9_w= """An anonymous chunk. -Time = @@(time.asctime()@@) -File = @@(theLocation@@) -Version = @@(__version__@@) -OS = @@(os.name@@) -CWD = @@(os.getcwd()@@) -""" -@} - -@d Weave Test overheads... -@{from __future__ import print_function -"""Weaver tests exercise various weaving features.""" -import pyweb -import unittest -import logging -import StringIO -import os -import difflib -import string -@} - -@d Weave Test main program... -@{ -if __name__ == "__main__": - import sys - logging.basicConfig( stream=sys.stdout, level= logging.WARN ) - unittest.main() -@} diff --git a/test/intro.w b/test/intro.w deleted file mode 100644 index 4fa7d2c..0000000 --- a/test/intro.w +++ /dev/null @@ -1,47 +0,0 @@ - - -

    There are two levels of testing in this document.

    - - -

    Other testing, like performance or security, is possible. -But for this application, not very interesting. -

    - -

    This doument builds a complete test suite, test.py. - -

    -MacBook-6:pyweb slott$ cd test
    -MacBook-6:test slott$ export PYTHONPATH=..
    -MacBook-6:test slott$ python -m pyweb pyweb_test.w
    -INFO:pyweb:Reading 'pyweb_test.w'
    -INFO:pyweb:Starting Load [WebReader, Web 'pyweb_test.w']
    -INFO:pyweb:Including 'intro.w'
    -INFO:pyweb:Including 'unit.w'
    -INFO:pyweb:Including 'func.w'
    -INFO:pyweb:Including 'combined.w'
    -INFO:pyweb:Starting Tangle [Web 'pyweb_test.w']
    -INFO:pyweb:Tangling 'test_unit.py'
    -INFO:pyweb:No change to 'test_unit.py'
    -INFO:pyweb:Tangling 'test_weaver.py'
    -INFO:pyweb:No change to 'test_weaver.py'
    -INFO:pyweb:Tangling 'test_tangler.py'
    -INFO:pyweb:No change to 'test_tangler.py'
    -INFO:pyweb:Tangling 'test.py'
    -INFO:pyweb:No change to 'test.py'
    -INFO:pyweb:Tangling 'test_loader.py'
    -INFO:pyweb:No change to 'test_loader.py'
    -INFO:pyweb:Starting Weave [Web 'pyweb_test.w', None]
    -INFO:pyweb:Weaving 'pyweb_test.html'
    -INFO:pyweb:Wrote 2519 lines to 'pyweb_test.html'
    -INFO:pyweb:pyWeb: Load 1695 lines from 5 files in 0 sec., Tangle 80 lines in 0.1 sec., Weave 2519 lines in 0.0 sec.
    -MacBook-6:test slott$ python test.py
    -.......................................................................
    -----------------------------------------------------------------------
    -Ran 71 tests in 2.043s
    -
    -OK
    -MacBook-6:test slott$ 
    -
    \ No newline at end of file diff --git a/test/pyweb_test.html b/test/pyweb_test.html deleted file mode 100644 index be94ae9..0000000 --- a/test/pyweb_test.html +++ /dev/null @@ -1,2704 +0,0 @@ - - - - - pyWeb Literate Programming 2.1 - Test Suite - - - - - -
    - - -

    pyWeb 2.1 Test Suite

    -

    In Python, Yet Another Literate Programming Tool

    -

    Steven F. Lott

    - -
    -

    Table of Contents

    - -
    - -

    Introduction

    -
    - - -

    There are two levels of testing in this document.

    - - -

    Other testing, like performance or security, is possible. -But for this application, not very interesting. -

    - -

    This doument builds a complete test suite, test.py. - -

    -MacBook-6:pyweb slott$ cd test
    -MacBook-6:test slott$ export PYTHONPATH=..
    -MacBook-6:test slott$ python -m pyweb pyweb_test.w
    -INFO:pyweb:Reading 'pyweb_test.w'
    -INFO:pyweb:Starting Load [WebReader, Web 'pyweb_test.w']
    -INFO:pyweb:Including 'intro.w'
    -INFO:pyweb:Including 'unit.w'
    -INFO:pyweb:Including 'func.w'
    -INFO:pyweb:Including 'combined.w'
    -INFO:pyweb:Starting Tangle [Web 'pyweb_test.w']
    -INFO:pyweb:Tangling 'test_unit.py'
    -INFO:pyweb:No change to 'test_unit.py'
    -INFO:pyweb:Tangling 'test_weaver.py'
    -INFO:pyweb:No change to 'test_weaver.py'
    -INFO:pyweb:Tangling 'test_tangler.py'
    -INFO:pyweb:No change to 'test_tangler.py'
    -INFO:pyweb:Tangling 'test.py'
    -INFO:pyweb:No change to 'test.py'
    -INFO:pyweb:Tangling 'test_loader.py'
    -INFO:pyweb:No change to 'test_loader.py'
    -INFO:pyweb:Starting Weave [Web 'pyweb_test.w', None]
    -INFO:pyweb:Weaving 'pyweb_test.html'
    -INFO:pyweb:Wrote 2519 lines to 'pyweb_test.html'
    -INFO:pyweb:pyWeb: Load 1695 lines from 5 files in 0 sec., Tangle 80 lines in 0.1 sec., Weave 2519 lines in 0.0 sec.
    -MacBook-6:test slott$ python test.py
    -.......................................................................
    -----------------------------------------------------------------------
    -Ran 71 tests in 2.043s
    -
    -OK
    -MacBook-6:test slott$ 
    -
    - -

    Unit Testing

    -
    - - - -

    There are several broad areas of unit testing. There are the 34 classes in this application. -However, it isn't really necessary to test everyone single one of these classes. -We'll decompose these into several hierarchies. -

    - -
      -
    • Emitters -
        -
      • class Emitter( object ):
      • -
      • class Weaver( Emitter ):
      • -
      • class LaTeX( Weaver ):
      • -
      • class HTML( Weaver ):
      • -
      • class HTMLShort( HTML ):
      • -
      • class Tangler( Emitter ):
      • -
      • class TanglerMake( Tangler ):
      • -
      -
    • -
    • Structure: Chunk, Command -
        -
      • class Chunk( object ):
      • -
      • class NamedChunk( Chunk ):
      • -
      • class OutputChunk( NamedChunk ):
      • -
      • class NamedDocumentChunk( NamedChunk ):
      • -
      • class MyNewCommand( Command ):
      • -
      • class Command( object ):
      • -
      • class TextCommand( Command ):
      • -
      • class CodeCommand( TextCommand ):
      • -
      • class XrefCommand( Command ):
      • -
      • class FileXrefCommand( XrefCommand ):
      • -
      • class MacroXrefCommand( XrefCommand ):
      • -
      • class UserIdXrefCommand( XrefCommand ):
      • -
      • class ReferenceCommand( Command ):
      • -
      -
    • -
    • class Error( Exception ): pass
    • -
    • Reference Handling -
        -
      • class Reference( object ):
      • -
      • class SimpleReference( Reference ):
      • -
      • class TransitiveReference( Reference ):
      • -
      -
    • -
    • class Web( object ):
    • -
    • class WebReader( object ):
    • -
    • Action -
        -
      • class Action( object ):
      • -
      • class ActionSequence( Action ):
      • -
      • class WeaveAction( Action ):
      • -
      • class TangleAction( Action ):
      • -
      • class LoadAction( Action ):
      • -
      -
    • -
    • class Application( object ):
    • -
    • class MyWeaver( HTML ):
    • -
    • class MyHTML( pyweb.HTML ):
    • -
    - -

    This gives us the following outline for unit testing.

    - - - -

    test_unit.py (1) =

    -
    
    -    Unit Test overheads: imports, etc. (45)    
    -    Unit Test of Emitter class hierarchy (2)    
    -    Unit Test of Chunk class hierarchy (11)    
    -    Unit Test of Command class hierarchy (22)    
    -    Unit Test of Reference class hierarchy (31)    
    -    Unit Test of Web class (32)    
    -    Unit Test of WebReader class (38)    
    -    Unit Test of Action class hierarchy (39)    
    -    Unit Test of Application class (44)    
    -    Unit Test main (46)    
    -
    -

    test_unit.py (1). - -

    - - -

    Emitter Tests

    - -

    The emitter class hierarchy produces output files; either woven output -which uses templates to generate proper markup, or tangled output which -precisely follows the document structure. -

    - - - - -

    Unit Test of Emitter class hierarchy (2) =

    -
    
    -
    -Unit Test Mock Chunk class (4)
    -Unit Test of Emitter Superclass (3)
    -Unit Test of Weaver subclass of Emitter (5)
    -Unit Test of LaTeX subclass of Emitter (6)
    -Unit Test of HTML subclass of Emitter (7)
    -Unit Test of HTMLShort subclass of Emitter (8)
    -Unit Test of Tangler subclass of Emitter (9)
    -Unit Test of TanglerMake subclass of Emitter (10)
    -
    -    
    -

    Unit Test of Emitter class hierarchy (2). - Used by test_unit.py (1). -

    - - -

    The Emitter superclass is designed to be extended. The test -creates a subclass to exercise a few key features.

    - - - - -

    Unit Test of Emitter Superclass (3) =

    -
    
    - 
    -class EmitterExtension( pyweb.Emitter ):
    -    def doOpen( self, fileName ):
    -        self.file= StringIO.StringIO()
    -    def doClose( self ):
    -        self.file.flush()
    -    def doWrite( self, text ):
    -        self.file.write( text )
    -        
    -class TestEmitter( unittest.TestCase ):
    -    def setUp( self ):
    -        self.emitter= EmitterExtension()
    -    def test_emitter_should_open_close_write( self ):
    -        self.emitter.open( "test.tmp" )
    -        self.emitter.write( "Something" )
    -        self.emitter.close()
    -        self.assertEquals( "Something", self.emitter.file.getvalue() )
    -    def test_emitter_should_codeBlock( self ):
    -        self.emitter.open( "test.tmp" )
    -        self.emitter.codeBlock( "Some Code" )
    -        self.emitter.close()
    -        self.assertEquals( "Some Code\n", self.emitter.file.getvalue() )
    -    def test_emitter_should_indent( self ):
    -        self.emitter.open( "test.tmp" )
    -        self.emitter.codeBlock( "Begin\n" )
    -        self.emitter.setIndent( 4 )
    -        self.emitter.codeBlock( "More Code\n" )
    -        self.emitter.clrIndent()
    -        self.emitter.codeBlock( "End" )
    -        self.emitter.close()
    -        self.assertEquals( "Begin\n    More Code\nEnd\n", self.emitter.file.getvalue() )
    -
    -    
    -

    Unit Test of Emitter Superclass (3). - Used by Unit Test of Emitter class hierarchy (2); test_unit.py (1). -

    - - -

    A Mock Chunk is a Chunk-like object that we can use to test Weavers.

    - - - - -

    Unit Test Mock Chunk class (4) =

    -
    
    -
    -class MockChunk( object ):
    -    def __init__( self, name, seq, lineNumber ):
    -        self.name= name
    -        self.fullName= name
    -        self.seq= seq
    -        self.lineNumber= lineNumber
    -        self.initial= True
    -        self.commands= []
    -        self.referencedBy= []
    -
    -    
    -

    Unit Test Mock Chunk class (4). - Used by Unit Test of Emitter class hierarchy (2); test_unit.py (1). -

    - - -

    The default Weaver is an Emitter that uses templates to produce RST markup.

    - - - - -

    Unit Test of Weaver subclass of Emitter (5) =

    -
    
    -
    -class TestWeaver( unittest.TestCase ):
    -    def setUp( self ):
    -        self.weaver= pyweb.Weaver()
    -        self.filename= "testweaver.w" 
    -        self.aFileChunk= MockChunk( "File", 123, 456 )
    -        self.aFileChunk.references_list= [ ]
    -        self.aChunk= MockChunk( "Chunk", 314, 278 )
    -        self.aChunk.references_list= [ ("Container", 123) ]
    -    def tearDown( self ):
    -        import os
    -        try:
    -            os.remove( "testweaver.rst" )
    -        except OSError:
    -            pass
    -        
    -    def test_weaver_functions( self ):
    -        result= self.weaver.quote( "|char| `code` *em* _em_" )
    -        self.assertEquals( "\|char\| \`code\` \*em\* \_em\_", result )
    -        result= self.weaver.references( self.aChunk )
    -        self.assertEquals( "\nUsed by: Container (`123`_)\n", result )
    -        result= self.weaver.referenceTo( "Chunk", 314 )
    -        self.assertEquals( "|srarr| Chunk (`314`_)", result )
    -  
    -    def test_weaver_should_codeBegin( self ):
    -        self.weaver.open( self.filename )
    -        self.weaver.codeBegin( self.aChunk )
    -        self.weaver.codeBlock( self.weaver.quote( "*The* `Code`\n" ) )
    -        self.weaver.codeEnd( self.aChunk )
    -        self.weaver.close()
    -        with open( "testweaver.rst", "r" ) as result:
    -            txt= result.read()
    -        self.assertEquals( "\n..  _`314`:\n..  rubric:: Chunk (314)\n..  parsed-literal::\n\n    \\*The\\* \\`Code\\`\n\n\nUsed by: Container (`123`_)\n\n\n", txt )
    -  
    -    def test_weaver_should_fileBegin( self ):
    -        self.weaver.open( self.filename )
    -        self.weaver.fileBegin( self.aFileChunk )
    -        self.weaver.codeBlock( self.weaver.quote( "*The* `Code`\n" ) )
    -        self.weaver.fileEnd( self.aFileChunk )
    -        self.weaver.close()
    -        with open( "testweaver.rst", "r" ) as result:
    -            txt= result.read()
    -        self.assertEquals( "\n..  _`123`:\n..  rubric:: File (123)\n..  parsed-literal::\n\n    \\*The\\* \\`Code\\`\n\n\n\n", txt )
    -
    -    def test_weaver_should_xref( self ):
    -        self.weaver.open( self.filename )
    -        self.weaver.xrefHead( )
    -        self.weaver.xrefLine( "Chunk", [ ("Container", 123) ] )
    -        self.weaver.xrefFoot( )
    -        self.weaver.fileEnd( self.aFileChunk )
    -        self.weaver.close()
    -        with open( "testweaver.rst", "r" ) as result:
    -            txt= result.read()
    -        self.assertEquals( "\n:Chunk:\n    |srarr| (`('Container', 123)`_)\n\n\n\n", txt )
    -
    -    def test_weaver_should_xref_def( self ):
    -        self.weaver.open( self.filename )
    -        self.weaver.xrefHead( )
    -        self.weaver.xrefDefLine( "Chunk", 314, [ ("Container", 123), ("Chunk", 314) ] )
    -        self.weaver.xrefFoot( )
    -        self.weaver.fileEnd( self.aFileChunk )
    -        self.weaver.close()
    -        with open( "testweaver.rst", "r" ) as result:
    -            txt= result.read()
    -        self.assertEquals( "\n:Chunk:\n    [`314`_] `('Chunk', 314)`_ `('Container', 123)`_\n\n\n\n", txt )
    -
    -    
    -

    Unit Test of Weaver subclass of Emitter (5). - Used by Unit Test of Emitter class hierarchy (2); test_unit.py (1). -

    - - -

    A significant fraction of the various subclasses of weaver are simply -expansion of templates. There's no real point in testing the template -expansion, since that's more easily tested by running a document -through pyweb and looking at the results. -

    - -

    We'll examine a few features of the LaTeX templates.

    - - - - -

    Unit Test of LaTeX subclass of Emitter (6) =

    -
    
    - 
    -class TestLaTeX( unittest.TestCase ):
    -    def setUp( self ):
    -        self.weaver= pyweb.LaTeX()
    -        self.filename= "testweaver.w" 
    -        self.aFileChunk= MockChunk( "File", 123, 456 )
    -        self.aFileChunk.references_list= [ ]
    -        self.aChunk= MockChunk( "Chunk", 314, 278 )
    -        self.aChunk.references_list= [ ("Container", 123) ]
    -    def tearDown( self ):
    -        import os
    -        try:
    -            os.remove( "testweaver.tex" )
    -        except OSError:
    -            pass
    -            
    -    def test_weaver_functions( self ):
    -        result= self.weaver.quote( "\\end{Verbatim}" )
    -        self.assertEquals( "\\end\\,{Verbatim}", result )
    -        result= self.weaver.references( self.aChunk )
    -        self.assertEquals( "\n    \\footnotesize\n    Used by:\n    \\begin{list}{}{}\n    \n    \\item Code example Container (123) (Sect. \\ref{pyweb123}, p. \\pageref{pyweb123})\n\n    \\end{list}\n    \\normalsize\n", result )
    -        result= self.weaver.referenceTo( "Chunk", 314 )
    -        self.assertEquals( "$\\triangleright$ Code Example Chunk (314)", result )
    -
    -    
    -

    Unit Test of LaTeX subclass of Emitter (6). - Used by Unit Test of Emitter class hierarchy (2); test_unit.py (1). -

    - - -

    We'll examine a few features of the HTML templates.

    - - - - -

    Unit Test of HTML subclass of Emitter (7) =

    -
    
    - 
    -class TestHTML( unittest.TestCase ):
    -    def setUp( self ):
    -        self.weaver= pyweb.HTML()
    -        self.filename= "testweaver.w" 
    -        self.aFileChunk= MockChunk( "File", 123, 456 )
    -        self.aFileChunk.references_list= [ ]
    -        self.aChunk= MockChunk( "Chunk", 314, 278 )
    -        self.aChunk.references_list= [ ("Container", 123) ]
    -    def tearDown( self ):
    -        import os
    -        try:
    -            os.remove( "testweaver.html" )
    -        except OSError:
    -            pass
    -            
    -    def test_weaver_functions( self ):
    -        result= self.weaver.quote( "a < b && c > d" )
    -        self.assertEquals( "a &lt; b &amp;&amp; c &gt; d", result )
    -        result= self.weaver.references( self.aChunk )
    -        self.assertEquals( '  Used by <a href="#pyweb123"><em>Container</em>&nbsp;(123)</a>.', result )
    -        result= self.weaver.referenceTo( "Chunk", 314 )
    -        self.assertEquals( '<a href="#pyweb314">&rarr;<em>Chunk</em> (314)</a>', result )
    -
    -
    -    
    -

    Unit Test of HTML subclass of Emitter (7). - Used by Unit Test of Emitter class hierarchy (2); test_unit.py (1). -

    - - -

    The unique feature of the HTMLShort class is just a template change. -

    - -

    To Do: Test this.

    - - - - -

    Unit Test of HTMLShort subclass of Emitter (8) =

    -
    
    - 
    -    
    -

    Unit Test of HTMLShort subclass of Emitter (8). - Used by Unit Test of Emitter class hierarchy (2); test_unit.py (1). -

    - - -

    A Tangler emits the various named source files in proper format for the desired -compiler and language.

    - - - - -

    Unit Test of Tangler subclass of Emitter (9) =

    -
    
    - 
    -class TestTangler( unittest.TestCase ):
    -    def setUp( self ):
    -        self.tangler= pyweb.Tangler()
    -        self.filename= "testtangler.w" 
    -        self.aFileChunk= MockChunk( "File", 123, 456 )
    -        self.aFileChunk.references_list= [ ]
    -        self.aChunk= MockChunk( "Chunk", 314, 278 )
    -        self.aChunk.references_list= [ ("Container", 123) ]
    -    def tearDown( self ):
    -        import os
    -        try:
    -            os.remove( "testtangler.w" )
    -        except OSError:
    -            pass
    -        
    -    def test_tangler_functions( self ):
    -        result= self.tangler.quote( string.printable )
    -        self.assertEquals( string.printable, result )
    -    def test_tangler_should_codeBegin( self ):
    -        self.tangler.open( self.filename )
    -        self.tangler.codeBegin( self.aChunk )
    -        self.tangler.codeBlock( self.tangler.quote( "*The* `Code`\n" ) )
    -        self.tangler.codeEnd( self.aChunk )
    -        self.tangler.close()
    -        with open( "testtangler.w", "r" ) as result:
    -            txt= result.read()
    -        self.assertEquals( "*The* `Code`\n", txt )
    -
    -    
    -

    Unit Test of Tangler subclass of Emitter (9). - Used by Unit Test of Emitter class hierarchy (2); test_unit.py (1). -

    - - -

    A TanglerMake uses a cheap hack to see if anything changed. -It creates a temporary file and then does a complete file difference -check. If the file is different, the old version is replaced with -the new version. If the file content is the same, the old version -is left intact with all of the operating system creation timestamps -untouched. -

    - -

    In order to be sure that the timestamps really have changed, we -need to wait for a full second to elapse. -

    - - - - - -

    Unit Test of TanglerMake subclass of Emitter (10) =

    -
    
    -
    -class TestTanglerMake( unittest.TestCase ):
    -    def setUp( self ):
    -        self.tangler= pyweb.TanglerMake()
    -        self.filename= "testtangler.w" 
    -        self.aChunk= MockChunk( "Chunk", 314, 278 )
    -        self.aChunk.references_list= [ ("Container", 123) ]
    -        self.tangler.open( self.filename )
    -        self.tangler.codeBegin( self.aChunk )
    -        self.tangler.codeBlock( self.tangler.quote( "*The* `Code`\n" ) )
    -        self.tangler.codeEnd( self.aChunk )
    -        self.tangler.close()
    -        self.original= os.path.getmtime( self.filename )
    -        time.sleep( 1.0 ) # Attempt to assure timestamps are different
    -    def tearDown( self ):
    -        import os
    -        try:
    -            os.remove( "testtangler.w" )
    -        except OSError:
    -            pass
    -        
    -    def test_same_should_leave( self ):
    -        self.tangler.open( self.filename )
    -        self.tangler.codeBegin( self.aChunk )
    -        self.tangler.codeBlock( self.tangler.quote( "*The* `Code`\n" ) )
    -        self.tangler.codeEnd( self.aChunk )
    -        self.tangler.close()
    -        self.assertEquals( self.original, os.path.getmtime( self.filename ) )
    -        
    -    def test_different_should_update( self ):
    -        self.tangler.open( self.filename )
    -        self.tangler.codeBegin( self.aChunk )
    -        self.tangler.codeBlock( self.tangler.quote( "*Completely Different* `Code`\n" ) )
    -        self.tangler.codeEnd( self.aChunk )
    -        self.tangler.close()
    -        self.assertNotEquals( self.original, os.path.getmtime( self.filename ) )
    -
    -    
    -

    Unit Test of TanglerMake subclass of Emitter (10). - Used by Unit Test of Emitter class hierarchy (2); test_unit.py (1). -

    - - -

    Chunk Tests

    - -

    The Chunk and Command class hierarchies model the input document -- the web -of chunks that are used to produce the documentation and the source files. -

    - - - - -

    Unit Test of Chunk class hierarchy (11) =

    -
    
    -
    -Unit Test of Chunk superclass (12)(13)(14)(15)
    -Unit Test of NamedChunk subclass (19)
    -Unit Test of OutputChunk subclass (20)
    -Unit Test of NamedDocumentChunk subclass (21)
    -
    -    
    -

    Unit Test of Chunk class hierarchy (11). - Used by test_unit.py (1). -

    - - -

    In order to test the Chunk superclass, we need several mock objects. -A Chunk contains one or more commands. A Chunk is a part of a Web. -Also, a Chunk is processed by a Tangler or a Weaver. We'll need -Mock objects for all of these relationships in which a Chunk participates. -

    - -

    A MockCommand can be attached to a Chunk.

    - - - - -

    Unit Test of Chunk superclass (12) =

    -
    
    -
    -class MockCommand( object ):
    -    def __init__( self ):
    -        self.lineNumber= 314
    -    def startswith( self, text ):
    -        return False
    -
    -    
    -

    Unit Test of Chunk superclass (12). - Used by Unit Test of Chunk class hierarchy (11); test_unit.py (1). -

    - - -

    A MockWeb can contain a Chunk.

    - - - - -

    Unit Test of Chunk superclass (13) +=

    -
    
    -
    -class MockWeb( object ):
    -    def __init__( self ):
    -        self.chunks= []
    -        self.wove= None
    -        self.tangled= None
    -    def add( self, aChunk ):
    -        self.chunks.append( aChunk )
    -    def addNamed( self, aChunk ):
    -        self.chunks.append( aChunk )
    -    def addOutput( self, aChunk ):
    -        self.chunks.append( aChunk )
    -    def fullNameFor( self, name ):
    -        return name
    -    def fileXref( self ):
    -        return { 'file':[1,2,3] }
    -    def chunkXref( self ):
    -        return { 'chunk':[4,5,6] }
    -    def userNamesXref( self ):
    -        return { 'name':(7,[8,9,10]) }
    -    def getchunk( self, name ):
    -        return [ MockChunk( name, 1, 314 ) ]
    -    def createUsedBy( self ):
    -        pass
    -    def weaveChunk( self, name, weaver ):
    -        weaver.write( name )
    -    def tangleChunk( self, name, tangler ):
    -        tangler.write( name )
    -    def weave( self, weaver ):
    -        self.wove= weaver
    -    def tangle( self, tangler ):
    -        self.tangled= tangler
    -
    -    
    -

    Unit Test of Chunk superclass (13). - Used by Unit Test of Chunk class hierarchy (11); test_unit.py (1). -

    - - -

    A MockWeaver or MockTangle can process a Chunk.

    - - - - -

    Unit Test of Chunk superclass (14) +=

    -
    
    -
    -class MockWeaver( object ):
    -    def __init__( self ):
    -        self.begin_chunk= []
    -        self.end_chunk= []
    -        self.written= []
    -        self.code_indent= None
    -    def quote( self, text ):
    -        return text.replace( "&", "&amp;" ) # token quoting
    -    def docBegin( self, aChunk ):
    -        self.begin_chunk.append( aChunk )
    -    def write( self, text ):
    -        self.written.append( text )
    -    def docEnd( self, aChunk ):
    -        self.end_chunk.append( aChunk )
    -    def codeBegin( self, aChunk ):
    -        self.begin_chunk.append( aChunk )
    -    def codeBlock( self, text ):
    -        self.written.append( text )
    -    def codeEnd( self, aChunk ):
    -        self.end_chunk.append( aChunk )
    -    def fileBegin( self, aChunk ):
    -        self.begin_chunk.append( aChunk )
    -    def fileEnd( self, aChunk ):
    -        self.end_chunk.append( aChunk )
    -    def setIndent( self, fixed=None, command=None ):
    -        pass
    -    def clrIndent( self ):
    -        pass
    -    def xrefHead( self ):
    -        pass
    -    def xrefLine( self, name, refList ):
    -        self.written.append( "%s %s" % ( name, refList ) )
    -    def xrefDefLine( self, name, defn, refList ):
    -        self.written.append( "%s %s %s" % ( name, defn, refList ) )
    -    def xrefFoot( self ):
    -        pass
    -    def open( self, aFile ):
    -        pass
    -    def close( self ):
    -        pass
    -    def referenceTo( self, name, seq ):
    -        pass
    -
    -class MockTangler( MockWeaver ):
    -    def __init__( self ):
    -        super( MockTangler, self ).__init__()
    -        self.context= [0]
    -
    -    
    -

    Unit Test of Chunk superclass (14). - Used by Unit Test of Chunk class hierarchy (11); test_unit.py (1). -

    - - -

    A Chunk is built, interrogated and then emitted.

    - - - - -

    Unit Test of Chunk superclass (15) +=

    -
    
    -
    -class TestChunk( unittest.TestCase ):
    -    def setUp( self ):
    -        self.theChunk= pyweb.Chunk()
    -    Unit Test of Chunk construction (16)
    -    Unit Test of Chunk interrogation (17)
    -    Unit Test of Chunk emission (18)
    -
    -    
    -

    Unit Test of Chunk superclass (15). - Used by Unit Test of Chunk class hierarchy (11); test_unit.py (1). -

    - - -

    Can we build a Chunk?

    - - - - -

    Unit Test of Chunk construction (16) =

    -
    
    -
    -def test_append_command_should_work( self ):
    -    cmd1= MockCommand()
    -    self.theChunk.append( cmd1 )
    -    self.assertEquals( 1, len(self.theChunk.commands ) )
    -    cmd2= MockCommand()
    -    self.theChunk.append( cmd2 )
    -    self.assertEquals( 2, len(self.theChunk.commands ) )
    -    
    -def test_append_initial_and_more_text_should_work( self ):
    -    self.theChunk.appendText( "hi mom" )
    -    self.assertEquals( 1, len(self.theChunk.commands ) )
    -    self.theChunk.appendText( "&more text" )
    -    self.assertEquals( 1, len(self.theChunk.commands ) )
    -    self.assertEquals( "hi mom&more text", self.theChunk.commands[0].text )
    -    
    -def test_append_following_text_should_work( self ):
    -    cmd1= MockCommand()
    -    self.theChunk.append( cmd1 )
    -    self.theChunk.appendText( "hi mom" )
    -    self.assertEquals( 2, len(self.theChunk.commands ) )
    -    
    -def test_append_to_web_should_work( self ):
    -    web= MockWeb()
    -    self.theChunk.webAdd( web )
    -    self.assertEquals( 1, len(web.chunks) )
    -
    -    
    -

    Unit Test of Chunk construction (16). - Used by Unit Test of Chunk superclass (15); Unit Test of Chunk class hierarchy (11); test_unit.py (1). -

    - - -

    Can we interrogate a Chunk?

    - - - - -

    Unit Test of Chunk interrogation (17) =

    -
    
    -
    -def test_leading_command_should_not_find( self ):
    -    self.assertFalse( self.theChunk.startswith( "hi mom" ) )
    -    cmd1= MockCommand()
    -    self.theChunk.append( cmd1 )
    -    self.assertFalse( self.theChunk.startswith( "hi mom" ) )
    -    self.theChunk.appendText( "hi mom" )
    -    self.assertEquals( 2, len(self.theChunk.commands ) )
    -    self.assertFalse( self.theChunk.startswith( "hi mom" ) )
    -    
    -def test_leading_text_should_not_find( self ):
    -    self.assertFalse( self.theChunk.startswith( "hi mom" ) )
    -    self.theChunk.appendText( "hi mom" )
    -    self.assertTrue( self.theChunk.startswith( "hi mom" ) )
    -    cmd1= MockCommand()
    -    self.theChunk.append( cmd1 )
    -    self.assertTrue( self.theChunk.startswith( "hi mom" ) )
    -    self.assertEquals( 2, len(self.theChunk.commands ) )
    -
    -def test_regexp_exists_should_find( self ):
    -    self.theChunk.appendText( "this chunk has many words" )
    -    pat= re.compile( r"\Wchunk\W" )
    -    found= self.theChunk.searchForRE(pat)
    -    self.assertTrue( found is self.theChunk )
    -def test_regexp_missing_should_not_find( self ):
    -    self.theChunk.appendText( "this chunk has many words" )
    -    pat= re.compile( "\Warpigs\W" )
    -    found= self.theChunk.searchForRE(pat)
    -    self.assertTrue( found is None )
    -    
    -def test_lineNumber_should_work( self ):
    -    self.assertTrue( self.theChunk.lineNumber is None )
    -    cmd1= MockCommand()
    -    self.theChunk.append( cmd1 )
    -    self.assertEqual( 314, self.theChunk.lineNumber )
    -
    -    
    -

    Unit Test of Chunk interrogation (17). - Used by Unit Test of Chunk superclass (15); Unit Test of Chunk class hierarchy (11); test_unit.py (1). -

    - - -

    Can we emit a Chunk with a weaver or tangler?

    - - - - -

    Unit Test of Chunk emission (18) =

    -
    
    -
    -def test_weave_should_work( self ):
    -    wvr = MockWeaver()
    -    web = MockWeb()
    -    self.theChunk.appendText( "this chunk has very & many words" )
    -    self.theChunk.weave( web, wvr )
    -    self.assertEquals( 1, len(wvr.begin_chunk) )
    -    self.assertTrue( wvr.begin_chunk[0] is self.theChunk )
    -    self.assertEquals( 1, len(wvr.end_chunk) )
    -    self.assertTrue( wvr.end_chunk[0] is self.theChunk )
    -    self.assertEquals(  "this chunk has very & many words", "".join( wvr.written ) )
    -    
    -def test_tangle_should_fail( self ):
    -    tnglr = MockTangler()
    -    web = MockWeb()
    -    self.theChunk.appendText( "this chunk has very & many words" )
    -    try:
    -        self.theChunk.tangle( web, tnglr )
    -        self.fail()
    -    except pyweb.Error, e:
    -        self.assertEquals( "Cannot tangle an anonymous chunk", e.args[0] )
    -
    -    
    -

    Unit Test of Chunk emission (18). - Used by Unit Test of Chunk superclass (15); Unit Test of Chunk class hierarchy (11); test_unit.py (1). -

    - - -

    The NamedChunk is created by a @d command. -Since it's named, it appears in the Web's index. Also, it is woven -and tangled differently than anonymous chunks. -

    - - - - -

    Unit Test of NamedChunk subclass (19) =

    -
    
    - 
    -class TestNamedChunk( unittest.TestCase ):
    -    def setUp( self ):
    -        self.theChunk= pyweb.NamedChunk( "Some Name..." )
    -        cmd= self.theChunk.makeContent( "the words & text of this Chunk" )
    -        self.theChunk.append( cmd )
    -        self.theChunk.setUserIDRefs( "index terms" )
    -        
    -    def test_should_find_xref_words( self ):
    -        self.assertEquals( 2, len(self.theChunk.getUserIDRefs()) )
    -        self.assertEquals( "index", self.theChunk.getUserIDRefs()[0] )
    -        self.assertEquals( "terms", self.theChunk.getUserIDRefs()[1] )
    -        
    -    def test_append_to_web_should_work( self ):
    -        web= MockWeb()
    -        self.theChunk.webAdd( web )
    -        self.assertEquals( 1, len(web.chunks) )
    -        
    -    def test_weave_should_work( self ):
    -        wvr = MockWeaver()
    -        web = MockWeb()
    -        self.theChunk.weave( web, wvr )
    -        self.assertEquals( 1, len(wvr.begin_chunk) )
    -        self.assertTrue( wvr.begin_chunk[0] is self.theChunk )
    -        self.assertEquals( 1, len(wvr.end_chunk) )
    -        self.assertTrue( wvr.end_chunk[0] is self.theChunk )
    -        self.assertEquals(  "the words &amp; text of this Chunk", "".join( wvr.written ) )
    -
    -    def test_tangle_should_work( self ):
    -        tnglr = MockTangler()
    -        web = MockWeb()
    -        self.theChunk.tangle( web, tnglr )
    -        self.assertEquals( 1, len(tnglr.begin_chunk) )
    -        self.assertTrue( tnglr.begin_chunk[0] is self.theChunk )
    -        self.assertEquals( 1, len(tnglr.end_chunk) )
    -        self.assertTrue( tnglr.end_chunk[0] is self.theChunk )
    -        self.assertEquals(  "the words & text of this Chunk", "".join( tnglr.written ) )
    -
    -    
    -

    Unit Test of NamedChunk subclass (19). - Used by Unit Test of Chunk class hierarchy (11); test_unit.py (1). -

    - - -

    The OutputChunk is created by a @o command. -Since it's named, it appears in the Web's index. Also, it is woven -and tangled differently than anonymous chunks. -

    - - - - -

    Unit Test of OutputChunk subclass (20) =

    -
    
    -
    -class TestOutputChunk( unittest.TestCase ):
    -    def setUp( self ):
    -        self.theChunk= pyweb.OutputChunk( "filename", "#", "" )
    -        cmd= self.theChunk.makeContent( "the words & text of this Chunk" )
    -        self.theChunk.append( cmd )
    -        self.theChunk.setUserIDRefs( "index terms" )
    -        
    -    def test_append_to_web_should_work( self ):
    -        web= MockWeb()
    -        self.theChunk.webAdd( web )
    -        self.assertEquals( 1, len(web.chunks) )
    -        
    -    def test_weave_should_work( self ):
    -        wvr = MockWeaver()
    -        web = MockWeb()
    -        self.theChunk.weave( web, wvr )
    -        self.assertEquals( 1, len(wvr.begin_chunk) )
    -        self.assertTrue( wvr.begin_chunk[0] is self.theChunk )
    -        self.assertEquals( 1, len(wvr.end_chunk) )
    -        self.assertTrue( wvr.end_chunk[0] is self.theChunk )
    -        self.assertEquals(  "the words &amp; text of this Chunk", "".join( wvr.written ) )
    -
    -    def test_tangle_should_work( self ):
    -        tnglr = MockTangler()
    -        web = MockWeb()
    -        self.theChunk.tangle( web, tnglr )
    -        self.assertEquals( 1, len(tnglr.begin_chunk) )
    -        self.assertTrue( tnglr.begin_chunk[0] is self.theChunk )
    -        self.assertEquals( 1, len(tnglr.end_chunk) )
    -        self.assertTrue( tnglr.end_chunk[0] is self.theChunk )
    -        self.assertEquals(  "the words & text of this Chunk", "".join( tnglr.written ) )
    -
    -    
    -

    Unit Test of OutputChunk subclass (20). - Used by Unit Test of Chunk class hierarchy (11); test_unit.py (1). -

    - - -

    The NamedDocumentChunk is a little-used feature.

    - - - - -

    Unit Test of NamedDocumentChunk subclass (21) =

    -
    
    - 
    -    
    -

    Unit Test of NamedDocumentChunk subclass (21). - Used by Unit Test of Chunk class hierarchy (11); test_unit.py (1). -

    - - -

    Command Tests

    - - - - -

    Unit Test of Command class hierarchy (22) =

    -
    
    - 
    -Unit Test of Command superclass (23)
    -Unit Test of TextCommand class to contain a document text block (24)
    -Unit Test of CodeCommand class to contain a program source code block (25)
    -Unit Test of XrefCommand superclass for all cross-reference commands (26)
    -Unit Test of FileXrefCommand class for an output file cross-reference (27)
    -Unit Test of MacroXrefCommand class for a named chunk cross-reference (28)
    -Unit Test of UserIdXrefCommand class for a user identifier cross-reference (29)
    -Unit Test of ReferenceCommand class for chunk references (30)
    -
    -    
    -

    Unit Test of Command class hierarchy (22). - Used by test_unit.py (1). -

    - - -

    This Command superclass is essentially an inteface definition, it -has no real testable features.

    - - - -

    Unit Test of Command superclass (23) =

    -
    
    - 
    -    
    -

    Unit Test of Command superclass (23). - Used by Unit Test of Command class hierarchy (22); test_unit.py (1). -

    - - -

    A TextCommand object must be constructed, interrogated and emitted.

    - - - - -

    Unit Test of TextCommand class to contain a document text block (24) =

    -
    
    - 
    -class TestTextCommand( unittest.TestCase ):
    -    def setUp( self ):
    -        self.cmd= pyweb.TextCommand( "Some text & words in the document\n    ", 314 )
    -        self.cmd2= pyweb.TextCommand( "No Indent\n", 314 )
    -    def test_methods_should_work( self ):
    -        self.assertTrue( self.cmd.startswith("Some") )
    -        self.assertFalse( self.cmd.startswith("text") )
    -        pat1= re.compile( r"\Wthe\W" )
    -        self.assertTrue( self.cmd.searchForRE(pat1) is not None )
    -        pat2= re.compile( r"\Wnothing\W" )
    -        self.assertTrue( self.cmd.searchForRE(pat2) is None )
    -        self.assertEquals( 4, self.cmd.indent() )
    -        self.assertEquals( 0, self.cmd2.indent() )
    -    def test_weave_should_work( self ):
    -        wvr = MockWeaver()
    -        web = MockWeb()
    -        self.cmd.weave( web, wvr )
    -        self.assertEquals(  "Some text & words in the document\n    ", "".join( wvr.written ) )
    -    def test_tangle_should_work( self ):
    -        tnglr = MockTangler()
    -        web = MockWeb()
    -        self.cmd.tangle( web, tnglr )
    -        self.assertEquals(  "Some text & words in the document\n    ", "".join( tnglr.written ) )
    -
    -    
    -

    Unit Test of TextCommand class to contain a document text block (24). - Used by Unit Test of Command class hierarchy (22); test_unit.py (1). -

    - - -

    A CodeCommand object is a TextCommand with different processing for being emitted.

    - - - - -

    Unit Test of CodeCommand class to contain a program source code block (25) =

    -
    
    -
    -class TestCodeCommand( unittest.TestCase ):
    -    def setUp( self ):
    -        self.cmd= pyweb.CodeCommand( "Some text & words in the document\n    ", 314 )
    -    def test_weave_should_work( self ):
    -        wvr = MockWeaver()
    -        web = MockWeb()
    -        self.cmd.weave( web, wvr )
    -        self.assertEquals(  "Some text &amp; words in the document\n    ", "".join( wvr.written ) )
    -    def test_tangle_should_work( self ):
    -        tnglr = MockTangler()
    -        web = MockWeb()
    -        self.cmd.tangle( web, tnglr )
    -        self.assertEquals(  "Some text & words in the document\n    ", "".join( tnglr.written ) )
    -
    -    
    -

    Unit Test of CodeCommand class to contain a program source code block (25). - Used by Unit Test of Command class hierarchy (22); test_unit.py (1). -

    - - -

    The XrefCommand class is largely abstract.

    - - - - -

    Unit Test of XrefCommand superclass for all cross-reference commands (26) =

    -
    
    - 
    -    
    -

    Unit Test of XrefCommand superclass for all cross-reference commands (26). - Used by Unit Test of Command class hierarchy (22); test_unit.py (1). -

    - - -

    The FileXrefCommand command is expanded by a weaver to a list of all @o -locations.

    - - - - -

    Unit Test of FileXrefCommand class for an output file cross-reference (27) =

    -
    
    - 
    -class TestFileXRefCommand( unittest.TestCase ):
    -    def setUp( self ):
    -        self.cmd= pyweb.FileXrefCommand( 314 )
    -    def test_weave_should_work( self ):
    -        wvr = MockWeaver()
    -        web = MockWeb()
    -        self.cmd.weave( web, wvr )
    -        self.assertEquals(  "file [1, 2, 3]", "".join( wvr.written ) )
    -    def test_tangle_should_fail( self ):
    -        tnglr = MockTangler()
    -        web = MockWeb()
    -        try:
    -            self.cmd.tangle( web, tnglr )
    -            self.fail()
    -        except pyweb.Error:
    -            pass
    -
    -    
    -

    Unit Test of FileXrefCommand class for an output file cross-reference (27). - Used by Unit Test of Command class hierarchy (22); test_unit.py (1). -

    - - -

    The MacroXrefCommand command is expanded by a weaver to a list of all @d -locations.

    - - - - -

    Unit Test of MacroXrefCommand class for a named chunk cross-reference (28) =

    -
    
    -
    -class TestMacroXRefCommand( unittest.TestCase ):
    -    def setUp( self ):
    -        self.cmd= pyweb.MacroXrefCommand( 314 )
    -    def test_weave_should_work( self ):
    -        wvr = MockWeaver()
    -        web = MockWeb()
    -        self.cmd.weave( web, wvr )
    -        self.assertEquals(  "chunk [4, 5, 6]", "".join( wvr.written ) )
    -    def test_tangle_should_fail( self ):
    -        tnglr = MockTangler()
    -        web = MockWeb()
    -        try:
    -            self.cmd.tangle( web, tnglr )
    -            self.fail()
    -        except pyweb.Error:
    -            pass
    -
    -    
    -

    Unit Test of MacroXrefCommand class for a named chunk cross-reference (28). - Used by Unit Test of Command class hierarchy (22); test_unit.py (1). -

    - - -

    The UserIdXrefCommand command is expanded by a weaver to a list of all @| -names.

    - - - - -

    Unit Test of UserIdXrefCommand class for a user identifier cross-reference (29) =

    -
    
    -
    -class TestUserIdXrefCommand( unittest.TestCase ):
    -    def setUp( self ):
    -        self.cmd= pyweb.UserIdXrefCommand( 314 )
    -    def test_weave_should_work( self ):
    -        wvr = MockWeaver()
    -        web = MockWeb()
    -        self.cmd.weave( web, wvr )
    -        self.assertEquals(  "name 7 [8, 9, 10]", "".join( wvr.written ) )
    -    def test_tangle_should_fail( self ):
    -        tnglr = MockTangler()
    -        web = MockWeb()
    -        try:
    -            self.cmd.tangle( web, tnglr )
    -            self.fail()
    -        except pyweb.Error:
    -            pass
    -
    -    
    -

    Unit Test of UserIdXrefCommand class for a user identifier cross-reference (29). - Used by Unit Test of Command class hierarchy (22); test_unit.py (1). -

    - - -

    Reference commands require a context when tangling. -The context helps provide the required indentation. -They can't be simply tangled. -

    - - - - -

    Unit Test of ReferenceCommand class for chunk references (30) =

    -
    
    - 
    -class TestReferenceCommand( unittest.TestCase ):
    -    def setUp( self ):
    -        self.chunk= MockChunk( "Owning Chunk", 123, 456 )
    -        self.cmd= pyweb.ReferenceCommand( "Some Name", 314 )
    -        self.cmd.chunk= self.chunk
    -        self.chunk.commands.append( self.cmd )
    -        self.chunk.previous_command= pyweb.TextCommand( "", self.chunk.commands[0].lineNumber )
    -    def test_weave_should_work( self ):
    -        wvr = MockWeaver()
    -        web = MockWeb()
    -        self.cmd.weave( web, wvr )
    -        self.assertEquals(  "Some Name", "".join( wvr.written ) )
    -    def test_tangle_should_work( self ):
    -        tnglr = MockTangler()
    -        web = MockWeb()
    -        self.cmd.tangle( web, tnglr )
    -        self.assertEquals(  "Some Name", "".join( tnglr.written ) )
    -
    -    
    -

    Unit Test of ReferenceCommand class for chunk references (30). - Used by Unit Test of Command class hierarchy (22); test_unit.py (1). -

    - - -

    Reference Tests

    - -

    The Reference class implements one of two search strategies for -cross-references. Either simple (or "immediate") or transitive. -

    - -

    The superclass is little more than an interface definition, -it's completely abstract. The two subclasses differ in -a single method. -

    - - - - -

    Unit Test of Reference class hierarchy (31) =

    -
    
    - 
    -class TestReference( unittest.TestCase ):
    -    def setUp( self ):
    -        self.web= MockWeb()
    -        self.main= MockChunk( "Main", 1, 11 )
    -        self.parent= MockChunk( "Parent", 2, 22 )
    -        self.parent.referencedBy= [ self.main ]
    -        self.chunk= MockChunk( "Sub", 3, 33 )
    -        self.chunk.referencedBy= [ self.parent ]
    -    def test_simple_should_find_one( self ):
    -        self.reference= pyweb.SimpleReference( self.web )
    -        theList= self.reference.chunkReferencedBy( self.chunk )
    -        self.assertEquals( 1, len(theList) )
    -        self.assertEquals( ('Parent',2), theList[0] )
    -    def test_transitive_should_find_all( self ):
    -        self.reference= pyweb.TransitiveReference( self.web )
    -        theList= self.reference.chunkReferencedBy( self.chunk )
    -        self.assertEquals( 2, len(theList) )
    -        self.assertEquals( ('Parent',2), theList[0] )
    -        self.assertEquals( ('Main',1), theList[1] )
    -
    -    
    -

    Unit Test of Reference class hierarchy (31). - Used by test_unit.py (1). -

    - - -

    Web Tests

    - -

    This is more difficult to create mocks for.

    - - - - -

    Unit Test of Web class (32) =

    -
    
    - 
    -class TestWebConstruction( unittest.TestCase ):
    -    def setUp( self ):
    -        self.web= pyweb.Web( "Test" )
    -    Unit Test Web class construction methods (33)
    -    
    -class TestWebProcessing( unittest.TestCase ):
    -    def setUp( self ):
    -        self.web= pyweb.Web( "Test" )
    -        self.chunk= pyweb.Chunk()
    -        self.chunk.appendText( "some text" )
    -        self.chunk.webAdd( self.web )
    -        self.out= pyweb.OutputChunk( "A File" )
    -        self.out.appendText( "some code" )
    -        nm= self.web.addDefName( "A Chunk" )
    -        self.out.append( pyweb.ReferenceCommand( nm ) )
    -        self.out.webAdd( self.web )
    -        self.named= pyweb.NamedChunk( "A Chunk..." )
    -        self.named.appendText( "some user2a code" )
    -        self.named.setUserIDRefs( "user1" )
    -        nm= self.web.addDefName( "Another Chunk" )
    -        self.named.append( pyweb.ReferenceCommand( nm ) )
    -        self.named.webAdd( self.web )
    -        self.named2= pyweb.NamedChunk( "Another Chunk..." )
    -        self.named2.appendText(  "some user1 code"  )
    -        self.named2.setUserIDRefs( "user2a user2b" )
    -        self.named2.webAdd( self.web )
    -    Unit Test Web class name resolution methods (34)
    -    Unit Test Web class chunk cross-reference (35)
    -    Unit Test Web class tangle (36)
    -    Unit Test Web class weave (37)
    -
    -    
    -

    Unit Test of Web class (32). - Used by test_unit.py (1). -

    - - - - - -

    Unit Test Web class construction methods (33) =

    -
    
    -
    -def test_names_definition_should_resolve( self ):
    -    name1= self.web.addDefName( "A Chunk..." )
    -    self.assertTrue( name1 is None )
    -    self.assertEquals( 0, len(self.web.named) )
    -    name2= self.web.addDefName( "A Chunk Of Code" )
    -    self.assertEquals( "A Chunk Of Code", name2 )
    -    self.assertEquals( 1, len(self.web.named) )
    -    name3= self.web.addDefName( "A Chunk..." )
    -    self.assertEquals( "A Chunk Of Code", name3 )
    -    self.assertEquals( 1, len(self.web.named) )
    -    
    -def test_chunks_should_add_and_index( self ):
    -    chunk= pyweb.Chunk()
    -    chunk.appendText( "some text" )
    -    chunk.webAdd( self.web )
    -    self.assertEquals( 1, len(self.web.chunkSeq) )
    -    self.assertEquals( 0, len(self.web.named) )
    -    self.assertEquals( 0, len(self.web.output) )
    -    named= pyweb.NamedChunk( "A Chunk" )
    -    named.appendText( "some code" )
    -    named.webAdd( self.web )
    -    self.assertEquals( 2, len(self.web.chunkSeq) )
    -    self.assertEquals( 1, len(self.web.named) )
    -    self.assertEquals( 0, len(self.web.output) )
    -    out= pyweb.OutputChunk( "A File" )
    -    out.appendText( "some code" )
    -    out.webAdd( self.web )
    -    self.assertEquals( 3, len(self.web.chunkSeq) )
    -    self.assertEquals( 1, len(self.web.named) )
    -    self.assertEquals( 1, len(self.web.output) )
    -
    -    
    -

    Unit Test Web class construction methods (33). - Used by Unit Test of Web class (32); test_unit.py (1). -

    - - - - - -

    Unit Test Web class name resolution methods (34) =

    -
    
    - 
    -def test_name_queries_should_resolve( self ):
    -    self.assertEquals( "A Chunk", self.web.fullNameFor( "A C..." ) )    
    -    self.assertEquals( "A Chunk", self.web.fullNameFor( "A Chunk" ) )    
    -    self.assertNotEquals( "A Chunk", self.web.fullNameFor( "A File" ) )
    -    self.assertTrue( self.named is self.web.getchunk( "A C..." )[0] )
    -    self.assertTrue( self.named is self.web.getchunk( "A Chunk" )[0] )
    -    try:
    -        self.assertTrue( None is not self.web.getchunk( "A File" ) )
    -        self.fail()
    -    except pyweb.Error, e:
    -        self.assertTrue( e.args[0].startswith("Cannot resolve 'A File'") )  
    -
    -    
    -

    Unit Test Web class name resolution methods (34). - Used by Unit Test of Web class (32); test_unit.py (1). -

    - - - - - -

    Unit Test Web class chunk cross-reference (35) =

    -
    
    - 
    -def test_valid_web_should_createUsedBy( self ):
    -    self.web.createUsedBy()
    -    # If it raises an exception, the web structure is damaged
    -def test_valid_web_should_createFileXref( self ):
    -    file_xref= self.web.fileXref()
    -    self.assertEquals( 1, len(file_xref) )
    -    self.assertTrue( "A File" in file_xref ) 
    -    self.assertTrue( 1, len(file_xref["A File"]) )
    -def test_valid_web_should_createChunkXref( self ):
    -    chunk_xref= self.web.chunkXref()
    -    self.assertEquals( 2, len(chunk_xref) )
    -    self.assertTrue( "A Chunk" in chunk_xref )
    -    self.assertEquals( 1, len(chunk_xref["A Chunk"]) )
    -    self.assertTrue( "Another Chunk" in chunk_xref )
    -    self.assertEquals( 1, len(chunk_xref["Another Chunk"]) )
    -    self.assertFalse( "Not A Real Chunk" in chunk_xref )
    -def test_valid_web_should_create_userNamesXref( self ):
    -    user_xref= self.web.userNamesXref() 
    -    self.assertEquals( 3, len(user_xref) )
    -    self.assertTrue( "user1" in user_xref )
    -    defn, reflist= user_xref["user1"]
    -    self.assertEquals( 1, len(reflist), "did not find user1" )
    -    self.assertTrue( "user2a" in user_xref )
    -    defn, reflist= user_xref["user2a"]
    -    self.assertEquals( 1, len(reflist), "did not find user2a" )
    -    self.assertTrue( "user2b" in user_xref )
    -    defn, reflist= user_xref["user2b"]
    -    self.assertEquals( 0, len(reflist) )
    -    self.assertFalse( "Not A User Symbol" in user_xref )
    -
    -    
    -

    Unit Test Web class chunk cross-reference (35). - Used by Unit Test of Web class (32); test_unit.py (1). -

    - - - - - -

    Unit Test Web class tangle (36) =

    -
    
    - 
    -def test_valid_web_should_tangle( self ):
    -    tangler= MockTangler()
    -    self.web.tangle( tangler )
    -    self.assertEquals( 3, len(tangler.written) )
    -    self.assertEquals( ['some code', 'some user2a code', 'some user1 code'], tangler.written )
    -
    -    
    -

    Unit Test Web class tangle (36). - Used by Unit Test of Web class (32); test_unit.py (1). -

    - - - - - -

    Unit Test Web class weave (37) =

    -
    
    - 
    -def test_valid_web_should_weave( self ):
    -    weaver= MockWeaver()
    -    self.web.weave( weaver )
    -    self.assertEquals( 6, len(weaver.written) )
    -    expected= ['some text', 'some code', None, 'some user2a code', None, 'some user1 code']
    -    self.assertEquals( expected, weaver.written )
    -
    -    
    -

    Unit Test Web class weave (37). - Used by Unit Test of Web class (32); test_unit.py (1). -

    - - - -

    WebReader Tests

    - -

    Generally, this is tested separately through the functional tests. -Those tests each present source files to be processed by the -WebReader. -

    - - - - -

    Unit Test of WebReader class (38) =

    -
    
    - 
    -    
    -

    Unit Test of WebReader class (38). - Used by test_unit.py (1). -

    - - -

    Action Tests

    - -

    Each class is tested separately. Sequence of some mocks, -load, tangle, weave. -

    - - - - -

    Unit Test of Action class hierarchy (39) =

    -
    
    - 
    -Unit test of Action Sequence class (40)
    -Unit test of LoadAction class (43)
    -Unit test of TangleAction class (42)
    -Unit test of WeaverAction class (41)
    -
    -    
    -

    Unit Test of Action class hierarchy (39). - Used by test_unit.py (1). -

    - - - - - -

    Unit test of Action Sequence class (40) =

    -
    
    -
    -class MockAction( object ):
    -    def __init__( self ):
    -        self.count= 0
    -    def __call__( self ):
    -        self.count += 1
    -        
    -class MockWebReader( object ):
    -    def __init__( self ):
    -        self.count= 0
    -        self.theWeb= None
    -    def web( self, aWeb ):
    -        self.theWeb= aWeb
    -        return self
    -    def load( self ):
    -        self.count += 1
    -    
    -class TestActionSequence( unittest.TestCase ):
    -    def setUp( self ):
    -        self.web= MockWeb()
    -        self.a1= MockAction()
    -        self.a2= MockAction()
    -        self.action= pyweb.ActionSequence( "TwoSteps", [self.a1, self.a2] )
    -        self.action.web= self.web
    -    def test_should_execute_both( self ):
    -        self.action()
    -        for c in self.action.opSequence:
    -            self.assertEquals( 1, c.count )
    -            self.assertTrue( self.web is c.web )
    -
    -    
    -

    Unit test of Action Sequence class (40). - Used by Unit Test of Action class hierarchy (39); test_unit.py (1). -

    - - - - - -

    Unit test of WeaverAction class (41) =

    -
    
    - 
    -class TestWeaveAction( unittest.TestCase ):
    -    def setUp( self ):
    -        self.web= MockWeb()
    -        self.action= pyweb.WeaveAction(  )
    -        self.weaver= MockWeaver()
    -        self.action.theWeaver= self.weaver
    -        self.action.web= self.web
    -    def test_should_execute_weaving( self ):
    -        self.action()
    -        self.assertTrue( self.web.wove is self.weaver )
    -
    -    
    -

    Unit test of WeaverAction class (41). - Used by Unit Test of Action class hierarchy (39); test_unit.py (1). -

    - - - - - -

    Unit test of TangleAction class (42) =

    -
    
    - 
    -class TestTangleAction( unittest.TestCase ):
    -    def setUp( self ):
    -        self.web= MockWeb()
    -        self.action= pyweb.TangleAction(  )
    -        self.tangler= MockTangler()
    -        self.action.theTangler= self.tangler
    -        self.action.web= self.web
    -    def test_should_execute_tangling( self ):
    -        self.action()
    -        self.assertTrue( self.web.tangled is self.tangler )
    -
    -    
    -

    Unit test of TangleAction class (42). - Used by Unit Test of Action class hierarchy (39); test_unit.py (1). -

    - - - - - -

    Unit test of LoadAction class (43) =

    -
    
    - 
    -class TestLoadAction( unittest.TestCase ):
    -    def setUp( self ):
    -        self.web= MockWeb()
    -        self.action= pyweb.LoadAction(  )
    -        self.webReader= MockWebReader()
    -        self.webReader.theWeb= self.web
    -        self.action.webReader= self.webReader
    -        self.action.web= self.web
    -    def test_should_execute_tangling( self ):
    -        self.action()
    -        self.assertEquals( 1, self.webReader.count )
    -
    -    
    -

    Unit test of LoadAction class (43). - Used by Unit Test of Action class hierarchy (39); test_unit.py (1). -

    - - -

    Application Tests

    - -

    As with testing WebReader, this requires extensive mocking. -It's easier to simply run the various use cases. -

    - - - - -

    Unit Test of Application class (44) =

    -
    
    - 
    -    
    -

    Unit Test of Application class (44). - Used by test_unit.py (1). -

    - - -

    Overheads and Main Script

    - -

    The boilerplate code for unit testing is the following.

    - - - - -

    Unit Test overheads: imports, etc. (45) =

    -
    
    -from __future__ import print_function
    -"""Unit tests."""
    -import pyweb
    -import unittest
    -import logging
    -import StringIO
    -import string
    -import os
    -import time
    -import re
    -
    -    
    -

    Unit Test overheads: imports, etc. (45). - Used by test_unit.py (1). -

    - - - - - -

    Unit Test main (46) =

    -
    
    -
    -if __name__ == "__main__":
    -    import sys
    -    logging.basicConfig( stream=sys.stdout, level= logging.WARN )
    -    unittest.main()
    -
    -    
    -

    Unit Test main (46). - Used by test_unit.py (1). -

    -
    - -

    Functional Testing

    -
    - - - -

    There are three broad areas of functional testing.

    - -
      -
    • Loading
    • -
    • Tanging
    • -
    • Weaving
    • -
    - -

    There are a total of 11 test cases.

    - -

    Tests for Loading

    - -

    We need to be able to load a web from one or more source files.

    - - - -

    test_loader.py (47) =

    -
    
    -    Load Test overheads: imports, etc. (53)    
    -    Load Test superclass to refactor common setup (48)    
    -    Load Test error handling with a few common syntax errors (49)    
    -    Load Test include processing with syntax errors (51)    
    -    Load Test main program (54)    
    -
    -

    test_loader.py (47). - -

    - - -

    Parsing test cases have a common setup shown in this superclass.

    - -

    By using some class-level variables text, -file_name, we can simply provide a file-like -input object to the WebReader instance. -

    - - - - -

    Load Test superclass to refactor common setup (48) =

    -
    
    -
    -class ParseTestcase( unittest.TestCase ):
    -    text= ""
    -    file_name= ""
    -    def setUp( self ):
    -        source= StringIO.StringIO( self.text )
    -        self.web= pyweb.Web( self.file_name )
    -        self.rdr= pyweb.WebReader()
    -        self.rdr.source( self.file_name, source ).web( self.web )
    -
    -    
    -

    Load Test superclass to refactor common setup (48). - Used by test_loader.py (47). -

    - - -

    There are a lot of specific parsing exceptions which can be thrown. -We'll cover most of the cases with a quick check for a failure to -find an expected next token. -

    - - - - -

    Load Test error handling with a few common syntax errors (49) =

    -
    
    -
    -Sample Document 1 with correct and incorrect syntax (50)
    -
    -class Test_ParseErrors( ParseTestcase ):
    -    text= test1_w
    -    file_name= "test1.w"
    -    def test_should_raise_syntax( self ):
    -        try:
    -            self.rdr.load()
    -            self.fail( "Should not parse" )
    -        except pyweb.Error, e:
    -            self.assertEquals( "At ('test1.w', 8, 8): expected ('@{',), found '@o'", e.args[0] )
    -
    -    
    -

    Load Test error handling with a few common syntax errors (49). - Used by test_loader.py (47). -

    - - - - - -

    Sample Document 1 with correct and incorrect syntax (50) =

    -
    
    -
    -test1_w= """Some anonymous chunk
    -@o test1.tmp
    -@{@<part1@>
    -@<part2@>
    -@}@@
    -@d part1 @{This is part 1.@}
    -Okay, now for an error.
    -@o show how @o commands work
    -@{ @{ @] @]
    -"""
    -
    -    
    -

    Sample Document 1 with correct and incorrect syntax (50). - Used by Load Test error handling with a few common syntax errors (49); test_loader.py (47). -

    - - -

    All of the parsing exceptions should be correctly identified with -any included file. -We'll cover most of the cases with a quick check for a failure to -find an expected next token. -

    - -

    In order to handle the include file processing, we have to actually -create a temporary file. It's hard to mock the include processing. -

    - - - - -

    Load Test include processing with syntax errors (51) =

    -
    
    -
    -Sample Document 8 and the file it includes (52)
    -
    -class Test_IncludeParseErrors( ParseTestcase ):
    -    text= test8_w
    -    file_name= "test8.w"
    -    def setUp( self ):
    -        with open('test8_inc.tmp','w') as temp:
    -            temp.write( test8_inc_w )
    -        super( Test_IncludeParseErrors, self ).setUp()
    -    def test_should_raise_include_syntax( self ):
    -        try:
    -            self.rdr.load()
    -            self.fail( "Should not parse" )
    -        except pyweb.Error, e:
    -            self.assertEquals( "At ('test8_inc.tmp', 3, 4): end of input, ('@{', '@[') not found", e.args[0] )
    -    def tearDown( self ):
    -        os.remove( 'test8_inc.tmp' )
    -        super( Test_IncludeParseErrors, self ).tearDown()
    -
    -    
    -

    Load Test include processing with syntax errors (51). - Used by test_loader.py (47). -

    - - -

    The sample document must reference the correct name that will -be given to the included document by setUp. -

    - - - - -

    Sample Document 8 and the file it includes (52) =

    -
    
    -
    -test8_w= """Some anonymous chunk.
    -@d title @[the title of this document, defined with @@[ and @@]@]
    -A reference to @<title@>.
    -@i test8_inc.tmp
    -A final anonymous chunk from test8.w
    -"""
    -
    -test8_inc_w="""A chunk from test8a.w
    -And now for an error - incorrect syntax in an included file!
    -@d yap
    -"""
    -
    -    
    -

    Sample Document 8 and the file it includes (52). - Used by Load Test include processing with syntax errors (51); test_loader.py (47). -

    - - -

    The overheads for a Python unittest.

    - - - - -

    Load Test overheads: imports, etc. (53) =

    -
    
    -from __future__ import print_function
    -"""Loader and parsing tests."""
    -import pyweb
    -import unittest
    -import logging
    -import StringIO
    -import os
    -
    -    
    -

    Load Test overheads: imports, etc. (53). - Used by test_loader.py (47). -

    - - -

    A main program that configures logging and then runs the test.

    - - - - -

    Load Test main program (54) =

    -
    
    -
    -if __name__ == "__main__":
    -    import sys
    -    logging.basicConfig( stream=sys.stdout, level= logging.WARN )
    -    unittest.main()
    -
    -    
    -

    Load Test main program (54). - Used by test_loader.py (47). -

    - - -

    Tests for Tangling

    - -

    We need to be able to tangle a web.

    - - - -

    test_tangler.py (55) =

    -
    
    -    Tangle Test overheads: imports, etc. (69)    
    -    Tangle Test superclass to refactor common setup (56)    
    -    Tangle Test semantic error 2 (57)    
    -    Tangle Test semantic error 3 (59)    
    -    Tangle Test semantic error 4 (61)    
    -    Tangle Test semantic error 5 (63)    
    -    Tangle Test semantic error 6 (65)    
    -    Tangle Test include error 7 (67)    
    -    Tangle Test main program (70)    
    -
    -

    test_tangler.py (55). - -

    - - -

    Tangling test cases have a common setup and teardown shown in this superclass. -Since tangling must produce a file, it's helpful to remove the file that gets created. -The essential test case is to load and attempt to tangle, checking the -exceptions raised. -

    - - - - -

    Tangle Test superclass to refactor common setup (56) =

    -
    
    -
    -class TangleTestcase( unittest.TestCase ):
    -    text= ""
    -    file_name= ""
    -    error= ""
    -    def setUp( self ):
    -        source= StringIO.StringIO( self.text )
    -        self.web= pyweb.Web( self.file_name )
    -        self.rdr= pyweb.WebReader()
    -        self.rdr.source( self.file_name, source ).web( self.web )
    -        self.tangler= pyweb.Tangler()
    -    def tangle_and_check_exception( self, exception_text ):
    -        try:
    -            self.rdr.load()
    -            self.web.tangle( self.tangler )
    -            self.web.createUsedBy()
    -            self.fail( "Should not tangle" )
    -        except pyweb.Error, e:
    -            self.assertEquals( exception_text, e.args[0] )
    -    def tearDown( self ):
    -        name, _ = os.path.splitext( self.file_name )
    -        try:
    -            os.remove( name + ".tmp" )
    -        except OSError:
    -            pass
    -
    -    
    -

    Tangle Test superclass to refactor common setup (56). - Used by test_tangler.py (55). -

    - - - - - -

    Tangle Test semantic error 2 (57) =

    -
    
    -
    -Sample Document 2 (58)
    -
    -class Test_SemanticError_2( TangleTestcase ):
    -    text= test2_w
    -    file_name= "test2.w"
    -    def test_should_raise_undefined( self ):
    -        self.tangle_and_check_exception( "Attempt to tangle an undefined Chunk, part2." )
    -
    -    
    -

    Tangle Test semantic error 2 (57). - Used by test_tangler.py (55). -

    - - - - - -

    Sample Document 2 (58) =

    -
    
    -
    -test2_w= """Some anonymous chunk
    -@o test2.tmp
    -@{@<part1@>
    -@<part2@>
    -@}@@
    -@d part1 @{This is part 1.@}
    -Okay, now for some errors: no part2!
    -"""
    -
    -    
    -

    Sample Document 2 (58). - Used by Tangle Test semantic error 2 (57); test_tangler.py (55). -

    - - - - - -

    Tangle Test semantic error 3 (59) =

    -
    
    -
    -Sample Document 3 (60)
    -
    -class Test_SemanticError_3( TangleTestcase ):
    -    text= test3_w
    -    file_name= "test3.w"
    -    def test_should_raise_bad_xref( self ):
    -        self.tangle_and_check_exception( "Illegal tangling of a cross reference command." )
    -
    -    
    -

    Tangle Test semantic error 3 (59). - Used by test_tangler.py (55). -

    - - - - - -

    Sample Document 3 (60) =

    -
    
    -
    -test3_w= """Some anonymous chunk
    -@o test3.tmp
    -@{@<part1@>
    -@<part2@>
    -@}@@
    -@d part1 @{This is part 1.@}
    -@d part2 @{This is part 2, with an illegal: @f.@}
    -Okay, now for some errors: attempt to tangle a cross-reference!
    -"""
    -
    -    
    -

    Sample Document 3 (60). - Used by Tangle Test semantic error 3 (59); test_tangler.py (55). -

    - - - - - - -

    Tangle Test semantic error 4 (61) =

    -
    
    -
    -Sample Document 4 (62)
    -
    -class Test_SemanticError_4( TangleTestcase ):
    -    text= test4_w
    -    file_name= "test4.w"
    -    def test_should_raise_noFullName( self ):
    -        self.tangle_and_check_exception( "No full name for 'part1...'" )
    -
    -    
    -

    Tangle Test semantic error 4 (61). - Used by test_tangler.py (55). -

    - - - - - -

    Sample Document 4 (62) =

    -
    
    -
    -test4_w= """Some anonymous chunk
    -@o test4.tmp
    -@{@<part1...@>
    -@<part2@>
    -@}@@
    -@d part1... @{This is part 1.@}
    -@d part2 @{This is part 2.@}
    -Okay, now for some errors: attempt to weave but no full name for part1....
    -"""
    -
    -    
    -

    Sample Document 4 (62). - Used by Tangle Test semantic error 4 (61); test_tangler.py (55). -

    - - - - - -

    Tangle Test semantic error 5 (63) =

    -
    
    -
    -Sample Document 5 (64)
    -
    -class Test_SemanticError_5( TangleTestcase ):
    -    text= test5_w
    -    file_name= "test5.w"
    -    def test_should_raise_ambiguous( self ):
    -        self.tangle_and_check_exception( "Ambiguous abbreviation 'part1...', matches ['part1b', 'part1a']" )
    -
    -    
    -

    Tangle Test semantic error 5 (63). - Used by test_tangler.py (55). -

    - - - - - -

    Sample Document 5 (64) =

    -
    
    -
    -test5_w= """
    -Some anonymous chunk
    -@o test5.tmp
    -@{@<part1...@>
    -@<part2@>
    -@}@@
    -@d part1a @{This is part 1 a.@}
    -@d part1b @{This is part 1 b.@}
    -@d part2 @{This is part 2.@}
    -Okay, now for some errors: part1... is ambiguous
    -"""
    -
    -    
    -

    Sample Document 5 (64). - Used by Tangle Test semantic error 5 (63); test_tangler.py (55). -

    - - - - - -

    Tangle Test semantic error 6 (65) =

    -
    
    - 
    -Sample Document 6 (66)
    -
    -class Test_SemanticError_6( TangleTestcase ):
    -    text= test6_w
    -    file_name= "test6.w"
    -    def test_should_warn( self ):
    -        self.rdr.load()
    -        self.web.tangle( self.tangler )
    -        self.web.createUsedBy()
    -        self.assertEquals( 1, len( self.web.no_reference() ) )
    -        self.assertEquals( 1, len( self.web.multi_reference() ) )
    -        self.assertEquals( 0, len( self.web.no_definition() ) )
    -
    -    
    -

    Tangle Test semantic error 6 (65). - Used by test_tangler.py (55). -

    - - - - - -

    Sample Document 6 (66) =

    -
    
    -
    -test6_w= """Some anonymous chunk
    -@o test6.tmp
    -@{@<part1...@>
    -@<part1a@>
    -@}@@
    -@d part1a @{This is part 1 a.@}
    -@d part2 @{This is part 2.@}
    -Okay, now for some warnings: 
    -- part1 has multiple references.
    -- part2 is unreferenced.
    -"""
    -
    -    
    -

    Sample Document 6 (66). - Used by Tangle Test semantic error 6 (65); test_tangler.py (55). -

    - - - - - -

    Tangle Test include error 7 (67) =

    -
    
    -
    -Sample Document 7 and it's included file (68)
    -
    -class Test_IncludeError_7( TangleTestcase ):
    -    text= test7_w
    -    file_name= "test7.w"
    -    def setUp( self ):
    -        with open('test7_inc.tmp','w') as temp:
    -            temp.write( test7_inc_w )
    -        super( Test_IncludeError_7, self ).setUp()
    -    def test_should_include( self ):
    -        self.rdr.load()
    -        self.web.tangle( self.tangler )
    -        self.web.createUsedBy()
    -        self.assertEquals( 5, len(self.web.chunkSeq) )
    -        self.assertEquals( test7_inc_w, self.web.chunkSeq[3].commands[0].text )
    -    def tearDown( self ):
    -        os.remove( 'test7_inc.tmp' )
    -        super( Test_IncludeError_7, self ).tearDown()
    -
    -    
    -

    Tangle Test include error 7 (67). - Used by test_tangler.py (55). -

    - - - - - -

    Sample Document 7 and it's included file (68) =

    -
    
    -
    -test7_w= """
    -Some anonymous chunk.
    -@d title @[the title of this document, defined with @@[ and @@]@]
    -A reference to @<title@>.
    -@i test7_inc.tmp
    -A final anonymous chunk from test7.w
    -"""
    -
    -test7_inc_w= """The test7a.tmp chunk for test7.w
    -"""
    -
    -    
    -

    Sample Document 7 and it's included file (68). - Used by Tangle Test include error 7 (67); test_tangler.py (55). -

    - - - - - -

    Tangle Test overheads: imports, etc. (69) =

    -
    
    -from __future__ import print_function
    -"""Tangler tests exercise various semantic features."""
    -import pyweb
    -import unittest
    -import logging
    -import StringIO
    -import os
    -
    -    
    -

    Tangle Test overheads: imports, etc. (69). - Used by test_tangler.py (55). -

    - - - - - -

    Tangle Test main program (70) =

    -
    
    -
    -if __name__ == "__main__":
    -    import sys
    -    logging.basicConfig( stream=sys.stdout, level= logging.WARN )
    -    unittest.main()
    -
    -    
    -

    Tangle Test main program (70). - Used by test_tangler.py (55). -

    - - - -

    Tests for Weaving

    - -

    We need to be able to weave a document from one or more source files.

    - - - -

    test_weaver.py (71) =

    -
    
    -    Weave Test overheads: imports, etc. (78)    
    -    Weave Test superclass to refactor common setup (72)    
    -    Weave Test references and definitions (73)    
    -    Weave Test evaluation of expressions (76)    
    -    Weave Test main program (79)    
    -
    -

    test_weaver.py (71). - -

    - - -

    Weaving test cases have a common setup shown in this superclass.

    - - - - -

    Weave Test superclass to refactor common setup (72) =

    -
    
    -
    -class WeaveTestcase( unittest.TestCase ):
    -    text= ""
    -    file_name= ""
    -    error= ""
    -    def setUp( self ):
    -        source= StringIO.StringIO( self.text )
    -        self.web= pyweb.Web( self.file_name )
    -        self.rdr= pyweb.WebReader()
    -        self.rdr.source( self.file_name, source ).web( self.web )
    -        self.rdr.load()
    -    def tangle_and_check_exception( self, exception_text ):
    -        try:
    -            self.rdr.load()
    -            self.web.tangle( self.tangler )
    -            self.web.createUsedBy()
    -            self.fail( "Should not tangle" )
    -        except pyweb.Error, e:
    -            self.assertEquals( exception_text, e.args[0] )
    -    def tearDown( self ):
    -        name, _ = os.path.splitext( self.file_name )
    -        try:
    -            os.remove( name + ".html" )
    -        except OSError:
    -            pass
    -
    -    
    -

    Weave Test superclass to refactor common setup (72). - Used by test_weaver.py (71). -

    - - - - - -

    Weave Test references and definitions (73) =

    -
    
    -
    -Sample Document 0 (74)
    -Expected Output 0 (75)
    -
    -class Test_RefDefWeave( WeaveTestcase ):
    -    text= test0_w
    -    file_name = "test0.w"
    -    def test_load_should_createChunks( self ):
    -        self.assertEquals( 3, len( self.web.chunkSeq ) )
    -    def test_weave_should_createFile( self ):
    -        doc= pyweb.HTML()
    -        self.web.weave( doc )
    -        with open("test0.html","r") as source:
    -            actual= source.read()
    -        m= difflib.SequenceMatcher( lambda x: x in string.whitespace, expected, actual )
    -        for tag, i1, i2, j1, j2 in m.get_opcodes():
    -            if tag == "equal": continue
    -            self.fail( "At %d %s: expected %r, actual %r" % ( j1, tag, repr(expected[i1:i2]), repr(actual[j1:j2]) ) )
    -
    -
    -    
    -

    Weave Test references and definitions (73). - Used by test_weaver.py (71). -

    - - - - - -

    Sample Document 0 (74) =

    -
    
    - 
    -test0_w= """<html>
    -<head>
    -    <link rel="StyleSheet" href="pyweb.css" type="text/css" />
    -</head>
    -<body>
    -@<some code@>
    -
    -@d some code 
    -@{
    -def fastExp( n, p ):
    -    r= 1
    -    while p > 0:
    -        if p%2 == 1: return n*fastExp(n,p-1)
    -	return n*n*fastExp(n,p/2)
    -
    -for i in range(24):
    -    fastExp(2,i)
    -@}
    -</body>
    -</html>
    -"""
    -
    -    
    -

    Sample Document 0 (74). - Used by Weave Test references and definitions (73); test_weaver.py (71). -

    - - - - - -

    Expected Output 0 (75) =

    -
    
    -
    -expected= """<html>
    -<head>
    -    <link rel="StyleSheet" href="pyweb.css" type="text/css" />
    -</head>
    -<body>
    -    <a href="#pyweb1">&rarr;<em>some code</em> (1)</a>
    -
    -
    -    <a name="pyweb1"></a>
    -    <!--line number 9-->
    -    <p><em>some code</em> (1)&nbsp;=</p>
    -    <code><pre>
    -
    -def fastExp( n, p ):
    -    r= 1
    -    while p &gt; 0:
    -        if p%2 == 1: return n*fastExp(n,p-1)
    -	return n*n*fastExp(n,p/2)
    -
    -for i in range(24):
    -    fastExp(2,i)
    -
    -    </pre></code>
    -    <p>&loz; <em>some code</em> (1).
    -    
    -    </p>
    -
    -</body>
    -</html>
    -"""
    -
    -    
    -

    Expected Output 0 (75). - Used by Weave Test references and definitions (73); test_weaver.py (71). -

    - - - - - -

    Weave Test evaluation of expressions (76) =

    -
    
    -
    -Sample Document 9 (77)
    -
    -class TestEvaluations( WeaveTestcase ):
    -    text= test9_w
    -    file_name = "test9.w"
    -    def test_should_evaluate( self ):
    -        doc= pyweb.HTML()
    -        self.web.weave( doc )
    -        with open("test9.html","r") as source:
    -            actual= source.readlines()
    -        #print( actual )
    -        self.assertEquals( "An anonymous chunk.\n", actual[0] )
    -        self.assertTrue( actual[1].startswith( "Time =" ) )
    -        self.assertEquals( "File = ('test9.w', 3, 3)\n", actual[2] )
    -        self.assertEquals( 'Version = $Revision$\n', actual[3] )
    -        self.assertEquals( 'OS = %s\n' % os.name, actual[4] )
    -        self.assertEquals( 'CWD = %s\n' % os.getcwd(), actual[5] )
    -
    -    
    -

    Weave Test evaluation of expressions (76). - Used by test_weaver.py (71). -

    - - - - - -

    Sample Document 9 (77) =

    -
    
    -
    -test9_w= """An anonymous chunk.
    -Time = @(time.asctime()@)
    -File = @(theLocation@)
    -Version = @(__version__@)
    -OS = @(os.name@)
    -CWD = @(os.getcwd()@)
    -"""
    -
    -    
    -

    Sample Document 9 (77). - Used by Weave Test evaluation of expressions (76); test_weaver.py (71). -

    - - - - - -

    Weave Test overheads: imports, etc. (78) =

    -
    
    -from __future__ import print_function
    -"""Weaver tests exercise various weaving features."""
    -import pyweb
    -import unittest
    -import logging
    -import StringIO
    -import os
    -import difflib
    -import string
    -
    -    
    -

    Weave Test overheads: imports, etc. (78). - Used by test_weaver.py (71). -

    - - - - - -

    Weave Test main program (79) =

    -
    
    -
    -if __name__ == "__main__":
    -    import sys
    -    logging.basicConfig( stream=sys.stdout, level= logging.WARN )
    -    unittest.main()
    -
    -    
    -

    Weave Test main program (79). - Used by test_weaver.py (71). -

    - -
    - -

    Combined Test Script

    -
    - - - -

    The combined test script runs all tests in all test modules.

    - - - -

    test.py (80) =

    -
    
    -    Combined Test overheads, imports, etc. (81)    
    -    Combined Test suite which imports all other test modules (82)    
    -    Combined Test main script (83)    
    -
    -

    test.py (80). - -

    - - -

    The overheads import unittest and logging, because those are essential -infrastructure. Additionally, each of the test modules is also imported. -

    - - - - -

    Combined Test overheads, imports, etc. (81) =

    -
    
    -from __future__ import print_function
    -"""Combined tests."""
    -import unittest
    -import test_loader
    -import test_tangler
    -import test_weaver
    -import test_unit
    -import logging
    -
    -    
    -

    Combined Test overheads, imports, etc. (81). - Used by test.py (80). -

    - - -

    The test suite is built from each of the individual test modules.

    - - - - -

    Combined Test suite which imports all other test modules (82) =

    -
    
    -
    -def suite():
    -    s= unittest.TestSuite()
    -    for m in ( test_loader, test_tangler, test_weaver, test_unit ):
    -        s.addTests( unittest.defaultTestLoader.loadTestsFromModule( m ) )
    -    return s
    -
    -    
    -

    Combined Test suite which imports all other test modules (82). - Used by test.py (80). -

    - - -

    The main script initializes logging and then executes the -unittest.TextTestRunner on the test suite. -

    - - - - -

    Combined Test main script (83) =

    -
    
    -
    -if __name__ == "__main__":
    -    import sys
    -    logging.basicConfig( stream=sys.stdout, level=logging.CRITICAL )
    -    tr= unittest.TextTestRunner()
    -    result= tr.run( suite() )
    -    logging.shutdown()
    -    sys.exit( len(result.failures) + len(result.errors) )
    -
    -    
    -

    Combined Test main script (83). - Used by test.py (80). -

    -
    - -

    Indices

    -
    -

    Files

    - -
    -
    test.py
    (80)
    -
    test_loader.py
    (47)
    -
    test_tangler.py
    (55)
    -
    test_unit.py
    (1)
    -
    test_weaver.py
    (71)
    -
    - -

    Macros

    -
    -
    Combined Test main script
    (83)
    -
    Combined Test overheads, imports, etc.
    (81)
    -
    Combined Test suite which imports all other test modules
    (82)
    -
    Expected Output 0
    (75)
    -
    Load Test error handling with a few common syntax errors
    (49)
    -
    Load Test include processing with syntax errors
    (51)
    -
    Load Test main program
    (54)
    -
    Load Test overheads: imports, etc.
    (53)
    -
    Load Test superclass to refactor common setup
    (48)
    -
    Sample Document 0
    (74)
    -
    Sample Document 1 with correct and incorrect syntax
    (50)
    -
    Sample Document 2
    (58)
    -
    Sample Document 3
    (60)
    -
    Sample Document 4
    (62)
    -
    Sample Document 5
    (64)
    -
    Sample Document 6
    (66)
    -
    Sample Document 7 and it's included file
    (68)
    -
    Sample Document 8 and the file it includes
    (52)
    -
    Sample Document 9
    (77)
    -
    Tangle Test include error 7
    (67)
    -
    Tangle Test main program
    (70)
    -
    Tangle Test overheads: imports, etc.
    (69)
    -
    Tangle Test semantic error 2
    (57)
    -
    Tangle Test semantic error 3
    (59)
    -
    Tangle Test semantic error 4
    (61)
    -
    Tangle Test semantic error 5
    (63)
    -
    Tangle Test semantic error 6
    (65)
    -
    Tangle Test superclass to refactor common setup
    (56)
    -
    Unit Test Mock Chunk class
    (4)
    -
    Unit Test Web class chunk cross-reference
    (35)
    -
    Unit Test Web class construction methods
    (33)
    -
    Unit Test Web class name resolution methods
    (34)
    -
    Unit Test Web class tangle
    (36)
    -
    Unit Test Web class weave
    (37)
    -
    Unit Test main
    (46)
    -
    Unit Test of Action class hierarchy
    (39)
    -
    Unit Test of Application class
    (44)
    -
    Unit Test of Chunk class hierarchy
    (11)
    -
    Unit Test of Chunk construction
    (16)
    -
    Unit Test of Chunk emission
    (18)
    -
    Unit Test of Chunk interrogation
    (17)
    -
    Unit Test of Chunk superclass
    (12) (13) (14) (15)
    -
    Unit Test of CodeCommand class to contain a program source code block
    (25)
    -
    Unit Test of Command class hierarchy
    (22)
    -
    Unit Test of Command superclass
    (23)
    -
    Unit Test of Emitter Superclass
    (3)
    -
    Unit Test of Emitter class hierarchy
    (2)
    -
    Unit Test of FileXrefCommand class for an output file cross-reference
    (27)
    -
    Unit Test of HTML subclass of Emitter
    (7)
    -
    Unit Test of HTMLShort subclass of Emitter
    (8)
    -
    Unit Test of LaTeX subclass of Emitter
    (6)
    -
    Unit Test of MacroXrefCommand class for a named chunk cross-reference
    (28)
    -
    Unit Test of NamedChunk subclass
    (19)
    -
    Unit Test of NamedDocumentChunk subclass
    (21)
    -
    Unit Test of OutputChunk subclass
    (20)
    -
    Unit Test of Reference class hierarchy
    (31)
    -
    Unit Test of ReferenceCommand class for chunk references
    (30)
    -
    Unit Test of Tangler subclass of Emitter
    (9)
    -
    Unit Test of TanglerMake subclass of Emitter
    (10)
    -
    Unit Test of TextCommand class to contain a document text block
    (24)
    -
    Unit Test of UserIdXrefCommand class for a user identifier cross-reference
    (29)
    -
    Unit Test of Weaver subclass of Emitter
    (5)
    -
    Unit Test of Web class
    (32)
    -
    Unit Test of WebReader class
    (38)
    -
    Unit Test of XrefCommand superclass for all cross-reference commands
    (26)
    -
    Unit Test overheads: imports, etc.
    (45)
    -
    Unit test of Action Sequence class
    (40)
    -
    Unit test of LoadAction class
    (43)
    -
    Unit test of TangleAction class
    (42)
    -
    Unit test of WeaverAction class
    (41)
    -
    Weave Test evaluation of expressions
    (76)
    -
    Weave Test main program
    (79)
    -
    Weave Test overheads: imports, etc.
    (78)
    -
    Weave Test references and definitions
    (73)
    -
    Weave Test superclass to refactor common setup
    (72)
    -
    - -

    User Identifiers

    -
    -
    - -
    - -
    -

    Created by /Users/slott/Documents/Projects/pyWeb-2.1/pyweb/pyweb.py at Wed Mar 10 08:00:56 2010.

    -

    pyweb.__version__ '$Revision$'.

    -

    Source pyweb_test.w modified Mon Mar 1 07:57:54 2010. -

    -

    Working directory '/Users/slott/Documents/Projects/pyWeb-2.1/pyweb/test'.

    - -
    - - diff --git a/test/pyweb_test.w b/test/pyweb_test.w deleted file mode 100644 index 746c167..0000000 --- a/test/pyweb_test.w +++ /dev/null @@ -1,64 +0,0 @@ - - - - - pyWeb Literate Programming 2.3 - Test Suite - - - - - -
    - - -

    pyWeb 2.3 Test Suite

    -

    In Python, Yet Another Literate Programming Tool

    -

    Steven F. Lott

    - -
    -

    Table of Contents

    - -
    - -

    Introduction

    -
    -@i intro.w -
    - -

    Unit Testing

    -
    -@i unit.w -
    - -

    Functional Testing

    -
    -@i func.w -
    - -

    Combined Test Script

    -
    -@i combined.w -
    - -

    Indices

    -
    -

    Files

    -@f -

    Macros

    -@m -

    User Identifiers

    -@u -
    - -
    -

    Created by @(thisApplication@) at @(datetime.datetime.now().ctime()@).

    -

    pyweb.__version__ '@(__version__@)'.

    -

    Source @(theFile@) modified @(datetime.datetime.fromtimestamp(os.path.getmtime(theFile))@). -

    -

    Working directory '@(os.path.realpath('.')@)'.

    - - -
    - - diff --git a/test/test.py b/test/test.py deleted file mode 100644 index a48e749..0000000 --- a/test/test.py +++ /dev/null @@ -1,25 +0,0 @@ -from __future__ import print_function -"""Combined tests.""" -import unittest -import test_loader -import test_tangler -import test_weaver -import test_unit -import logging - - -def suite(): - s= unittest.TestSuite() - for m in ( test_loader, test_tangler, test_weaver, test_unit ): - s.addTests( unittest.defaultTestLoader.loadTestsFromModule( m ) ) - return s - - -if __name__ == "__main__": - import sys - logging.basicConfig( stream=sys.stdout, level=logging.CRITICAL ) - tr= unittest.TextTestRunner() - result= tr.run( suite() ) - logging.shutdown() - sys.exit( len(result.failures) + len(result.errors) ) - diff --git a/test/test_latex.pdf b/test/test_latex.pdf deleted file mode 100644 index a7047b8..0000000 Binary files a/test/test_latex.pdf and /dev/null differ diff --git a/test/test_loader.py b/test/test_loader.py deleted file mode 100644 index 11e3a11..0000000 --- a/test/test_loader.py +++ /dev/null @@ -1,80 +0,0 @@ -from __future__ import print_function -"""Loader and parsing tests.""" -import pyweb -import unittest -import logging -import StringIO -import os - - -class ParseTestcase( unittest.TestCase ): - text= "" - file_name= "" - def setUp( self ): - source= StringIO.StringIO( self.text ) - self.web= pyweb.Web( self.file_name ) - self.rdr= pyweb.WebReader() - self.rdr.source( self.file_name, source ).web( self.web ) - - - -test1_w= """Some anonymous chunk -@o test1.tmp -@{@ -@ -@}@@ -@d part1 @{This is part 1.@} -Okay, now for an error. -@o show how @o commands work -@{ @{ @] @] -""" - - -class Test_ParseErrors( ParseTestcase ): - text= test1_w - file_name= "test1.w" - def test_should_raise_syntax( self ): - try: - self.rdr.load() - self.fail( "Should not parse" ) - except pyweb.Error, e: - self.assertEquals( "At ('test1.w', 8, 8): expected ('@{',), found '@o'", e.args[0] ) - - - -test8_w= """Some anonymous chunk. -@d title @[the title of this document, defined with @@[ and @@]@] -A reference to @. -@i test8_inc.tmp -A final anonymous chunk from test8.w -""" - -test8_inc_w="""A chunk from test8a.w -And now for an error - incorrect syntax in an included file! -@d yap -""" - - -class Test_IncludeParseErrors( ParseTestcase ): - text= test8_w - file_name= "test8.w" - def setUp( self ): - with open('test8_inc.tmp','w') as temp: - temp.write( test8_inc_w ) - super( Test_IncludeParseErrors, self ).setUp() - def test_should_raise_include_syntax( self ): - try: - self.rdr.load() - self.fail( "Should not parse" ) - except pyweb.Error, e: - self.assertEquals( "At ('test8_inc.tmp', 3, 4): end of input, ('@{', '@[') not found", e.args[0] ) - def tearDown( self ): - os.remove( 'test8_inc.tmp' ) - super( Test_IncludeParseErrors, self ).tearDown() - - -if __name__ == "__main__": - import sys - logging.basicConfig( stream=sys.stdout, level= logging.WARN ) - unittest.main() - diff --git a/test/test_rst.rst b/test/test_rst.rst deleted file mode 100644 index d300462..0000000 --- a/test/test_rst.rst +++ /dev/null @@ -1,146 +0,0 @@ -#################### -Test Program -#################### - -=============== -Jason R. Fruit -=============== - -.. include:: - -.. contents:: - - -Introduction -============ - -This test program prints the word "hello", followed by the name of -the operating system as understood by Python. It is implemented in -Python and uses the ``os`` module. It builds the message string -in two different ways, and writes separate versions of the program to -two different files. - -Implementation -============== - -Output files ------------- - -This document contains the makings of two files; the first, -``test.py``, uses simple string concatenation to build its output -message: - - -.. _`1`: -.. rubric:: test.py (1) -.. parsed-literal:: - - |srarr| Import the os module (`3`_) - |srarr| Get the OS description (`4`_) - |srarr| Construct the message with Concatenation (`5`_) - |srarr| Print the message (`7`_) - - - - -The second uses string substitution: - - -.. _`2`: -.. rubric:: test2.py (2) -.. parsed-literal:: - - |srarr| Import the os module (`3`_) - |srarr| Get the OS description (`4`_) - |srarr| Construct the message with Substitution (`6`_) - |srarr| Print the message (`7`_) - - - - -Retrieving the OS description -------------------------------- - -First we must import the os module so we can learn about the OS: - - -.. _`3`: -.. rubric:: Import the os module (3) -.. parsed-literal:: - - import os - - -Used by: test.py (`1`_); test2.py (`2`_) - - - -That having been done, we can retrieve Python's name for the OS type: - - -.. _`4`: -.. rubric:: Get the OS description (4) -.. parsed-literal:: - - os\_name = os.name - - -Used by: test.py (`1`_); test2.py (`2`_) - - - -Building the message ---------------------- - -Now, we're ready for the meat of the application: concatenating two strings: - - -.. _`5`: -.. rubric:: Construct the message with Concatenation (5) -.. parsed-literal:: - - msg = "Hello, " + os\_name + "!" - - -Used by: test.py (`1`_) - - - -But wait! Is there a better way? Using string substitution might be -better: - - -.. _`6`: -.. rubric:: Construct the message with Substitution (6) -.. parsed-literal:: - - msg = "Hello, %s!" % os\_name - - -Used by: test2.py (`2`_) - - - -We'll use the first of these methods in ``test.py``, and the -other in ``test2.py``. - -Printing the message ----------------------- - -Finally, we print the message out for the user to see. Hopefully, a -cheery greeting will make them happy to know what operating system -they have: - - -.. _`7`: -.. rubric:: Print the message (7) -.. parsed-literal:: - - print msg - - -Used by: test.py (`1`_); test2.py (`2`_) - - - - diff --git a/test/test_rst.tex b/test/test_rst.tex deleted file mode 100644 index bf9171b..0000000 --- a/test/test_rst.tex +++ /dev/null @@ -1,198 +0,0 @@ -#################### -Test Program -#################### - -=============== -Jason R. Fruit -=============== - -.. toc:: - - -Introduction -============ - -This test program prints the word "hello", followed by the name of -the operating system as understood by Python. It is implemented in -Python and uses the ``os`` module. It builds the message string -in two different ways, and writes separate versions of the program to -two different files. - -Implementation -============== - -Output files ------------- - -This document contains the makings of two files; the first, -``test.py``, uses simple string concatenation to build its output -message: - -\label{pyweb1} - \begin{flushleft} - \textit{Code example test.py (1)} - \begin{Verbatim}[commandchars=\\\{\},codes={\catcode`$=3\catcode`^=7},frame=single] - -$\triangleright$ Code Example Import the os module (3) -$\triangleright$ Code Example Get the OS description (4) -$\triangleright$ Code Example Construct the message with Concatenation (5) -$\triangleright$ Code Example Print the message (7) - - \end{Verbatim} - - \end{flushleft} - - -The second uses string substitution: - -\label{pyweb2} - \begin{flushleft} - \textit{Code example test2.py (2)} - \begin{Verbatim}[commandchars=\\\{\},codes={\catcode`$=3\catcode`^=7},frame=single] - -$\triangleright$ Code Example Import the os module (3) -$\triangleright$ Code Example Get the OS description (4) -$\triangleright$ Code Example Construct the message with Substitution (6) -$\triangleright$ Code Example Print the message (7) - - \end{Verbatim} - - \end{flushleft} - - -Retrieving the OS description -------------------------------- - -First we must import the os module so we can learn about the OS: - -\label{pyweb3} - \begin{flushleft} - \textit{Code example Import the os module (3)} - \begin{Verbatim}[commandchars=\\\{\},codes={\catcode`$=3\catcode`^=7},frame=single] - -import os - - \end{Verbatim} - - \footnotesize - Used by: - \begin{list}{}{} - - \item Code example test.py (1) (Sect. \ref{pyweb1}, p. \pageref{pyweb1}) -; - \item Code example test2.py (2) (Sect. \ref{pyweb2}, p. \pageref{pyweb2}) - - \end{list} - \normalsize - - \end{flushleft} - - -That having been done, we can retrieve Python's name for the OS type: - -\label{pyweb4} - \begin{flushleft} - \textit{Code example Get the OS description (4)} - \begin{Verbatim}[commandchars=\\\{\},codes={\catcode`$=3\catcode`^=7},frame=single] - -os_name = os.name - - \end{Verbatim} - - \footnotesize - Used by: - \begin{list}{}{} - - \item Code example test.py (1) (Sect. \ref{pyweb1}, p. \pageref{pyweb1}) -; - \item Code example test2.py (2) (Sect. \ref{pyweb2}, p. \pageref{pyweb2}) - - \end{list} - \normalsize - - \end{flushleft} - - -Building the message ---------------------- - -Now, we're ready for the meat of the application: concatenating two strings: - -\label{pyweb5} - \begin{flushleft} - \textit{Code example Construct the message with Concatenation (5)} - \begin{Verbatim}[commandchars=\\\{\},codes={\catcode`$=3\catcode`^=7},frame=single] - -msg = "Hello, " + os_name + "!" - - \end{Verbatim} - - \footnotesize - Used by: - \begin{list}{}{} - - \item Code example test.py (1) (Sect. \ref{pyweb1}, p. \pageref{pyweb1}) - - \end{list} - \normalsize - - \end{flushleft} - - -But wait! Is there a better way? Using string substitution might be -better: - -\label{pyweb6} - \begin{flushleft} - \textit{Code example Construct the message with Substitution (6)} - \begin{Verbatim}[commandchars=\\\{\},codes={\catcode`$=3\catcode`^=7},frame=single] - -msg = "Hello, %s!" % os_name - - \end{Verbatim} - - \footnotesize - Used by: - \begin{list}{}{} - - \item Code example test2.py (2) (Sect. \ref{pyweb2}, p. \pageref{pyweb2}) - - \end{list} - \normalsize - - \end{flushleft} - - -We'll use the first of these methods in ``test.py``, and the -other in ``test2.py``. - -Printing the message ----------------------- - -Finally, we print the message out for the user to see. Hopefully, a -cheery greeting will make them happy to know what operating system -they have: - -\label{pyweb7} - \begin{flushleft} - \textit{Code example Print the message (7)} - \begin{Verbatim}[commandchars=\\\{\},codes={\catcode`$=3\catcode`^=7},frame=single] - -print msg - - \end{Verbatim} - - \footnotesize - Used by: - \begin{list}{}{} - - \item Code example test.py (1) (Sect. \ref{pyweb1}, p. \pageref{pyweb1}) -; - \item Code example test2.py (2) (Sect. \ref{pyweb2}, p. \pageref{pyweb2}) - - \end{list} - \normalsize - - \end{flushleft} - - diff --git a/test/test_tangler.py b/test/test_tangler.py deleted file mode 100644 index 23a09cc..0000000 --- a/test/test_tangler.py +++ /dev/null @@ -1,174 +0,0 @@ -from __future__ import print_function -"""Tangler tests exercise various semantic features.""" -import pyweb -import unittest -import logging -import StringIO -import os - - -class TangleTestcase( unittest.TestCase ): - text= "" - file_name= "" - error= "" - def setUp( self ): - source= StringIO.StringIO( self.text ) - self.web= pyweb.Web( self.file_name ) - self.rdr= pyweb.WebReader() - self.rdr.source( self.file_name, source ).web( self.web ) - self.tangler= pyweb.Tangler() - def tangle_and_check_exception( self, exception_text ): - try: - self.rdr.load() - self.web.tangle( self.tangler ) - self.web.createUsedBy() - self.fail( "Should not tangle" ) - except pyweb.Error, e: - self.assertEquals( exception_text, e.args[0] ) - def tearDown( self ): - name, _ = os.path.splitext( self.file_name ) - try: - os.remove( name + ".tmp" ) - except OSError: - pass - - - -test2_w= """Some anonymous chunk -@o test2.tmp -@{@ -@ -@}@@ -@d part1 @{This is part 1.@} -Okay, now for some errors: no part2! -""" - - -class Test_SemanticError_2( TangleTestcase ): - text= test2_w - file_name= "test2.w" - def test_should_raise_undefined( self ): - self.tangle_and_check_exception( "Attempt to tangle an undefined Chunk, part2." ) - - - -test3_w= """Some anonymous chunk -@o test3.tmp -@{@ -@ -@}@@ -@d part1 @{This is part 1.@} -@d part2 @{This is part 2, with an illegal: @f.@} -Okay, now for some errors: attempt to tangle a cross-reference! -""" - - -class Test_SemanticError_3( TangleTestcase ): - text= test3_w - file_name= "test3.w" - def test_should_raise_bad_xref( self ): - self.tangle_and_check_exception( "Illegal tangling of a cross reference command." ) - - - -test4_w= """Some anonymous chunk -@o test4.tmp -@{@ -@ -@}@@ -@d part1... @{This is part 1.@} -@d part2 @{This is part 2.@} -Okay, now for some errors: attempt to weave but no full name for part1.... -""" - - -class Test_SemanticError_4( TangleTestcase ): - text= test4_w - file_name= "test4.w" - def test_should_raise_noFullName( self ): - self.tangle_and_check_exception( "No full name for 'part1...'" ) - - - -test5_w= """ -Some anonymous chunk -@o test5.tmp -@{@ -@ -@}@@ -@d part1a @{This is part 1 a.@} -@d part1b @{This is part 1 b.@} -@d part2 @{This is part 2.@} -Okay, now for some errors: part1... is ambiguous -""" - - -class Test_SemanticError_5( TangleTestcase ): - text= test5_w - file_name= "test5.w" - def test_should_raise_ambiguous( self ): - self.tangle_and_check_exception( "Ambiguous abbreviation 'part1...', matches ['part1b', 'part1a']" ) - - - -test6_w= """Some anonymous chunk -@o test6.tmp -@{@ -@ -@}@@ -@d part1a @{This is part 1 a.@} -@d part2 @{This is part 2.@} -Okay, now for some warnings: -- part1 has multiple references. -- part2 is unreferenced. -""" - - -class Test_SemanticError_6( TangleTestcase ): - text= test6_w - file_name= "test6.w" - def test_should_warn( self ): - self.rdr.load() - self.web.tangle( self.tangler ) - self.web.createUsedBy() - self.assertEquals( 1, len( self.web.no_reference() ) ) - self.assertEquals( 1, len( self.web.multi_reference() ) ) - self.assertEquals( 0, len( self.web.no_definition() ) ) - - - -test7_w= """ -Some anonymous chunk. -@d title @[the title of this document, defined with @@[ and @@]@] -A reference to @. -@i test7_inc.tmp -A final anonymous chunk from test7.w -""" - -test7_inc_w= """The test7a.tmp chunk for test7.w -""" - - -class Test_IncludeError_7( TangleTestcase ): - text= test7_w - file_name= "test7.w" - def setUp( self ): - with open('test7_inc.tmp','w') as temp: - temp.write( test7_inc_w ) - super( Test_IncludeError_7, self ).setUp() - def test_should_include( self ): - self.rdr.load() - self.web.tangle( self.tangler ) - self.web.createUsedBy() - self.assertEquals( 5, len(self.web.chunkSeq) ) - self.assertEquals( test7_inc_w, self.web.chunkSeq[3].commands[0].text ) - def tearDown( self ): - os.remove( 'test7_inc.tmp' ) - super( Test_IncludeError_7, self ).tearDown() - - -if __name__ == "__main__": - import sys - logging.basicConfig( stream=sys.stdout, level= logging.WARN ) - unittest.main() - diff --git a/test/test_unit.py b/test/test_unit.py deleted file mode 100644 index dc2b497..0000000 --- a/test/test_unit.py +++ /dev/null @@ -1,821 +0,0 @@ -from __future__ import print_function -"""Unit tests.""" -import pyweb -import unittest -import logging -import StringIO -import string -import os -import time -import re - - - -class MockChunk( object ): - def __init__( self, name, seq, lineNumber ): - self.name= name - self.fullName= name - self.seq= seq - self.lineNumber= lineNumber - self.initial= True - self.commands= [] - self.referencedBy= [] - - -class EmitterExtension( pyweb.Emitter ): - def doOpen( self, fileName ): - self.file= StringIO.StringIO() - def doClose( self ): - self.file.flush() - def doWrite( self, text ): - self.file.write( text ) - -class TestEmitter( unittest.TestCase ): - def setUp( self ): - self.emitter= EmitterExtension() - def test_emitter_should_open_close_write( self ): - self.emitter.open( "test.tmp" ) - self.emitter.write( "Something" ) - self.emitter.close() - self.assertEquals( "Something", self.emitter.file.getvalue() ) - def test_emitter_should_codeBlock( self ): - self.emitter.open( "test.tmp" ) - self.emitter.codeBlock( "Some Code" ) - self.emitter.close() - self.assertEquals( "Some Code\n", self.emitter.file.getvalue() ) - def test_emitter_should_indent( self ): - self.emitter.open( "test.tmp" ) - self.emitter.codeBlock( "Begin\n" ) - self.emitter.setIndent( 4 ) - self.emitter.codeBlock( "More Code\n" ) - self.emitter.clrIndent() - self.emitter.codeBlock( "End" ) - self.emitter.close() - self.assertEquals( "Begin\n More Code\nEnd\n", self.emitter.file.getvalue() ) - - -class TestWeaver( unittest.TestCase ): - def setUp( self ): - self.weaver= pyweb.Weaver() - self.filename= "testweaver.w" - self.aFileChunk= MockChunk( "File", 123, 456 ) - self.aFileChunk.references_list= [ ] - self.aChunk= MockChunk( "Chunk", 314, 278 ) - self.aChunk.references_list= [ ("Container", 123) ] - def tearDown( self ): - import os - try: - os.remove( "testweaver.rst" ) - except OSError: - pass - - def test_weaver_functions( self ): - result= self.weaver.quote( "|char| `code` *em* _em_" ) - self.assertEquals( "\|char\| \`code\` \*em\* \_em\_", result ) - result= self.weaver.references( self.aChunk ) - self.assertEquals( "\nUsed by: Container (`123`_)\n", result ) - result= self.weaver.referenceTo( "Chunk", 314 ) - self.assertEquals( "|srarr| Chunk (`314`_)", result ) - - def test_weaver_should_codeBegin( self ): - self.weaver.open( self.filename ) - self.weaver.codeBegin( self.aChunk ) - self.weaver.codeBlock( self.weaver.quote( "*The* `Code`\n" ) ) - self.weaver.codeEnd( self.aChunk ) - self.weaver.close() - with open( "testweaver.rst", "r" ) as result: - txt= result.read() - self.assertEquals( "\n.. _`314`:\n.. rubric:: Chunk (314)\n.. parsed-literal::\n\n \\*The\\* \\`Code\\`\n\n\nUsed by: Container (`123`_)\n\n\n", txt ) - - def test_weaver_should_fileBegin( self ): - self.weaver.open( self.filename ) - self.weaver.fileBegin( self.aFileChunk ) - self.weaver.codeBlock( self.weaver.quote( "*The* `Code`\n" ) ) - self.weaver.fileEnd( self.aFileChunk ) - self.weaver.close() - with open( "testweaver.rst", "r" ) as result: - txt= result.read() - self.assertEquals( "\n.. _`123`:\n.. rubric:: File (123)\n.. parsed-literal::\n\n \\*The\\* \\`Code\\`\n\n\n\n", txt ) - - def test_weaver_should_xref( self ): - self.weaver.open( self.filename ) - self.weaver.xrefHead( ) - self.weaver.xrefLine( "Chunk", [ ("Container", 123) ] ) - self.weaver.xrefFoot( ) - self.weaver.fileEnd( self.aFileChunk ) - self.weaver.close() - with open( "testweaver.rst", "r" ) as result: - txt= result.read() - self.assertEquals( "\n:Chunk:\n |srarr| (`('Container', 123)`_)\n\n\n\n", txt ) - - def test_weaver_should_xref_def( self ): - self.weaver.open( self.filename ) - self.weaver.xrefHead( ) - self.weaver.xrefDefLine( "Chunk", 314, [ ("Container", 123), ("Chunk", 314) ] ) - self.weaver.xrefFoot( ) - self.weaver.fileEnd( self.aFileChunk ) - self.weaver.close() - with open( "testweaver.rst", "r" ) as result: - txt= result.read() - self.assertEquals( "\n:Chunk:\n [`314`_] `('Chunk', 314)`_ `('Container', 123)`_\n\n\n\n", txt ) - - -class TestLaTeX( unittest.TestCase ): - def setUp( self ): - self.weaver= pyweb.LaTeX() - self.filename= "testweaver.w" - self.aFileChunk= MockChunk( "File", 123, 456 ) - self.aFileChunk.references_list= [ ] - self.aChunk= MockChunk( "Chunk", 314, 278 ) - self.aChunk.references_list= [ ("Container", 123) ] - def tearDown( self ): - import os - try: - os.remove( "testweaver.tex" ) - except OSError: - pass - - def test_weaver_functions( self ): - result= self.weaver.quote( "\\end{Verbatim}" ) - self.assertEquals( "\\end\\,{Verbatim}", result ) - result= self.weaver.references( self.aChunk ) - self.assertEquals( "\n \\footnotesize\n Used by:\n \\begin{list}{}{}\n \n \\item Code example Container (123) (Sect. \\ref{pyweb123}, p. \\pageref{pyweb123})\n\n \\end{list}\n \\normalsize\n", result ) - result= self.weaver.referenceTo( "Chunk", 314 ) - self.assertEquals( "$\\triangleright$ Code Example Chunk (314)", result ) - - -class TestHTML( unittest.TestCase ): - def setUp( self ): - self.weaver= pyweb.HTML() - self.filename= "testweaver.w" - self.aFileChunk= MockChunk( "File", 123, 456 ) - self.aFileChunk.references_list= [ ] - self.aChunk= MockChunk( "Chunk", 314, 278 ) - self.aChunk.references_list= [ ("Container", 123) ] - def tearDown( self ): - import os - try: - os.remove( "testweaver.html" ) - except OSError: - pass - - def test_weaver_functions( self ): - result= self.weaver.quote( "a < b && c > d" ) - self.assertEquals( "a < b && c > d", result ) - result= self.weaver.references( self.aChunk ) - self.assertEquals( ' Used by Container (123).', result ) - result= self.weaver.referenceTo( "Chunk", 314 ) - self.assertEquals( 'Chunk (314)', result ) - - - - -class TestTangler( unittest.TestCase ): - def setUp( self ): - self.tangler= pyweb.Tangler() - self.filename= "testtangler.w" - self.aFileChunk= MockChunk( "File", 123, 456 ) - self.aFileChunk.references_list= [ ] - self.aChunk= MockChunk( "Chunk", 314, 278 ) - self.aChunk.references_list= [ ("Container", 123) ] - def tearDown( self ): - import os - try: - os.remove( "testtangler.w" ) - except OSError: - pass - - def test_tangler_functions( self ): - result= self.tangler.quote( string.printable ) - self.assertEquals( string.printable, result ) - def test_tangler_should_codeBegin( self ): - self.tangler.open( self.filename ) - self.tangler.codeBegin( self.aChunk ) - self.tangler.codeBlock( self.tangler.quote( "*The* `Code`\n" ) ) - self.tangler.codeEnd( self.aChunk ) - self.tangler.close() - with open( "testtangler.w", "r" ) as result: - txt= result.read() - self.assertEquals( "*The* `Code`\n", txt ) - - -class TestTanglerMake( unittest.TestCase ): - def setUp( self ): - self.tangler= pyweb.TanglerMake() - self.filename= "testtangler.w" - self.aChunk= MockChunk( "Chunk", 314, 278 ) - self.aChunk.references_list= [ ("Container", 123) ] - self.tangler.open( self.filename ) - self.tangler.codeBegin( self.aChunk ) - self.tangler.codeBlock( self.tangler.quote( "*The* `Code`\n" ) ) - self.tangler.codeEnd( self.aChunk ) - self.tangler.close() - self.original= os.path.getmtime( self.filename ) - time.sleep( 1.0 ) # Attempt to assure timestamps are different - def tearDown( self ): - import os - try: - os.remove( "testtangler.w" ) - except OSError: - pass - - def test_same_should_leave( self ): - self.tangler.open( self.filename ) - self.tangler.codeBegin( self.aChunk ) - self.tangler.codeBlock( self.tangler.quote( "*The* `Code`\n" ) ) - self.tangler.codeEnd( self.aChunk ) - self.tangler.close() - self.assertEquals( self.original, os.path.getmtime( self.filename ) ) - - def test_different_should_update( self ): - self.tangler.open( self.filename ) - self.tangler.codeBegin( self.aChunk ) - self.tangler.codeBlock( self.tangler.quote( "*Completely Different* `Code`\n" ) ) - self.tangler.codeEnd( self.aChunk ) - self.tangler.close() - self.assertNotEquals( self.original, os.path.getmtime( self.filename ) ) - - - - -class MockCommand( object ): - def __init__( self ): - self.lineNumber= 314 - def startswith( self, text ): - return False - -class MockWeb( object ): - def __init__( self ): - self.chunks= [] - self.wove= None - self.tangled= None - def add( self, aChunk ): - self.chunks.append( aChunk ) - def addNamed( self, aChunk ): - self.chunks.append( aChunk ) - def addOutput( self, aChunk ): - self.chunks.append( aChunk ) - def fullNameFor( self, name ): - return name - def fileXref( self ): - return { 'file':[1,2,3] } - def chunkXref( self ): - return { 'chunk':[4,5,6] } - def userNamesXref( self ): - return { 'name':(7,[8,9,10]) } - def getchunk( self, name ): - return [ MockChunk( name, 1, 314 ) ] - def createUsedBy( self ): - pass - def weaveChunk( self, name, weaver ): - weaver.write( name ) - def tangleChunk( self, name, tangler ): - tangler.write( name ) - def weave( self, weaver ): - self.wove= weaver - def tangle( self, tangler ): - self.tangled= tangler - -class MockWeaver( object ): - def __init__( self ): - self.begin_chunk= [] - self.end_chunk= [] - self.written= [] - self.code_indent= None - def quote( self, text ): - return text.replace( "&", "&" ) # token quoting - def docBegin( self, aChunk ): - self.begin_chunk.append( aChunk ) - def write( self, text ): - self.written.append( text ) - def docEnd( self, aChunk ): - self.end_chunk.append( aChunk ) - def codeBegin( self, aChunk ): - self.begin_chunk.append( aChunk ) - def codeBlock( self, text ): - self.written.append( text ) - def codeEnd( self, aChunk ): - self.end_chunk.append( aChunk ) - def fileBegin( self, aChunk ): - self.begin_chunk.append( aChunk ) - def fileEnd( self, aChunk ): - self.end_chunk.append( aChunk ) - def setIndent( self, fixed=None, command=None ): - pass - def clrIndent( self ): - pass - def xrefHead( self ): - pass - def xrefLine( self, name, refList ): - self.written.append( "%s %s" % ( name, refList ) ) - def xrefDefLine( self, name, defn, refList ): - self.written.append( "%s %s %s" % ( name, defn, refList ) ) - def xrefFoot( self ): - pass - def open( self, aFile ): - pass - def close( self ): - pass - def referenceTo( self, name, seq ): - pass - -class MockTangler( MockWeaver ): - def __init__( self ): - super( MockTangler, self ).__init__() - self.context= [0] - -class TestChunk( unittest.TestCase ): - def setUp( self ): - self.theChunk= pyweb.Chunk() - - def test_append_command_should_work( self ): - cmd1= MockCommand() - self.theChunk.append( cmd1 ) - self.assertEquals( 1, len(self.theChunk.commands ) ) - cmd2= MockCommand() - self.theChunk.append( cmd2 ) - self.assertEquals( 2, len(self.theChunk.commands ) ) - - def test_append_initial_and_more_text_should_work( self ): - self.theChunk.appendText( "hi mom" ) - self.assertEquals( 1, len(self.theChunk.commands ) ) - self.theChunk.appendText( "&more text" ) - self.assertEquals( 1, len(self.theChunk.commands ) ) - self.assertEquals( "hi mom&more text", self.theChunk.commands[0].text ) - - def test_append_following_text_should_work( self ): - cmd1= MockCommand() - self.theChunk.append( cmd1 ) - self.theChunk.appendText( "hi mom" ) - self.assertEquals( 2, len(self.theChunk.commands ) ) - - def test_append_to_web_should_work( self ): - web= MockWeb() - self.theChunk.webAdd( web ) - self.assertEquals( 1, len(web.chunks) ) - - - def test_leading_command_should_not_find( self ): - self.assertFalse( self.theChunk.startswith( "hi mom" ) ) - cmd1= MockCommand() - self.theChunk.append( cmd1 ) - self.assertFalse( self.theChunk.startswith( "hi mom" ) ) - self.theChunk.appendText( "hi mom" ) - self.assertEquals( 2, len(self.theChunk.commands ) ) - self.assertFalse( self.theChunk.startswith( "hi mom" ) ) - - def test_leading_text_should_not_find( self ): - self.assertFalse( self.theChunk.startswith( "hi mom" ) ) - self.theChunk.appendText( "hi mom" ) - self.assertTrue( self.theChunk.startswith( "hi mom" ) ) - cmd1= MockCommand() - self.theChunk.append( cmd1 ) - self.assertTrue( self.theChunk.startswith( "hi mom" ) ) - self.assertEquals( 2, len(self.theChunk.commands ) ) - - def test_regexp_exists_should_find( self ): - self.theChunk.appendText( "this chunk has many words" ) - pat= re.compile( r"\Wchunk\W" ) - found= self.theChunk.searchForRE(pat) - self.assertTrue( found is self.theChunk ) - def test_regexp_missing_should_not_find( self ): - self.theChunk.appendText( "this chunk has many words" ) - pat= re.compile( "\Warpigs\W" ) - found= self.theChunk.searchForRE(pat) - self.assertTrue( found is None ) - - def test_lineNumber_should_work( self ): - self.assertTrue( self.theChunk.lineNumber is None ) - cmd1= MockCommand() - self.theChunk.append( cmd1 ) - self.assertEqual( 314, self.theChunk.lineNumber ) - - - def test_weave_should_work( self ): - wvr = MockWeaver() - web = MockWeb() - self.theChunk.appendText( "this chunk has very & many words" ) - self.theChunk.weave( web, wvr ) - self.assertEquals( 1, len(wvr.begin_chunk) ) - self.assertTrue( wvr.begin_chunk[0] is self.theChunk ) - self.assertEquals( 1, len(wvr.end_chunk) ) - self.assertTrue( wvr.end_chunk[0] is self.theChunk ) - self.assertEquals( "this chunk has very & many words", "".join( wvr.written ) ) - - def test_tangle_should_fail( self ): - tnglr = MockTangler() - web = MockWeb() - self.theChunk.appendText( "this chunk has very & many words" ) - try: - self.theChunk.tangle( web, tnglr ) - self.fail() - except pyweb.Error, e: - self.assertEquals( "Cannot tangle an anonymous chunk", e.args[0] ) - - - -class TestNamedChunk( unittest.TestCase ): - def setUp( self ): - self.theChunk= pyweb.NamedChunk( "Some Name..." ) - cmd= self.theChunk.makeContent( "the words & text of this Chunk" ) - self.theChunk.append( cmd ) - self.theChunk.setUserIDRefs( "index terms" ) - - def test_should_find_xref_words( self ): - self.assertEquals( 2, len(self.theChunk.getUserIDRefs()) ) - self.assertEquals( "index", self.theChunk.getUserIDRefs()[0] ) - self.assertEquals( "terms", self.theChunk.getUserIDRefs()[1] ) - - def test_append_to_web_should_work( self ): - web= MockWeb() - self.theChunk.webAdd( web ) - self.assertEquals( 1, len(web.chunks) ) - - def test_weave_should_work( self ): - wvr = MockWeaver() - web = MockWeb() - self.theChunk.weave( web, wvr ) - self.assertEquals( 1, len(wvr.begin_chunk) ) - self.assertTrue( wvr.begin_chunk[0] is self.theChunk ) - self.assertEquals( 1, len(wvr.end_chunk) ) - self.assertTrue( wvr.end_chunk[0] is self.theChunk ) - self.assertEquals( "the words & text of this Chunk", "".join( wvr.written ) ) - - def test_tangle_should_work( self ): - tnglr = MockTangler() - web = MockWeb() - self.theChunk.tangle( web, tnglr ) - self.assertEquals( 1, len(tnglr.begin_chunk) ) - self.assertTrue( tnglr.begin_chunk[0] is self.theChunk ) - self.assertEquals( 1, len(tnglr.end_chunk) ) - self.assertTrue( tnglr.end_chunk[0] is self.theChunk ) - self.assertEquals( "the words & text of this Chunk", "".join( tnglr.written ) ) - - -class TestOutputChunk( unittest.TestCase ): - def setUp( self ): - self.theChunk= pyweb.OutputChunk( "filename", "#", "" ) - cmd= self.theChunk.makeContent( "the words & text of this Chunk" ) - self.theChunk.append( cmd ) - self.theChunk.setUserIDRefs( "index terms" ) - - def test_append_to_web_should_work( self ): - web= MockWeb() - self.theChunk.webAdd( web ) - self.assertEquals( 1, len(web.chunks) ) - - def test_weave_should_work( self ): - wvr = MockWeaver() - web = MockWeb() - self.theChunk.weave( web, wvr ) - self.assertEquals( 1, len(wvr.begin_chunk) ) - self.assertTrue( wvr.begin_chunk[0] is self.theChunk ) - self.assertEquals( 1, len(wvr.end_chunk) ) - self.assertTrue( wvr.end_chunk[0] is self.theChunk ) - self.assertEquals( "the words & text of this Chunk", "".join( wvr.written ) ) - - def test_tangle_should_work( self ): - tnglr = MockTangler() - web = MockWeb() - self.theChunk.tangle( web, tnglr ) - self.assertEquals( 1, len(tnglr.begin_chunk) ) - self.assertTrue( tnglr.begin_chunk[0] is self.theChunk ) - self.assertEquals( 1, len(tnglr.end_chunk) ) - self.assertTrue( tnglr.end_chunk[0] is self.theChunk ) - self.assertEquals( "the words & text of this Chunk", "".join( tnglr.written ) ) - - - - - - -class TestTextCommand( unittest.TestCase ): - def setUp( self ): - self.cmd= pyweb.TextCommand( "Some text & words in the document\n ", 314 ) - self.cmd2= pyweb.TextCommand( "No Indent\n", 314 ) - def test_methods_should_work( self ): - self.assertTrue( self.cmd.startswith("Some") ) - self.assertFalse( self.cmd.startswith("text") ) - pat1= re.compile( r"\Wthe\W" ) - self.assertTrue( self.cmd.searchForRE(pat1) is not None ) - pat2= re.compile( r"\Wnothing\W" ) - self.assertTrue( self.cmd.searchForRE(pat2) is None ) - self.assertEquals( 4, self.cmd.indent() ) - self.assertEquals( 0, self.cmd2.indent() ) - def test_weave_should_work( self ): - wvr = MockWeaver() - web = MockWeb() - self.cmd.weave( web, wvr ) - self.assertEquals( "Some text & words in the document\n ", "".join( wvr.written ) ) - def test_tangle_should_work( self ): - tnglr = MockTangler() - web = MockWeb() - self.cmd.tangle( web, tnglr ) - self.assertEquals( "Some text & words in the document\n ", "".join( tnglr.written ) ) - - -class TestCodeCommand( unittest.TestCase ): - def setUp( self ): - self.cmd= pyweb.CodeCommand( "Some text & words in the document\n ", 314 ) - def test_weave_should_work( self ): - wvr = MockWeaver() - web = MockWeb() - self.cmd.weave( web, wvr ) - self.assertEquals( "Some text & words in the document\n ", "".join( wvr.written ) ) - def test_tangle_should_work( self ): - tnglr = MockTangler() - web = MockWeb() - self.cmd.tangle( web, tnglr ) - self.assertEquals( "Some text & words in the document\n ", "".join( tnglr.written ) ) - - - -class TestFileXRefCommand( unittest.TestCase ): - def setUp( self ): - self.cmd= pyweb.FileXrefCommand( 314 ) - def test_weave_should_work( self ): - wvr = MockWeaver() - web = MockWeb() - self.cmd.weave( web, wvr ) - self.assertEquals( "file [1, 2, 3]", "".join( wvr.written ) ) - def test_tangle_should_fail( self ): - tnglr = MockTangler() - web = MockWeb() - try: - self.cmd.tangle( web, tnglr ) - self.fail() - except pyweb.Error: - pass - - -class TestMacroXRefCommand( unittest.TestCase ): - def setUp( self ): - self.cmd= pyweb.MacroXrefCommand( 314 ) - def test_weave_should_work( self ): - wvr = MockWeaver() - web = MockWeb() - self.cmd.weave( web, wvr ) - self.assertEquals( "chunk [4, 5, 6]", "".join( wvr.written ) ) - def test_tangle_should_fail( self ): - tnglr = MockTangler() - web = MockWeb() - try: - self.cmd.tangle( web, tnglr ) - self.fail() - except pyweb.Error: - pass - - -class TestUserIdXrefCommand( unittest.TestCase ): - def setUp( self ): - self.cmd= pyweb.UserIdXrefCommand( 314 ) - def test_weave_should_work( self ): - wvr = MockWeaver() - web = MockWeb() - self.cmd.weave( web, wvr ) - self.assertEquals( "name 7 [8, 9, 10]", "".join( wvr.written ) ) - def test_tangle_should_fail( self ): - tnglr = MockTangler() - web = MockWeb() - try: - self.cmd.tangle( web, tnglr ) - self.fail() - except pyweb.Error: - pass - - -class TestReferenceCommand( unittest.TestCase ): - def setUp( self ): - self.chunk= MockChunk( "Owning Chunk", 123, 456 ) - self.cmd= pyweb.ReferenceCommand( "Some Name", 314 ) - self.cmd.chunk= self.chunk - self.chunk.commands.append( self.cmd ) - self.chunk.previous_command= pyweb.TextCommand( "", self.chunk.commands[0].lineNumber ) - def test_weave_should_work( self ): - wvr = MockWeaver() - web = MockWeb() - self.cmd.weave( web, wvr ) - self.assertEquals( "Some Name", "".join( wvr.written ) ) - def test_tangle_should_work( self ): - tnglr = MockTangler() - web = MockWeb() - self.cmd.tangle( web, tnglr ) - self.assertEquals( "Some Name", "".join( tnglr.written ) ) - - - -class TestReference( unittest.TestCase ): - def setUp( self ): - self.web= MockWeb() - self.main= MockChunk( "Main", 1, 11 ) - self.parent= MockChunk( "Parent", 2, 22 ) - self.parent.referencedBy= [ self.main ] - self.chunk= MockChunk( "Sub", 3, 33 ) - self.chunk.referencedBy= [ self.parent ] - def test_simple_should_find_one( self ): - self.reference= pyweb.SimpleReference( self.web ) - theList= self.reference.chunkReferencedBy( self.chunk ) - self.assertEquals( 1, len(theList) ) - self.assertEquals( ('Parent',2), theList[0] ) - def test_transitive_should_find_all( self ): - self.reference= pyweb.TransitiveReference( self.web ) - theList= self.reference.chunkReferencedBy( self.chunk ) - self.assertEquals( 2, len(theList) ) - self.assertEquals( ('Parent',2), theList[0] ) - self.assertEquals( ('Main',1), theList[1] ) - - -class TestWebConstruction( unittest.TestCase ): - def setUp( self ): - self.web= pyweb.Web( "Test" ) - - def test_names_definition_should_resolve( self ): - name1= self.web.addDefName( "A Chunk..." ) - self.assertTrue( name1 is None ) - self.assertEquals( 0, len(self.web.named) ) - name2= self.web.addDefName( "A Chunk Of Code" ) - self.assertEquals( "A Chunk Of Code", name2 ) - self.assertEquals( 1, len(self.web.named) ) - name3= self.web.addDefName( "A Chunk..." ) - self.assertEquals( "A Chunk Of Code", name3 ) - self.assertEquals( 1, len(self.web.named) ) - - def test_chunks_should_add_and_index( self ): - chunk= pyweb.Chunk() - chunk.appendText( "some text" ) - chunk.webAdd( self.web ) - self.assertEquals( 1, len(self.web.chunkSeq) ) - self.assertEquals( 0, len(self.web.named) ) - self.assertEquals( 0, len(self.web.output) ) - named= pyweb.NamedChunk( "A Chunk" ) - named.appendText( "some code" ) - named.webAdd( self.web ) - self.assertEquals( 2, len(self.web.chunkSeq) ) - self.assertEquals( 1, len(self.web.named) ) - self.assertEquals( 0, len(self.web.output) ) - out= pyweb.OutputChunk( "A File" ) - out.appendText( "some code" ) - out.webAdd( self.web ) - self.assertEquals( 3, len(self.web.chunkSeq) ) - self.assertEquals( 1, len(self.web.named) ) - self.assertEquals( 1, len(self.web.output) ) - - -class TestWebProcessing( unittest.TestCase ): - def setUp( self ): - self.web= pyweb.Web( "Test" ) - self.chunk= pyweb.Chunk() - self.chunk.appendText( "some text" ) - self.chunk.webAdd( self.web ) - self.out= pyweb.OutputChunk( "A File" ) - self.out.appendText( "some code" ) - nm= self.web.addDefName( "A Chunk" ) - self.out.append( pyweb.ReferenceCommand( nm ) ) - self.out.webAdd( self.web ) - self.named= pyweb.NamedChunk( "A Chunk..." ) - self.named.appendText( "some user2a code" ) - self.named.setUserIDRefs( "user1" ) - nm= self.web.addDefName( "Another Chunk" ) - self.named.append( pyweb.ReferenceCommand( nm ) ) - self.named.webAdd( self.web ) - self.named2= pyweb.NamedChunk( "Another Chunk..." ) - self.named2.appendText( "some user1 code" ) - self.named2.setUserIDRefs( "user2a user2b" ) - self.named2.webAdd( self.web ) - - def test_name_queries_should_resolve( self ): - self.assertEquals( "A Chunk", self.web.fullNameFor( "A C..." ) ) - self.assertEquals( "A Chunk", self.web.fullNameFor( "A Chunk" ) ) - self.assertNotEquals( "A Chunk", self.web.fullNameFor( "A File" ) ) - self.assertTrue( self.named is self.web.getchunk( "A C..." )[0] ) - self.assertTrue( self.named is self.web.getchunk( "A Chunk" )[0] ) - try: - self.assertTrue( None is not self.web.getchunk( "A File" ) ) - self.fail() - except pyweb.Error, e: - self.assertTrue( e.args[0].startswith("Cannot resolve 'A File'") ) - - - def test_valid_web_should_createUsedBy( self ): - self.web.createUsedBy() - # If it raises an exception, the web structure is damaged - def test_valid_web_should_createFileXref( self ): - file_xref= self.web.fileXref() - self.assertEquals( 1, len(file_xref) ) - self.assertTrue( "A File" in file_xref ) - self.assertTrue( 1, len(file_xref["A File"]) ) - def test_valid_web_should_createChunkXref( self ): - chunk_xref= self.web.chunkXref() - self.assertEquals( 2, len(chunk_xref) ) - self.assertTrue( "A Chunk" in chunk_xref ) - self.assertEquals( 1, len(chunk_xref["A Chunk"]) ) - self.assertTrue( "Another Chunk" in chunk_xref ) - self.assertEquals( 1, len(chunk_xref["Another Chunk"]) ) - self.assertFalse( "Not A Real Chunk" in chunk_xref ) - def test_valid_web_should_create_userNamesXref( self ): - user_xref= self.web.userNamesXref() - self.assertEquals( 3, len(user_xref) ) - self.assertTrue( "user1" in user_xref ) - defn, reflist= user_xref["user1"] - self.assertEquals( 1, len(reflist), "did not find user1" ) - self.assertTrue( "user2a" in user_xref ) - defn, reflist= user_xref["user2a"] - self.assertEquals( 1, len(reflist), "did not find user2a" ) - self.assertTrue( "user2b" in user_xref ) - defn, reflist= user_xref["user2b"] - self.assertEquals( 0, len(reflist) ) - self.assertFalse( "Not A User Symbol" in user_xref ) - - - def test_valid_web_should_tangle( self ): - tangler= MockTangler() - self.web.tangle( tangler ) - self.assertEquals( 3, len(tangler.written) ) - self.assertEquals( ['some code', 'some user2a code', 'some user1 code'], tangler.written ) - - - def test_valid_web_should_weave( self ): - weaver= MockWeaver() - self.web.weave( weaver ) - self.assertEquals( 6, len(weaver.written) ) - expected= ['some text', 'some code', None, 'some user2a code', None, 'some user1 code'] - self.assertEquals( expected, weaver.written ) - - - - - -class MockAction( object ): - def __init__( self ): - self.count= 0 - def __call__( self ): - self.count += 1 - -class MockWebReader( object ): - def __init__( self ): - self.count= 0 - self.theWeb= None - def web( self, aWeb ): - self.theWeb= aWeb - return self - def load( self ): - self.count += 1 - -class TestActionSequence( unittest.TestCase ): - def setUp( self ): - self.web= MockWeb() - self.a1= MockAction() - self.a2= MockAction() - self.action= pyweb.ActionSequence( "TwoSteps", [self.a1, self.a2] ) - self.action.web= self.web - def test_should_execute_both( self ): - self.action() - for c in self.action.opSequence: - self.assertEquals( 1, c.count ) - self.assertTrue( self.web is c.web ) - - -class TestLoadAction( unittest.TestCase ): - def setUp( self ): - self.web= MockWeb() - self.action= pyweb.LoadAction( ) - self.webReader= MockWebReader() - self.webReader.theWeb= self.web - self.action.webReader= self.webReader - self.action.web= self.web - def test_should_execute_tangling( self ): - self.action() - self.assertEquals( 1, self.webReader.count ) - - -class TestTangleAction( unittest.TestCase ): - def setUp( self ): - self.web= MockWeb() - self.action= pyweb.TangleAction( ) - self.tangler= MockTangler() - self.action.theTangler= self.tangler - self.action.web= self.web - def test_should_execute_tangling( self ): - self.action() - self.assertTrue( self.web.tangled is self.tangler ) - - -class TestWeaveAction( unittest.TestCase ): - def setUp( self ): - self.web= MockWeb() - self.action= pyweb.WeaveAction( ) - self.weaver= MockWeaver() - self.action.theWeaver= self.weaver - self.action.web= self.web - def test_should_execute_weaving( self ): - self.action() - self.assertTrue( self.web.wove is self.weaver ) - - - - -if __name__ == "__main__": - import sys - logging.basicConfig( stream=sys.stdout, level= logging.WARN ) - unittest.main() - diff --git a/test/test_weaver.py b/test/test_weaver.py deleted file mode 100644 index ef2b1ea..0000000 --- a/test/test_weaver.py +++ /dev/null @@ -1,142 +0,0 @@ -from __future__ import print_function -"""Weaver tests exercise various weaving features.""" -import pyweb -import unittest -import logging -import StringIO -import os -import difflib -import string - - -class WeaveTestcase( unittest.TestCase ): - text= "" - file_name= "" - error= "" - def setUp( self ): - source= StringIO.StringIO( self.text ) - self.web= pyweb.Web( self.file_name ) - self.rdr= pyweb.WebReader() - self.rdr.source( self.file_name, source ).web( self.web ) - self.rdr.load() - def tangle_and_check_exception( self, exception_text ): - try: - self.rdr.load() - self.web.tangle( self.tangler ) - self.web.createUsedBy() - self.fail( "Should not tangle" ) - except pyweb.Error, e: - self.assertEquals( exception_text, e.args[0] ) - def tearDown( self ): - name, _ = os.path.splitext( self.file_name ) - try: - os.remove( name + ".html" ) - except OSError: - pass - - - -test0_w= """ - - - - -@ - -@d some code -@{ -def fastExp( n, p ): - r= 1 - while p > 0: - if p%2 == 1: return n*fastExp(n,p-1) - return n*n*fastExp(n,p/2) - -for i in range(24): - fastExp(2,i) -@} - - -""" - - -expected= """ - - - - - some code (1) - - - - -

    some code (1) =

    -
    
    -
    -def fastExp( n, p ):
    -    r= 1
    -    while p > 0:
    -        if p%2 == 1: return n*fastExp(n,p-1)
    -	return n*n*fastExp(n,p/2)
    -
    -for i in range(24):
    -    fastExp(2,i)
    -
    -    
    -

    some code (1). - -

    - - - -""" - - -class Test_RefDefWeave( WeaveTestcase ): - text= test0_w - file_name = "test0.w" - def test_load_should_createChunks( self ): - self.assertEquals( 3, len( self.web.chunkSeq ) ) - def test_weave_should_createFile( self ): - doc= pyweb.HTML() - self.web.weave( doc ) - with open("test0.html","r") as source: - actual= source.read() - m= difflib.SequenceMatcher( lambda x: x in string.whitespace, expected, actual ) - for tag, i1, i2, j1, j2 in m.get_opcodes(): - if tag == "equal": continue - self.fail( "At %d %s: expected %r, actual %r" % ( j1, tag, repr(expected[i1:i2]), repr(actual[j1:j2]) ) ) - - - - -test9_w= """An anonymous chunk. -Time = @(time.asctime()@) -File = @(theLocation@) -Version = @(__version__@) -OS = @(os.name@) -CWD = @(os.getcwd()@) -""" - - -class TestEvaluations( WeaveTestcase ): - text= test9_w - file_name = "test9.w" - def test_should_evaluate( self ): - doc= pyweb.HTML() - self.web.weave( doc ) - with open("test9.html","r") as source: - actual= source.readlines() - #print( actual ) - self.assertEquals( "An anonymous chunk.\n", actual[0] ) - self.assertTrue( actual[1].startswith( "Time =" ) ) - self.assertEquals( "File = ('test9.w', 3, 3)\n", actual[2] ) - self.assertEquals( 'Version = $Revision$\n', actual[3] ) - self.assertEquals( 'OS = %s\n' % os.name, actual[4] ) - self.assertEquals( 'CWD = %s\n' % os.getcwd(), actual[5] ) - - -if __name__ == "__main__": - import sys - logging.basicConfig( stream=sys.stdout, level= logging.WARN ) - unittest.main() - diff --git a/test/unit.w b/test/unit.w deleted file mode 100644 index 8d40ed9..0000000 --- a/test/unit.w +++ /dev/null @@ -1,1136 +0,0 @@ - - -

    There are several broad areas of unit testing. There are the 34 classes in this application. -However, it isn't really necessary to test everyone single one of these classes. -We'll decompose these into several hierarchies. -

    - -
      -
    • Emitters -
        -
      • class Emitter( object ):
      • -
      • class Weaver( Emitter ):
      • -
      • class LaTeX( Weaver ):
      • -
      • class HTML( Weaver ):
      • -
      • class HTMLShort( HTML ):
      • -
      • class Tangler( Emitter ):
      • -
      • class TanglerMake( Tangler ):
      • -
      -
    • -
    • Structure: Chunk, Command -
        -
      • class Chunk( object ):
      • -
      • class NamedChunk( Chunk ):
      • -
      • class OutputChunk( NamedChunk ):
      • -
      • class NamedDocumentChunk( NamedChunk ):
      • -
      • class MyNewCommand( Command ):
      • -
      • class Command( object ):
      • -
      • class TextCommand( Command ):
      • -
      • class CodeCommand( TextCommand ):
      • -
      • class XrefCommand( Command ):
      • -
      • class FileXrefCommand( XrefCommand ):
      • -
      • class MacroXrefCommand( XrefCommand ):
      • -
      • class UserIdXrefCommand( XrefCommand ):
      • -
      • class ReferenceCommand( Command ):
      • -
      -
    • -
    • class Error( Exception ): pass
    • -
    • Reference Handling -
        -
      • class Reference( object ):
      • -
      • class SimpleReference( Reference ):
      • -
      • class TransitiveReference( Reference ):
      • -
      -
    • -
    • class Web( object ):
    • -
    • class WebReader( object ):
    • -
    • Action -
        -
      • class Action( object ):
      • -
      • class ActionSequence( Action ):
      • -
      • class WeaveAction( Action ):
      • -
      • class TangleAction( Action ):
      • -
      • class LoadAction( Action ):
      • -
      -
    • -
    • class Application( object ):
    • -
    • class MyWeaver( HTML ):
    • -
    • class MyHTML( pyweb.HTML ):
    • -
    - -

    This gives us the following outline for unit testing.

    - -@o test_unit.py -@{@ -@ -@ -@ -@ -@ -@ -@ -@ -@ -@} - -

    Emitter Tests

    - -

    The emitter class hierarchy produces output files; either woven output -which uses templates to generate proper markup, or tangled output which -precisely follows the document structure. -

    - -@d Unit Test of Emitter class hierarchy... @{ -@ -@ -@ -@ -@ -@ -@ -@ -@} - -

    The Emitter superclass is designed to be extended. The test -creates a subclass to exercise a few key features.

    - -@d Unit Test of Emitter Superclass... @{ -class EmitterExtension( pyweb.Emitter ): - def doOpen( self, fileName ): - self.file= StringIO.StringIO() - def doClose( self ): - self.file.flush() - def doWrite( self, text ): - self.file.write( text ) - -class TestEmitter( unittest.TestCase ): - def setUp( self ): - self.emitter= EmitterExtension() - def test_emitter_should_open_close_write( self ): - self.emitter.open( "test.tmp" ) - self.emitter.write( "Something" ) - self.emitter.close() - self.assertEquals( "Something", self.emitter.file.getvalue() ) - def test_emitter_should_codeBlock( self ): - self.emitter.open( "test.tmp" ) - self.emitter.codeBlock( "Some Code" ) - self.emitter.close() - self.assertEquals( "Some Code\n", self.emitter.file.getvalue() ) - def test_emitter_should_indent( self ): - self.emitter.open( "test.tmp" ) - self.emitter.codeBlock( "Begin\n" ) - self.emitter.setIndent( 4 ) - self.emitter.codeBlock( "More Code\n" ) - self.emitter.clrIndent() - self.emitter.codeBlock( "End" ) - self.emitter.close() - self.assertEquals( "Begin\n More Code\nEnd\n", self.emitter.file.getvalue() ) -@} - -

    A Mock Chunk is a Chunk-like object that we can use to test Weavers.

    - -@d Unit Test Mock Chunk... -@{ -class MockChunk( object ): - def __init__( self, name, seq, lineNumber ): - self.name= name - self.fullName= name - self.seq= seq - self.lineNumber= lineNumber - self.initial= True - self.commands= [] - self.referencedBy= [] -@} - -

    The default Weaver is an Emitter that uses templates to produce RST markup.

    - -@d Unit Test of Weaver... @{ -class TestWeaver( unittest.TestCase ): - def setUp( self ): - self.weaver= pyweb.Weaver() - self.filename= "testweaver.w" - self.aFileChunk= MockChunk( "File", 123, 456 ) - self.aFileChunk.references_list= [ ] - self.aChunk= MockChunk( "Chunk", 314, 278 ) - self.aChunk.references_list= [ ("Container", 123) ] - def tearDown( self ): - import os - try: - os.remove( "testweaver.rst" ) - except OSError: - pass - - def test_weaver_functions( self ): - result= self.weaver.quote( "|char| `code` *em* _em_" ) - self.assertEquals( "\|char\| \`code\` \*em\* \_em\_", result ) - result= self.weaver.references( self.aChunk ) - self.assertEquals( "\nUsed by: Container (`123`_)\n", result ) - result= self.weaver.referenceTo( "Chunk", 314 ) - self.assertEquals( "|srarr| Chunk (`314`_)", result ) - - def test_weaver_should_codeBegin( self ): - self.weaver.open( self.filename ) - self.weaver.codeBegin( self.aChunk ) - self.weaver.codeBlock( self.weaver.quote( "*The* `Code`\n" ) ) - self.weaver.codeEnd( self.aChunk ) - self.weaver.close() - with open( "testweaver.rst", "r" ) as result: - txt= result.read() - self.assertEquals( "\n.. _`314`:\n.. rubric:: Chunk (314)\n.. parsed-literal::\n\n \\*The\\* \\`Code\\`\n\n\nUsed by: Container (`123`_)\n\n\n", txt ) - - def test_weaver_should_fileBegin( self ): - self.weaver.open( self.filename ) - self.weaver.fileBegin( self.aFileChunk ) - self.weaver.codeBlock( self.weaver.quote( "*The* `Code`\n" ) ) - self.weaver.fileEnd( self.aFileChunk ) - self.weaver.close() - with open( "testweaver.rst", "r" ) as result: - txt= result.read() - self.assertEquals( "\n.. _`123`:\n.. rubric:: File (123)\n.. parsed-literal::\n\n \\*The\\* \\`Code\\`\n\n\n\n", txt ) - - def test_weaver_should_xref( self ): - self.weaver.open( self.filename ) - self.weaver.xrefHead( ) - self.weaver.xrefLine( "Chunk", [ ("Container", 123) ] ) - self.weaver.xrefFoot( ) - self.weaver.fileEnd( self.aFileChunk ) - self.weaver.close() - with open( "testweaver.rst", "r" ) as result: - txt= result.read() - self.assertEquals( "\n:Chunk:\n |srarr| (`('Container', 123)`_)\n\n\n\n", txt ) - - def test_weaver_should_xref_def( self ): - self.weaver.open( self.filename ) - self.weaver.xrefHead( ) - self.weaver.xrefDefLine( "Chunk", 314, [ ("Container", 123), ("Chunk", 314) ] ) - self.weaver.xrefFoot( ) - self.weaver.fileEnd( self.aFileChunk ) - self.weaver.close() - with open( "testweaver.rst", "r" ) as result: - txt= result.read() - self.assertEquals( "\n:Chunk:\n [`314`_] `('Chunk', 314)`_ `('Container', 123)`_\n\n\n\n", txt ) -@} - -

    A significant fraction of the various subclasses of weaver are simply -expansion of templates. There's no real point in testing the template -expansion, since that's more easily tested by running a document -through pyweb and looking at the results. -

    - -

    We'll examine a few features of the LaTeX templates.

    - -@d Unit Test of LaTeX... @{ -class TestLaTeX( unittest.TestCase ): - def setUp( self ): - self.weaver= pyweb.LaTeX() - self.filename= "testweaver.w" - self.aFileChunk= MockChunk( "File", 123, 456 ) - self.aFileChunk.references_list= [ ] - self.aChunk= MockChunk( "Chunk", 314, 278 ) - self.aChunk.references_list= [ ("Container", 123) ] - def tearDown( self ): - import os - try: - os.remove( "testweaver.tex" ) - except OSError: - pass - - def test_weaver_functions( self ): - result= self.weaver.quote( "\\end{Verbatim}" ) - self.assertEquals( "\\end\\,{Verbatim}", result ) - result= self.weaver.references( self.aChunk ) - self.assertEquals( "\n \\footnotesize\n Used by:\n \\begin{list}{}{}\n \n \\item Code example Container (123) (Sect. \\ref{pyweb123}, p. \\pageref{pyweb123})\n\n \\end{list}\n \\normalsize\n", result ) - result= self.weaver.referenceTo( "Chunk", 314 ) - self.assertEquals( "$\\triangleright$ Code Example Chunk (314)", result ) -@} - -

    We'll examine a few features of the HTML templates.

    - -@d Unit Test of HTML subclass... @{ -class TestHTML( unittest.TestCase ): - def setUp( self ): - self.weaver= pyweb.HTML() - self.filename= "testweaver.w" - self.aFileChunk= MockChunk( "File", 123, 456 ) - self.aFileChunk.references_list= [ ] - self.aChunk= MockChunk( "Chunk", 314, 278 ) - self.aChunk.references_list= [ ("Container", 123) ] - def tearDown( self ): - import os - try: - os.remove( "testweaver.html" ) - except OSError: - pass - - def test_weaver_functions( self ): - result= self.weaver.quote( "a < b && c > d" ) - self.assertEquals( "a < b && c > d", result ) - result= self.weaver.references( self.aChunk ) - self.assertEquals( ' Used by Container (123).', result ) - result= self.weaver.referenceTo( "Chunk", 314 ) - self.assertEquals( 'Chunk (314)', result ) - -@} - -

    The unique feature of the HTMLShort class is just a template change. -

    - -

    To Do: Test this.

    - -@d Unit Test of HTMLShort subclass... @{ @} - -

    A Tangler emits the various named source files in proper format for the desired -compiler and language.

    - -@d Unit Test of Tangler subclass... -@{ -class TestTangler( unittest.TestCase ): - def setUp( self ): - self.tangler= pyweb.Tangler() - self.filename= "testtangler.w" - self.aFileChunk= MockChunk( "File", 123, 456 ) - self.aFileChunk.references_list= [ ] - self.aChunk= MockChunk( "Chunk", 314, 278 ) - self.aChunk.references_list= [ ("Container", 123) ] - def tearDown( self ): - import os - try: - os.remove( "testtangler.w" ) - except OSError: - pass - - def test_tangler_functions( self ): - result= self.tangler.quote( string.printable ) - self.assertEquals( string.printable, result ) - def test_tangler_should_codeBegin( self ): - self.tangler.open( self.filename ) - self.tangler.codeBegin( self.aChunk ) - self.tangler.codeBlock( self.tangler.quote( "*The* `Code`\n" ) ) - self.tangler.codeEnd( self.aChunk ) - self.tangler.close() - with open( "testtangler.w", "r" ) as result: - txt= result.read() - self.assertEquals( "*The* `Code`\n", txt ) -@} - -

    A TanglerMake uses a cheap hack to see if anything changed. -It creates a temporary file and then does a complete file difference -check. If the file is different, the old version is replaced with -the new version. If the file content is the same, the old version -is left intact with all of the operating system creation timestamps -untouched. -

    - -

    In order to be sure that the timestamps really have changed, we -need to wait for a full second to elapse. -

    - - -@d Unit Test of TanglerMake subclass... @{ -class TestTanglerMake( unittest.TestCase ): - def setUp( self ): - self.tangler= pyweb.TanglerMake() - self.filename= "testtangler.w" - self.aChunk= MockChunk( "Chunk", 314, 278 ) - self.aChunk.references_list= [ ("Container", 123) ] - self.tangler.open( self.filename ) - self.tangler.codeBegin( self.aChunk ) - self.tangler.codeBlock( self.tangler.quote( "*The* `Code`\n" ) ) - self.tangler.codeEnd( self.aChunk ) - self.tangler.close() - self.original= os.path.getmtime( self.filename ) - time.sleep( 1.0 ) # Attempt to assure timestamps are different - def tearDown( self ): - import os - try: - os.remove( "testtangler.w" ) - except OSError: - pass - - def test_same_should_leave( self ): - self.tangler.open( self.filename ) - self.tangler.codeBegin( self.aChunk ) - self.tangler.codeBlock( self.tangler.quote( "*The* `Code`\n" ) ) - self.tangler.codeEnd( self.aChunk ) - self.tangler.close() - self.assertEquals( self.original, os.path.getmtime( self.filename ) ) - - def test_different_should_update( self ): - self.tangler.open( self.filename ) - self.tangler.codeBegin( self.aChunk ) - self.tangler.codeBlock( self.tangler.quote( "*Completely Different* `Code`\n" ) ) - self.tangler.codeEnd( self.aChunk ) - self.tangler.close() - self.assertNotEquals( self.original, os.path.getmtime( self.filename ) ) -@} - -

    Chunk Tests

    - -

    The Chunk and Command class hierarchies model the input document -- the web -of chunks that are used to produce the documentation and the source files. -

    - -@d Unit Test of Chunk class hierarchy... -@{ -@ -@ -@ -@ -@} - -

    In order to test the Chunk superclass, we need several mock objects. -A Chunk contains one or more commands. A Chunk is a part of a Web. -Also, a Chunk is processed by a Tangler or a Weaver. We'll need -Mock objects for all of these relationships in which a Chunk participates. -

    - -

    A MockCommand can be attached to a Chunk.

    - -@d Unit Test of Chunk superclass... -@{ -class MockCommand( object ): - def __init__( self ): - self.lineNumber= 314 - def startswith( self, text ): - return False -@} - -

    A MockWeb can contain a Chunk.

    - -@d Unit Test of Chunk superclass... -@{ -class MockWeb( object ): - def __init__( self ): - self.chunks= [] - self.wove= None - self.tangled= None - def add( self, aChunk ): - self.chunks.append( aChunk ) - def addNamed( self, aChunk ): - self.chunks.append( aChunk ) - def addOutput( self, aChunk ): - self.chunks.append( aChunk ) - def fullNameFor( self, name ): - return name - def fileXref( self ): - return { 'file':[1,2,3] } - def chunkXref( self ): - return { 'chunk':[4,5,6] } - def userNamesXref( self ): - return { 'name':(7,[8,9,10]) } - def getchunk( self, name ): - return [ MockChunk( name, 1, 314 ) ] - def createUsedBy( self ): - pass - def weaveChunk( self, name, weaver ): - weaver.write( name ) - def tangleChunk( self, name, tangler ): - tangler.write( name ) - def weave( self, weaver ): - self.wove= weaver - def tangle( self, tangler ): - self.tangled= tangler -@} - -

    A MockWeaver or MockTangle can process a Chunk.

    - -@d Unit Test of Chunk superclass... -@{ -class MockWeaver( object ): - def __init__( self ): - self.begin_chunk= [] - self.end_chunk= [] - self.written= [] - self.code_indent= None - def quote( self, text ): - return text.replace( "&", "&" ) # token quoting - def docBegin( self, aChunk ): - self.begin_chunk.append( aChunk ) - def write( self, text ): - self.written.append( text ) - def docEnd( self, aChunk ): - self.end_chunk.append( aChunk ) - def codeBegin( self, aChunk ): - self.begin_chunk.append( aChunk ) - def codeBlock( self, text ): - self.written.append( text ) - def codeEnd( self, aChunk ): - self.end_chunk.append( aChunk ) - def fileBegin( self, aChunk ): - self.begin_chunk.append( aChunk ) - def fileEnd( self, aChunk ): - self.end_chunk.append( aChunk ) - def setIndent( self, fixed=None, command=None ): - pass - def clrIndent( self ): - pass - def xrefHead( self ): - pass - def xrefLine( self, name, refList ): - self.written.append( "%s %s" % ( name, refList ) ) - def xrefDefLine( self, name, defn, refList ): - self.written.append( "%s %s %s" % ( name, defn, refList ) ) - def xrefFoot( self ): - pass - def open( self, aFile ): - pass - def close( self ): - pass - def referenceTo( self, name, seq ): - pass - -class MockTangler( MockWeaver ): - def __init__( self ): - super( MockTangler, self ).__init__() - self.context= [0] -@} - -

    A Chunk is built, interrogated and then emitted.

    - -@d Unit Test of Chunk superclass... -@{ -class TestChunk( unittest.TestCase ): - def setUp( self ): - self.theChunk= pyweb.Chunk() - @ - @ - @ -@} - -

    Can we build a Chunk?

    - -@d Unit Test of Chunk construction... -@{ -def test_append_command_should_work( self ): - cmd1= MockCommand() - self.theChunk.append( cmd1 ) - self.assertEquals( 1, len(self.theChunk.commands ) ) - cmd2= MockCommand() - self.theChunk.append( cmd2 ) - self.assertEquals( 2, len(self.theChunk.commands ) ) - -def test_append_initial_and_more_text_should_work( self ): - self.theChunk.appendText( "hi mom" ) - self.assertEquals( 1, len(self.theChunk.commands ) ) - self.theChunk.appendText( "&more text" ) - self.assertEquals( 1, len(self.theChunk.commands ) ) - self.assertEquals( "hi mom&more text", self.theChunk.commands[0].text ) - -def test_append_following_text_should_work( self ): - cmd1= MockCommand() - self.theChunk.append( cmd1 ) - self.theChunk.appendText( "hi mom" ) - self.assertEquals( 2, len(self.theChunk.commands ) ) - -def test_append_to_web_should_work( self ): - web= MockWeb() - self.theChunk.webAdd( web ) - self.assertEquals( 1, len(web.chunks) ) -@} - -

    Can we interrogate a Chunk?

    - -@d Unit Test of Chunk interrogation... -@{ -def test_leading_command_should_not_find( self ): - self.assertFalse( self.theChunk.startswith( "hi mom" ) ) - cmd1= MockCommand() - self.theChunk.append( cmd1 ) - self.assertFalse( self.theChunk.startswith( "hi mom" ) ) - self.theChunk.appendText( "hi mom" ) - self.assertEquals( 2, len(self.theChunk.commands ) ) - self.assertFalse( self.theChunk.startswith( "hi mom" ) ) - -def test_leading_text_should_not_find( self ): - self.assertFalse( self.theChunk.startswith( "hi mom" ) ) - self.theChunk.appendText( "hi mom" ) - self.assertTrue( self.theChunk.startswith( "hi mom" ) ) - cmd1= MockCommand() - self.theChunk.append( cmd1 ) - self.assertTrue( self.theChunk.startswith( "hi mom" ) ) - self.assertEquals( 2, len(self.theChunk.commands ) ) - -def test_regexp_exists_should_find( self ): - self.theChunk.appendText( "this chunk has many words" ) - pat= re.compile( r"\Wchunk\W" ) - found= self.theChunk.searchForRE(pat) - self.assertTrue( found is self.theChunk ) -def test_regexp_missing_should_not_find( self ): - self.theChunk.appendText( "this chunk has many words" ) - pat= re.compile( "\Warpigs\W" ) - found= self.theChunk.searchForRE(pat) - self.assertTrue( found is None ) - -def test_lineNumber_should_work( self ): - self.assertTrue( self.theChunk.lineNumber is None ) - cmd1= MockCommand() - self.theChunk.append( cmd1 ) - self.assertEqual( 314, self.theChunk.lineNumber ) -@} - -

    Can we emit a Chunk with a weaver or tangler?

    - -@d Unit Test of Chunk emission... -@{ -def test_weave_should_work( self ): - wvr = MockWeaver() - web = MockWeb() - self.theChunk.appendText( "this chunk has very & many words" ) - self.theChunk.weave( web, wvr ) - self.assertEquals( 1, len(wvr.begin_chunk) ) - self.assertTrue( wvr.begin_chunk[0] is self.theChunk ) - self.assertEquals( 1, len(wvr.end_chunk) ) - self.assertTrue( wvr.end_chunk[0] is self.theChunk ) - self.assertEquals( "this chunk has very & many words", "".join( wvr.written ) ) - -def test_tangle_should_fail( self ): - tnglr = MockTangler() - web = MockWeb() - self.theChunk.appendText( "this chunk has very & many words" ) - try: - self.theChunk.tangle( web, tnglr ) - self.fail() - except pyweb.Error, e: - self.assertEquals( "Cannot tangle an anonymous chunk", e.args[0] ) -@} - -

    The NamedChunk is created by a @@d command. -Since it's named, it appears in the Web's index. Also, it is woven -and tangled differently than anonymous chunks. -

    - -@d Unit Test of NamedChunk subclass... @{ -class TestNamedChunk( unittest.TestCase ): - def setUp( self ): - self.theChunk= pyweb.NamedChunk( "Some Name..." ) - cmd= self.theChunk.makeContent( "the words & text of this Chunk" ) - self.theChunk.append( cmd ) - self.theChunk.setUserIDRefs( "index terms" ) - - def test_should_find_xref_words( self ): - self.assertEquals( 2, len(self.theChunk.getUserIDRefs()) ) - self.assertEquals( "index", self.theChunk.getUserIDRefs()[0] ) - self.assertEquals( "terms", self.theChunk.getUserIDRefs()[1] ) - - def test_append_to_web_should_work( self ): - web= MockWeb() - self.theChunk.webAdd( web ) - self.assertEquals( 1, len(web.chunks) ) - - def test_weave_should_work( self ): - wvr = MockWeaver() - web = MockWeb() - self.theChunk.weave( web, wvr ) - self.assertEquals( 1, len(wvr.begin_chunk) ) - self.assertTrue( wvr.begin_chunk[0] is self.theChunk ) - self.assertEquals( 1, len(wvr.end_chunk) ) - self.assertTrue( wvr.end_chunk[0] is self.theChunk ) - self.assertEquals( "the words & text of this Chunk", "".join( wvr.written ) ) - - def test_tangle_should_work( self ): - tnglr = MockTangler() - web = MockWeb() - self.theChunk.tangle( web, tnglr ) - self.assertEquals( 1, len(tnglr.begin_chunk) ) - self.assertTrue( tnglr.begin_chunk[0] is self.theChunk ) - self.assertEquals( 1, len(tnglr.end_chunk) ) - self.assertTrue( tnglr.end_chunk[0] is self.theChunk ) - self.assertEquals( "the words & text of this Chunk", "".join( tnglr.written ) ) -@} - -

    The OutputChunk is created by a @@o command. -Since it's named, it appears in the Web's index. Also, it is woven -and tangled differently than anonymous chunks. -

    - -@d Unit Test of OutputChunk subclass... @{ -class TestOutputChunk( unittest.TestCase ): - def setUp( self ): - self.theChunk= pyweb.OutputChunk( "filename", "#", "" ) - cmd= self.theChunk.makeContent( "the words & text of this Chunk" ) - self.theChunk.append( cmd ) - self.theChunk.setUserIDRefs( "index terms" ) - - def test_append_to_web_should_work( self ): - web= MockWeb() - self.theChunk.webAdd( web ) - self.assertEquals( 1, len(web.chunks) ) - - def test_weave_should_work( self ): - wvr = MockWeaver() - web = MockWeb() - self.theChunk.weave( web, wvr ) - self.assertEquals( 1, len(wvr.begin_chunk) ) - self.assertTrue( wvr.begin_chunk[0] is self.theChunk ) - self.assertEquals( 1, len(wvr.end_chunk) ) - self.assertTrue( wvr.end_chunk[0] is self.theChunk ) - self.assertEquals( "the words & text of this Chunk", "".join( wvr.written ) ) - - def test_tangle_should_work( self ): - tnglr = MockTangler() - web = MockWeb() - self.theChunk.tangle( web, tnglr ) - self.assertEquals( 1, len(tnglr.begin_chunk) ) - self.assertTrue( tnglr.begin_chunk[0] is self.theChunk ) - self.assertEquals( 1, len(tnglr.end_chunk) ) - self.assertTrue( tnglr.end_chunk[0] is self.theChunk ) - self.assertEquals( "the words & text of this Chunk", "".join( tnglr.written ) ) -@} - -

    The NamedDocumentChunk is a little-used feature.

    - -@d Unit Test of NamedDocumentChunk subclass... @{ @} - -

    Command Tests

    - -@d Unit Test of Command class hierarchy... @{ -@ -@ -@ -@ -@ -@ -@ -@ -@} - -

    This Command superclass is essentially an inteface definition, it -has no real testable features.

    -@d Unit Test of Command superclass... @{ @} - -

    A TextCommand object must be constructed, interrogated and emitted.

    - -@d Unit Test of TextCommand class... @{ -class TestTextCommand( unittest.TestCase ): - def setUp( self ): - self.cmd= pyweb.TextCommand( "Some text & words in the document\n ", 314 ) - self.cmd2= pyweb.TextCommand( "No Indent\n", 314 ) - def test_methods_should_work( self ): - self.assertTrue( self.cmd.startswith("Some") ) - self.assertFalse( self.cmd.startswith("text") ) - pat1= re.compile( r"\Wthe\W" ) - self.assertTrue( self.cmd.searchForRE(pat1) is not None ) - pat2= re.compile( r"\Wnothing\W" ) - self.assertTrue( self.cmd.searchForRE(pat2) is None ) - self.assertEquals( 4, self.cmd.indent() ) - self.assertEquals( 0, self.cmd2.indent() ) - def test_weave_should_work( self ): - wvr = MockWeaver() - web = MockWeb() - self.cmd.weave( web, wvr ) - self.assertEquals( "Some text & words in the document\n ", "".join( wvr.written ) ) - def test_tangle_should_work( self ): - tnglr = MockTangler() - web = MockWeb() - self.cmd.tangle( web, tnglr ) - self.assertEquals( "Some text & words in the document\n ", "".join( tnglr.written ) ) -@} - -

    A CodeCommand object is a TextCommand with different processing for being emitted.

    - -@d Unit Test of CodeCommand class... @{ -class TestCodeCommand( unittest.TestCase ): - def setUp( self ): - self.cmd= pyweb.CodeCommand( "Some text & words in the document\n ", 314 ) - def test_weave_should_work( self ): - wvr = MockWeaver() - web = MockWeb() - self.cmd.weave( web, wvr ) - self.assertEquals( "Some text & words in the document\n ", "".join( wvr.written ) ) - def test_tangle_should_work( self ): - tnglr = MockTangler() - web = MockWeb() - self.cmd.tangle( web, tnglr ) - self.assertEquals( "Some text & words in the document\n ", "".join( tnglr.written ) ) -@} - -

    The XrefCommand class is largely abstract.

    - -@d Unit Test of XrefCommand superclass... @{ @} - -

    The FileXrefCommand command is expanded by a weaver to a list of all @@o -locations.

    - -@d Unit Test of FileXrefCommand class... @{ -class TestFileXRefCommand( unittest.TestCase ): - def setUp( self ): - self.cmd= pyweb.FileXrefCommand( 314 ) - def test_weave_should_work( self ): - wvr = MockWeaver() - web = MockWeb() - self.cmd.weave( web, wvr ) - self.assertEquals( "file [1, 2, 3]", "".join( wvr.written ) ) - def test_tangle_should_fail( self ): - tnglr = MockTangler() - web = MockWeb() - try: - self.cmd.tangle( web, tnglr ) - self.fail() - except pyweb.Error: - pass -@} - -

    The MacroXrefCommand command is expanded by a weaver to a list of all @@d -locations.

    - -@d Unit Test of MacroXrefCommand class... @{ -class TestMacroXRefCommand( unittest.TestCase ): - def setUp( self ): - self.cmd= pyweb.MacroXrefCommand( 314 ) - def test_weave_should_work( self ): - wvr = MockWeaver() - web = MockWeb() - self.cmd.weave( web, wvr ) - self.assertEquals( "chunk [4, 5, 6]", "".join( wvr.written ) ) - def test_tangle_should_fail( self ): - tnglr = MockTangler() - web = MockWeb() - try: - self.cmd.tangle( web, tnglr ) - self.fail() - except pyweb.Error: - pass -@} - -

    The UserIdXrefCommand command is expanded by a weaver to a list of all @@| -names.

    - -@d Unit Test of UserIdXrefCommand class... @{ -class TestUserIdXrefCommand( unittest.TestCase ): - def setUp( self ): - self.cmd= pyweb.UserIdXrefCommand( 314 ) - def test_weave_should_work( self ): - wvr = MockWeaver() - web = MockWeb() - self.cmd.weave( web, wvr ) - self.assertEquals( "name 7 [8, 9, 10]", "".join( wvr.written ) ) - def test_tangle_should_fail( self ): - tnglr = MockTangler() - web = MockWeb() - try: - self.cmd.tangle( web, tnglr ) - self.fail() - except pyweb.Error: - pass -@} - -

    Reference commands require a context when tangling. -The context helps provide the required indentation. -They can't be simply tangled. -

    - -@d Unit Test of ReferenceCommand class... @{ -class TestReferenceCommand( unittest.TestCase ): - def setUp( self ): - self.chunk= MockChunk( "Owning Chunk", 123, 456 ) - self.cmd= pyweb.ReferenceCommand( "Some Name", 314 ) - self.cmd.chunk= self.chunk - self.chunk.commands.append( self.cmd ) - self.chunk.previous_command= pyweb.TextCommand( "", self.chunk.commands[0].lineNumber ) - def test_weave_should_work( self ): - wvr = MockWeaver() - web = MockWeb() - self.cmd.weave( web, wvr ) - self.assertEquals( "Some Name", "".join( wvr.written ) ) - def test_tangle_should_work( self ): - tnglr = MockTangler() - web = MockWeb() - self.cmd.tangle( web, tnglr ) - self.assertEquals( "Some Name", "".join( tnglr.written ) ) -@} - -

    Reference Tests

    - -

    The Reference class implements one of two search strategies for -cross-references. Either simple (or "immediate") or transitive. -

    - -

    The superclass is little more than an interface definition, -it's completely abstract. The two subclasses differ in -a single method. -

    - -@d Unit Test of Reference class hierarchy... @{ -class TestReference( unittest.TestCase ): - def setUp( self ): - self.web= MockWeb() - self.main= MockChunk( "Main", 1, 11 ) - self.parent= MockChunk( "Parent", 2, 22 ) - self.parent.referencedBy= [ self.main ] - self.chunk= MockChunk( "Sub", 3, 33 ) - self.chunk.referencedBy= [ self.parent ] - def test_simple_should_find_one( self ): - self.reference= pyweb.SimpleReference( self.web ) - theList= self.reference.chunkReferencedBy( self.chunk ) - self.assertEquals( 1, len(theList) ) - self.assertEquals( ('Parent',2), theList[0] ) - def test_transitive_should_find_all( self ): - self.reference= pyweb.TransitiveReference( self.web ) - theList= self.reference.chunkReferencedBy( self.chunk ) - self.assertEquals( 2, len(theList) ) - self.assertEquals( ('Parent',2), theList[0] ) - self.assertEquals( ('Main',1), theList[1] ) -@} - -

    Web Tests

    - -

    This is more difficult to create mocks for.

    - -@d Unit Test of Web class... -@{ -class TestWebConstruction( unittest.TestCase ): - def setUp( self ): - self.web= pyweb.Web( "Test" ) - @ - -class TestWebProcessing( unittest.TestCase ): - def setUp( self ): - self.web= pyweb.Web( "Test" ) - self.chunk= pyweb.Chunk() - self.chunk.appendText( "some text" ) - self.chunk.webAdd( self.web ) - self.out= pyweb.OutputChunk( "A File" ) - self.out.appendText( "some code" ) - nm= self.web.addDefName( "A Chunk" ) - self.out.append( pyweb.ReferenceCommand( nm ) ) - self.out.webAdd( self.web ) - self.named= pyweb.NamedChunk( "A Chunk..." ) - self.named.appendText( "some user2a code" ) - self.named.setUserIDRefs( "user1" ) - nm= self.web.addDefName( "Another Chunk" ) - self.named.append( pyweb.ReferenceCommand( nm ) ) - self.named.webAdd( self.web ) - self.named2= pyweb.NamedChunk( "Another Chunk..." ) - self.named2.appendText( "some user1 code" ) - self.named2.setUserIDRefs( "user2a user2b" ) - self.named2.webAdd( self.web ) - @ - @ - @ - @ -@} - -@d Unit Test Web class construction... -@{ -def test_names_definition_should_resolve( self ): - name1= self.web.addDefName( "A Chunk..." ) - self.assertTrue( name1 is None ) - self.assertEquals( 0, len(self.web.named) ) - name2= self.web.addDefName( "A Chunk Of Code" ) - self.assertEquals( "A Chunk Of Code", name2 ) - self.assertEquals( 1, len(self.web.named) ) - name3= self.web.addDefName( "A Chunk..." ) - self.assertEquals( "A Chunk Of Code", name3 ) - self.assertEquals( 1, len(self.web.named) ) - -def test_chunks_should_add_and_index( self ): - chunk= pyweb.Chunk() - chunk.appendText( "some text" ) - chunk.webAdd( self.web ) - self.assertEquals( 1, len(self.web.chunkSeq) ) - self.assertEquals( 0, len(self.web.named) ) - self.assertEquals( 0, len(self.web.output) ) - named= pyweb.NamedChunk( "A Chunk" ) - named.appendText( "some code" ) - named.webAdd( self.web ) - self.assertEquals( 2, len(self.web.chunkSeq) ) - self.assertEquals( 1, len(self.web.named) ) - self.assertEquals( 0, len(self.web.output) ) - out= pyweb.OutputChunk( "A File" ) - out.appendText( "some code" ) - out.webAdd( self.web ) - self.assertEquals( 3, len(self.web.chunkSeq) ) - self.assertEquals( 1, len(self.web.named) ) - self.assertEquals( 1, len(self.web.output) ) -@} - -@d Unit Test Web class name resolution... -@{ -def test_name_queries_should_resolve( self ): - self.assertEquals( "A Chunk", self.web.fullNameFor( "A C..." ) ) - self.assertEquals( "A Chunk", self.web.fullNameFor( "A Chunk" ) ) - self.assertNotEquals( "A Chunk", self.web.fullNameFor( "A File" ) ) - self.assertTrue( self.named is self.web.getchunk( "A C..." )[0] ) - self.assertTrue( self.named is self.web.getchunk( "A Chunk" )[0] ) - try: - self.assertTrue( None is not self.web.getchunk( "A File" ) ) - self.fail() - except pyweb.Error, e: - self.assertTrue( e.args[0].startswith("Cannot resolve 'A File'") ) -@} - -@d Unit Test Web class chunk cross-reference @{ -def test_valid_web_should_createUsedBy( self ): - self.web.createUsedBy() - # If it raises an exception, the web structure is damaged -def test_valid_web_should_createFileXref( self ): - file_xref= self.web.fileXref() - self.assertEquals( 1, len(file_xref) ) - self.assertTrue( "A File" in file_xref ) - self.assertTrue( 1, len(file_xref["A File"]) ) -def test_valid_web_should_createChunkXref( self ): - chunk_xref= self.web.chunkXref() - self.assertEquals( 2, len(chunk_xref) ) - self.assertTrue( "A Chunk" in chunk_xref ) - self.assertEquals( 1, len(chunk_xref["A Chunk"]) ) - self.assertTrue( "Another Chunk" in chunk_xref ) - self.assertEquals( 1, len(chunk_xref["Another Chunk"]) ) - self.assertFalse( "Not A Real Chunk" in chunk_xref ) -def test_valid_web_should_create_userNamesXref( self ): - user_xref= self.web.userNamesXref() - self.assertEquals( 3, len(user_xref) ) - self.assertTrue( "user1" in user_xref ) - defn, reflist= user_xref["user1"] - self.assertEquals( 1, len(reflist), "did not find user1" ) - self.assertTrue( "user2a" in user_xref ) - defn, reflist= user_xref["user2a"] - self.assertEquals( 1, len(reflist), "did not find user2a" ) - self.assertTrue( "user2b" in user_xref ) - defn, reflist= user_xref["user2b"] - self.assertEquals( 0, len(reflist) ) - self.assertFalse( "Not A User Symbol" in user_xref ) -@} - -@d Unit Test Web class tangle @{ -def test_valid_web_should_tangle( self ): - tangler= MockTangler() - self.web.tangle( tangler ) - self.assertEquals( 3, len(tangler.written) ) - self.assertEquals( ['some code', 'some user2a code', 'some user1 code'], tangler.written ) -@} - -@d Unit Test Web class weave @{ -def test_valid_web_should_weave( self ): - weaver= MockWeaver() - self.web.weave( weaver ) - self.assertEquals( 6, len(weaver.written) ) - expected= ['some text', 'some code', None, 'some user2a code', None, 'some user1 code'] - self.assertEquals( expected, weaver.written ) -@} - - -

    WebReader Tests

    - -

    Generally, this is tested separately through the functional tests. -Those tests each present source files to be processed by the -WebReader. -

    - -@d Unit Test of WebReader... @{ @} - -

    Action Tests

    - -

    Each class is tested separately. Sequence of some mocks, -load, tangle, weave. -

    - -@d Unit Test of Action class hierarchy... @{ -@ -@ -@ -@ -@} - -@d Unit test of Action Sequence class... @{ -class MockAction( object ): - def __init__( self ): - self.count= 0 - def __call__( self ): - self.count += 1 - -class MockWebReader( object ): - def __init__( self ): - self.count= 0 - self.theWeb= None - def web( self, aWeb ): - self.theWeb= aWeb - return self - def load( self ): - self.count += 1 - -class TestActionSequence( unittest.TestCase ): - def setUp( self ): - self.web= MockWeb() - self.a1= MockAction() - self.a2= MockAction() - self.action= pyweb.ActionSequence( "TwoSteps", [self.a1, self.a2] ) - self.action.web= self.web - def test_should_execute_both( self ): - self.action() - for c in self.action.opSequence: - self.assertEquals( 1, c.count ) - self.assertTrue( self.web is c.web ) -@} - -@d Unit test of WeaverAction class... @{ -class TestWeaveAction( unittest.TestCase ): - def setUp( self ): - self.web= MockWeb() - self.action= pyweb.WeaveAction( ) - self.weaver= MockWeaver() - self.action.theWeaver= self.weaver - self.action.web= self.web - def test_should_execute_weaving( self ): - self.action() - self.assertTrue( self.web.wove is self.weaver ) -@} - -@d Unit test of TangleAction class... @{ -class TestTangleAction( unittest.TestCase ): - def setUp( self ): - self.web= MockWeb() - self.action= pyweb.TangleAction( ) - self.tangler= MockTangler() - self.action.theTangler= self.tangler - self.action.web= self.web - def test_should_execute_tangling( self ): - self.action() - self.assertTrue( self.web.tangled is self.tangler ) -@} - -@d Unit test of LoadAction class... @{ -class TestLoadAction( unittest.TestCase ): - def setUp( self ): - self.web= MockWeb() - self.action= pyweb.LoadAction( ) - self.webReader= MockWebReader() - self.webReader.theWeb= self.web - self.action.webReader= self.webReader - self.action.web= self.web - def test_should_execute_tangling( self ): - self.action() - self.assertEquals( 1, self.webReader.count ) -@} - -

    Application Tests

    - -

    As with testing WebReader, this requires extensive mocking. -It's easier to simply run the various use cases. -

    - -@d Unit Test of Application... @{ @} - -

    Overheads and Main Script

    - -

    The boilerplate code for unit testing is the following.

    - -@d Unit Test overheads... -@{from __future__ import print_function -"""Unit tests.""" -import pyweb -import unittest -import logging -import StringIO -import string -import os -import time -import re -@} - -@d Unit Test main... -@{ -if __name__ == "__main__": - import sys - logging.basicConfig( stream=sys.stdout, level= logging.WARN ) - unittest.main() -@} \ No newline at end of file diff --git a/tests.w b/tests.w deleted file mode 100644 index b153176..0000000 --- a/tests.w +++ /dev/null @@ -1,25 +0,0 @@ -.. pyweb/test.w - -Unit Tests -=========== - -The ``test`` directory includes ``pyweb_test.w``, which will create a -complete test suite. - -This source will weaves a ``pyweb_test.html`` file. See file:test/pyweb_test.html - -This source will tangle several test modules: ``test.py``, ``test_tangler.py``, ``test_weaver.py``, -``test_loader.py`` and ``test_unit.py``. Running the ``test.py`` module will include and -execute all 78 tests. - -Here's a script that works out well for running this without disturbing the development -environment. The ``PYTHONPATH`` setting is essential to support importing ``pyweb``. - -.. parsed-literal:: - - cd test - python ../pyweb.py pyweb_test.w - PYTHONPATH=.. python test.py - -Note that the last line really does set an environment variable and run -a program on a single line. diff --git a/tests/docutils.conf b/tests/docutils.conf new file mode 100644 index 0000000..522baed --- /dev/null +++ b/tests/docutils.conf @@ -0,0 +1,6 @@ +# docutils.conf + +[html4css1 writer] +stylesheet-path: /Users/slott/miniconda3/envs/pywebtool/lib/python3.10/site-packages/docutils/writers/html4css1/html4css1.css, + page-layout.css +syntax-highlight: long diff --git a/tests/func.w b/tests/func.w new file mode 100644 index 0000000..0090fde --- /dev/null +++ b/tests/func.w @@ -0,0 +1,576 @@ +Functional Testing +================== + +.. test/func.w + +There are three broad areas of functional testing. + +- `Tests for Loading`_ + +- `Tests for Tangling`_ + +- `Tests for Weaving`_ + +There are a total of 11 test cases. + +Tests for Loading +------------------ + +We need to be able to load a web from one or more source files. + +@o test_loader.py +@{@ + +@ + +@ + +@ + +@ +@} + +Parsing test cases have a common setup shown in this superclass. + +By using some class-level variables ``text``, +``file_path``, we can simply provide a file-like +input object to the ``WebReader`` instance. + +@d Load Test superclass... +@{ +class ParseTestcase(unittest.TestCase): + text: ClassVar[str] + file_path: ClassVar[Path] + + def setUp(self) -> None: + self.source = io.StringIO(self.text) + self.web = pyweb.Web() + self.rdr = pyweb.WebReader() +@} + +There are a lot of specific parsing exceptions which can be thrown. +We'll cover most of the cases with a quick check for a failure to +find an expected next token. + +@d Load Test overheads... +@{ +import logging.handlers +from pathlib import Path +from typing import ClassVar +@} + +@d Load Test error handling... +@{ +@ + +class Test_ParseErrors(ParseTestcase): + text = test1_w + file_path = Path("test1.w") + def test_error_should_count_1(self) -> None: + with self.assertLogs('WebReader', level='WARN') as log_capture: + self.rdr.load(self.web, self.file_path, self.source) + self.assertEqual(3, self.rdr.errors) + self.assertEqual(log_capture.output, + [ + "ERROR:WebReader:At ('test1.w', 8): expected ('@@{',), found '@@o'", + "ERROR:WebReader:Extra '@@{' (possibly missing chunk name) near ('test1.w', 9)", + "ERROR:WebReader:Extra '@@{' (possibly missing chunk name) near ('test1.w', 9)" + ] + ) +@} + +@d Sample Document 1... +@{ +test1_w = """Some anonymous chunk +@@o test1.tmp +@@{@@ +@@ +@@}@@@@ +@@d part1 @@{This is part 1.@@} +Okay, now for an error. +@@o show how @@o commands work +@@{ @@{ @@] @@] +""" +@} + +All of the parsing exceptions should be correctly identified with +any included file. +We'll cover most of the cases with a quick check for a failure to +find an expected next token. + +In order to test the include file processing, we have to actually +create a temporary file. It's hard to mock the include processing, +since it's a nested instance of the tokenizer. + +@d Load Test include... +@{ +@ + +class Test_IncludeParseErrors(ParseTestcase): + text = test8_w + file_path = Path("test8.w") + def setUp(self) -> None: + super().setUp() + Path('test8_inc.tmp').write_text(test8_inc_w) + def test_error_should_count_2(self) -> None: + with self.assertLogs('WebReader', level='WARN') as log_capture: + self.rdr.load(self.web, self.file_path, self.source) + self.assertEqual(1, self.rdr.errors) + self.assertEqual(log_capture.output, + [ + "ERROR:WebReader:At ('test8_inc.tmp', 4): end of input, ('@@{', '@@[') not found", + "ERROR:WebReader:Errors in included file 'test8_inc.tmp', output is incomplete." + ] + ) + def tearDown(self) -> None: + super().tearDown() + Path('test8_inc.tmp').unlink() +@} + +The sample document must reference the correct name that will +be given to the included document by ``setUp``. + +@d Sample Document 8... +@{ +test8_w = """Some anonymous chunk. +@@d title @@[the title of this document, defined with @@@@[ and @@@@]@@] +A reference to @@. +@@i test8_inc.tmp +A final anonymous chunk from test8.w +""" + +test8_inc_w="""A chunk from test8a.w +And now for an error - incorrect syntax in an included file! +@@d yap +""" +@} + +

    The overheads for a Python unittest.

    + +@d Load Test overheads... +@{ +"""Loader and parsing tests.""" +import io +import logging +import os +from pathlib import Path +import string +import sys +import types +import unittest + +import pyweb +@} + +A main program that configures logging and then runs the test. + +@d Load Test main program... +@{ +if __name__ == "__main__": + logging.basicConfig(stream=sys.stdout, level=logging.WARN) + unittest.main() +@} + +Tests for Tangling +------------------ + +We need to be able to tangle a web. + +@o test_tangler.py +@{@ +@ +@ +@ +@ +@ +@ +@ +@ +@} + +Tangling test cases have a common setup and teardown shown in this superclass. +Since tangling must produce a file, it's helpful to remove the file that gets created. +The essential test case is to load and attempt to tangle, checking the +exceptions raised. + + +@d Tangle Test superclass... +@{ +class TangleTestcase(unittest.TestCase): + text: ClassVar[str] + error: ClassVar[str] + file_path: ClassVar[Path] + + def setUp(self) -> None: + self.source = io.StringIO(self.text) + self.web = pyweb.Web() + self.rdr = pyweb.WebReader() + self.tangler = pyweb.Tangler() + + def tangle_and_check_exception(self, exception_text: str) -> None: + try: + self.rdr.load(self.web, self.file_path, self.source) + self.web.tangle(self.tangler) + self.web.createUsedBy() + self.fail("Should not tangle") + except pyweb.Error as e: + self.assertEqual(exception_text, e.args[0]) + + def tearDown(self) -> None: + try: + self.file_path.with_suffix(".tmp").unlink() + except FileNotFoundError: + pass # If the test fails, nothing to remove... +@} + +@d Tangle Test semantic error 2... +@{ +@ + +class Test_SemanticError_2(TangleTestcase): + text = test2_w + file_path = Path("test2.w") + def test_should_raise_undefined(self) -> None: + self.tangle_and_check_exception("Attempt to tangle an undefined Chunk, part2.") +@} + +@d Sample Document 2... @{ +test2_w = """Some anonymous chunk +@@o test2.tmp +@@{@@ +@@ +@@}@@@@ +@@d part1 @@{This is part 1.@@} +Okay, now for some errors: no part2! +""" +@} + +@d Tangle Test semantic error 3... +@{ +@ + +class Test_SemanticError_3(TangleTestcase): + text = test3_w + file_path = Path("test3.w") + def test_should_raise_bad_xref(self) -> None: + self.tangle_and_check_exception("Illegal tangling of a cross reference command.") +@} + +@d Sample Document 3... @{ +test3_w = """Some anonymous chunk +@@o test3.tmp +@@{@@ +@@ +@@}@@@@ +@@d part1 @@{This is part 1.@@} +@@d part2 @@{This is part 2, with an illegal: @@f.@@} +Okay, now for some errors: attempt to tangle a cross-reference! +""" +@} + + +@d Tangle Test semantic error 4... +@{ +@ + +class Test_SemanticError_4(TangleTestcase): + text = test4_w + file_path = Path("test4.w") + def test_should_raise_noFullName(self) -> None: + self.tangle_and_check_exception("No full name for 'part1...'") +@} + +@d Sample Document 4... @{ +test4_w = """Some anonymous chunk +@@o test4.tmp +@@{@@ +@@ +@@}@@@@ +@@d part1... @@{This is part 1.@@} +@@d part2 @@{This is part 2.@@} +Okay, now for some errors: attempt to weave but no full name for part1.... +""" +@} + +@d Tangle Test semantic error 5... +@{ +@ + +class Test_SemanticError_5(TangleTestcase): + text = test5_w + file_path = Path("test5.w") + def test_should_raise_ambiguous(self) -> None: + self.tangle_and_check_exception("Ambiguous abbreviation 'part1...', matches ['part1a', 'part1b']") +@} + +@d Sample Document 5... @{ +test5_w = """ +Some anonymous chunk +@@o test5.tmp +@@{@@ +@@ +@@}@@@@ +@@d part1a @@{This is part 1 a.@@} +@@d part1b @@{This is part 1 b.@@} +@@d part2 @@{This is part 2.@@} +Okay, now for some errors: part1... is ambiguous +""" +@} + +@d Tangle Test semantic error 6... +@{ +@ + +class Test_SemanticError_6(TangleTestcase): + text = test6_w + file_path = Path("test6.w") + def test_should_warn(self) -> None: + self.rdr.load(self.web, self.file_path, self.source) + self.web.tangle(self.tangler) + self.web.createUsedBy() + self.assertEqual(1, len(self.web.no_reference())) + self.assertEqual(1, len(self.web.multi_reference())) + self.assertEqual(0, len(self.web.no_definition())) +@} + +@d Sample Document 6... @{ +test6_w = """Some anonymous chunk +@@o test6.tmp +@@{@@ +@@ +@@}@@@@ +@@d part1a @@{This is part 1 a.@@} +@@d part2 @@{This is part 2.@@} +Okay, now for some warnings: +- part1 has multiple references. +- part2 is unreferenced. +""" +@} + +@d Tangle Test include error 7... +@{ +@ + +class Test_IncludeError_7(TangleTestcase): + text = test7_w + file_path = Path("test7.w") + def setUp(self) -> None: + Path('test7_inc.tmp').write_text(test7_inc_w) + super().setUp() + def test_should_include(self) -> None: + self.rdr.load(self.web, self.file_path, self.source) + self.web.tangle(self.tangler) + self.web.createUsedBy() + self.assertEqual(5, len(self.web.chunkSeq)) + self.assertEqual(test7_inc_w, self.web.chunkSeq[3].commands[0].text) + def tearDown(self) -> None: + Path('test7_inc.tmp').unlink() + super().tearDown() +@} + +@d Sample Document 7... @{ +test7_w = """ +Some anonymous chunk. +@@d title @@[the title of this document, defined with @@@@[ and @@@@]@@] +A reference to @@. +@@i test7_inc.tmp +A final anonymous chunk from test7.w +""" + +test7_inc_w = """The test7a.tmp chunk for test7.w +""" +@} + +@d Tangle Test overheads... +@{ +"""Tangler tests exercise various semantic features.""" +import io +import logging +import os +from pathlib import Path +from typing import ClassVar +import unittest + +import pyweb +@} + +@d Tangle Test main program... +@{ +if __name__ == "__main__": + import sys + logging.basicConfig(stream=sys.stdout, level=logging.WARN) + unittest.main() +@} + + +Tests for Weaving +----------------- + +We need to be able to weave a document from one or more source files. + +@o test_weaver.py +@{@ +@ +@ +@ +@ +@} + +Weaving test cases have a common setup shown in this superclass. + +@d Weave Test superclass... @{ +class WeaveTestcase(unittest.TestCase): + text: ClassVar[str] + error: ClassVar[str] + file_path: ClassVar[Path] + + def setUp(self) -> None: + self.source = io.StringIO(self.text) + self.web = pyweb.Web() + self.rdr = pyweb.WebReader() + + def tearDown(self) -> None: + try: + self.file_path.with_suffix(".html").unlink() + except FileNotFoundError: + pass # if the test failed, nothing to remove +@} + +@d Weave Test references... @{ +@ +@ + +class Test_RefDefWeave(WeaveTestcase): + text = test0_w + file_path = Path("test0.w") + def test_load_should_createChunks(self) -> None: + self.rdr.load(self.web, self.file_path, self.source) + self.assertEqual(3, len(self.web.chunkSeq)) + def test_weave_should_createFile(self) -> None: + self.rdr.load(self.web, self.file_path, self.source) + doc = pyweb.HTML() + doc.reference_style = pyweb.SimpleReference() + self.web.weave(doc) + actual = self.file_path.with_suffix(".html").read_text() + self.maxDiff = None + self.assertEqual(test0_expected, actual) + +@} + +@d Sample Document 0... +@{ +test0_w = """ + + + + +@@ + +@@d some code +@@{ +def fastExp(n, p): + r = 1 + while p > 0: + if p%2 == 1: return n*fastExp(n,p-1) + return n*n*fastExp(n,p/2) + +for i in range(24): + fastExp(2,i) +@@} + + +""" +@} + +@d Expected Output 0... @{ +test0_expected = """ + + + + +some code (1) + + + + +

    some code (1) =

    +
    
    +
    +def fastExp(n, p):
    +    r = 1
    +    while p > 0:
    +        if p%2 == 1: return n*fastExp(n,p-1)
    +    return n*n*fastExp(n,p/2)
    +
    +for i in range(24):
    +    fastExp(2,i)
    +
    +    
    +

    some code (1). + +

    + + + +""" +@} + +Note that this really requires a mocked ``time`` module in order +to properly provide a consistent output from ``time.asctime()``. + +@d Weave Test evaluation... @{ +@ + +from unittest.mock import Mock + +class TestEvaluations(WeaveTestcase): + text = test9_w + file_path = Path("test9.w") + def setUp(self): + super().setUp() + self.mock_time = Mock(asctime=Mock(return_value="mocked time")) + def test_should_evaluate(self) -> None: + self.rdr.load(self.web, self.file_path, self.source) + doc = pyweb.HTML( ) + doc.reference_style = pyweb.SimpleReference() + self.web.weave(doc) + actual = self.file_path.with_suffix(".html").read_text().splitlines() + #print(actual) + self.assertEqual("An anonymous chunk.", actual[0]) + self.assertTrue("Time = mocked time", actual[1]) + self.assertEqual("File = ('test9.w', 3)", actual[2]) + self.assertEqual('Version = 3.1', actual[3]) + self.assertEqual(f'CWD = {os.getcwd()}', actual[4]) +@} + +@d Sample Document 9... +@{ +test9_w= """An anonymous chunk. +Time = @@(time.asctime()@@) +File = @@(theLocation@@) +Version = @@(__version__@@) +CWD = @@(os.path.realpath('.')@@) +""" +@} + +@d Weave Test overheads... +@{ +"""Weaver tests exercise various weaving features.""" +import io +import logging +import os +from pathlib import Path +import string +import sys +from typing import ClassVar +import unittest + +import pyweb +@} + +@d Weave Test main program... +@{ +if __name__ == "__main__": + logging.basicConfig(stream=sys.stderr, level=logging.WARN) + unittest.main() +@} diff --git a/tests/intro.w b/tests/intro.w new file mode 100644 index 0000000..23b3631 --- /dev/null +++ b/tests/intro.w @@ -0,0 +1,61 @@ +Introduction +============ + +.. test/intro.w + +There are two levels of testing in this document. + +- `Unit Testing`_ + +- `Functional Testing`_ + +Other testing, like performance or security, is possible. +But for this application, not very interesting. + +This doument builds a complete test suite, ``test.py``. + +.. parsed-literal:: + + MacBookPro-SLott:test slott$ python3.3 ../pyweb.py pyweb_test.w + INFO:Application:Setting root log level to 'INFO' + INFO:Application:Setting command character to '@@' + INFO:Application:Weaver RST + INFO:Application:load, tangle and weave 'pyweb_test.w' + INFO:LoadAction:Starting Load + INFO:WebReader:Including 'intro.w' + WARNING:WebReader:Unknown @@-command in input: "@@'" + INFO:WebReader:Including 'unit.w' + INFO:WebReader:Including 'func.w' + INFO:WebReader:Including 'combined.w' + INFO:TangleAction:Starting Tangle + INFO:TanglerMake:Tangling 'test_unit.py' + INFO:TanglerMake:No change to 'test_unit.py' + INFO:TanglerMake:Tangling 'test_loader.py' + INFO:TanglerMake:No change to 'test_loader.py' + INFO:TanglerMake:Tangling 'test.py' + INFO:TanglerMake:No change to 'test.py' + INFO:TanglerMake:Tangling 'page-layout.css' + INFO:TanglerMake:No change to 'page-layout.css' + INFO:TanglerMake:Tangling 'docutils.conf' + INFO:TanglerMake:No change to 'docutils.conf' + INFO:TanglerMake:Tangling 'test_tangler.py' + INFO:TanglerMake:No change to 'test_tangler.py' + INFO:TanglerMake:Tangling 'test_weaver.py' + INFO:TanglerMake:No change to 'test_weaver.py' + INFO:WeaveAction:Starting Weave + INFO:RST:Weaving 'pyweb_test.rst' + INFO:RST:Wrote 3173 lines to 'pyweb_test.rst' + INFO:WeaveAction:Finished Normally + INFO:Application:Load 1911 lines from 5 files in 0.05 sec., Tangle 138 lines in 0.03 sec., Weave 3173 lines in 0.02 sec. + MacBookPro-SLott:test slott$ PYTHONPATH=.. python3.3 test.py + ERROR:WebReader:At ('test8_inc.tmp', 4): end of input, ('@@{', '@@[') not found + ERROR:WebReader:Errors in included file test8_inc.tmp, output is incomplete. + .ERROR:WebReader:At ('test1.w', 8): expected ('@@{',), found '@@o' + ERROR:WebReader:Extra '@@{' (possibly missing chunk name) near ('test1.w', 9) + ERROR:WebReader:Extra '@@{' (possibly missing chunk name) near ('test1.w', 9) + ............................................................................. + ---------------------------------------------------------------------- + Ran 78 tests in 0.025s + + OK + MacBookPro-SLott:test slott$ rst2html.py pyweb_test.rst pyweb_test.html diff --git a/tests/page-layout.css b/tests/page-layout.css new file mode 100644 index 0000000..e11a707 --- /dev/null +++ b/tests/page-layout.css @@ -0,0 +1,17 @@ +/* Page layout tweaks */ +div.document { width: 7in; } +.small { font-size: smaller; } +.code +{ + color: #101080; + display: block; + border-color: black; + border-width: thin; + border-style: solid; + background-color: #E0FFFF; + /*#99FFFF*/ + padding: 0 0 0 1%; + margin: 0 6% 0 6%; + text-align: left; + font-size: smaller; +} diff --git a/test/pyweb.css b/tests/pyweb.css similarity index 100% rename from test/pyweb.css rename to tests/pyweb.css diff --git a/tests/pyweb_test.html b/tests/pyweb_test.html new file mode 100644 index 0000000..3665dd7 --- /dev/null +++ b/tests/pyweb_test.html @@ -0,0 +1,3219 @@ + + + + + + +pyWeb Literate Programming 3.1 - Test Suite + + + + +
    +

    pyWeb Literate Programming 3.1 - Test Suite

    +

    Yet Another Literate Programming Tool

    + + + + + + +
    +

    Introduction

    + +

    There are two levels of testing in this document.

    + +

    Other testing, like performance or security, is possible. +But for this application, not very interesting.

    +

    This doument builds a complete test suite, test.py.

    +
    +MacBookPro-SLott:test slott$ python3.3 ../pyweb.py pyweb_test.w
    +INFO:Application:Setting root log level to 'INFO'
    +INFO:Application:Setting command character to '@'
    +INFO:Application:Weaver RST
    +INFO:Application:load, tangle and weave 'pyweb_test.w'
    +INFO:LoadAction:Starting Load
    +INFO:WebReader:Including 'intro.w'
    +WARNING:WebReader:Unknown @-command in input: "@'"
    +INFO:WebReader:Including 'unit.w'
    +INFO:WebReader:Including 'func.w'
    +INFO:WebReader:Including 'combined.w'
    +INFO:TangleAction:Starting Tangle
    +INFO:TanglerMake:Tangling 'test_unit.py'
    +INFO:TanglerMake:No change to 'test_unit.py'
    +INFO:TanglerMake:Tangling 'test_loader.py'
    +INFO:TanglerMake:No change to 'test_loader.py'
    +INFO:TanglerMake:Tangling 'test.py'
    +INFO:TanglerMake:No change to 'test.py'
    +INFO:TanglerMake:Tangling 'page-layout.css'
    +INFO:TanglerMake:No change to 'page-layout.css'
    +INFO:TanglerMake:Tangling 'docutils.conf'
    +INFO:TanglerMake:No change to 'docutils.conf'
    +INFO:TanglerMake:Tangling 'test_tangler.py'
    +INFO:TanglerMake:No change to 'test_tangler.py'
    +INFO:TanglerMake:Tangling 'test_weaver.py'
    +INFO:TanglerMake:No change to 'test_weaver.py'
    +INFO:WeaveAction:Starting Weave
    +INFO:RST:Weaving 'pyweb_test.rst'
    +INFO:RST:Wrote 3173 lines to 'pyweb_test.rst'
    +INFO:WeaveAction:Finished Normally
    +INFO:Application:Load 1911 lines from 5 files in 0.05 sec., Tangle 138 lines in 0.03 sec., Weave 3173 lines in 0.02 sec.
    +MacBookPro-SLott:test slott$ PYTHONPATH=.. python3.3 test.py
    +ERROR:WebReader:At ('test8_inc.tmp', 4): end of input, ('@{', '@[') not found
    +ERROR:WebReader:Errors in included file test8_inc.tmp, output is incomplete.
    +.ERROR:WebReader:At ('test1.w', 8): expected ('@{',), found '@o'
    +ERROR:WebReader:Extra '@{' (possibly missing chunk name) near ('test1.w', 9)
    +ERROR:WebReader:Extra '@{' (possibly missing chunk name) near ('test1.w', 9)
    +.............................................................................
    +----------------------------------------------------------------------
    +Ran 78 tests in 0.025s
    +
    +OK
    +MacBookPro-SLott:test slott$ rst2html.py pyweb_test.rst pyweb_test.html
    +
    +
    +
    +

    Unit Testing

    + +

    There are several broad areas of unit testing. There are the 34 classes in this application. +However, it isn't really necessary to test everyone single one of these classes. +We'll decompose these into several hierarchies.

    +
      +
    • Emitters

      +
      +

      class Emitter:

      +

      class Weaver(Emitter):

      +

      class LaTeX(Weaver):

      +

      class HTML(Weaver):

      +

      class HTMLShort(HTML):

      +

      class Tangler(Emitter):

      +

      class TanglerMake(Tangler):

      +
      +
    • +
    • Structure: Chunk, Command

      +
      +

      class Chunk:

      +

      class NamedChunk(Chunk):

      +

      class NamedChunk_Noindent(Chunk):

      +

      class OutputChunk(NamedChunk):

      +

      class NamedDocumentChunk(NamedChunk):

      +

      class Command:

      +

      class TextCommand(Command):

      +

      class CodeCommand(TextCommand):

      +

      class XrefCommand(Command):

      +

      class FileXrefCommand(XrefCommand):

      +

      class MacroXrefCommand(XrefCommand):

      +

      class UserIdXrefCommand(XrefCommand):

      +

      class ReferenceCommand(Command):

      +
      +
    • +
    • class Error(Exception):

      +
    • +
    • Reference Handling

      +
      +

      class Reference:

      +

      class SimpleReference(Reference):

      +

      class TransitiveReference(Reference):

      +
      +
    • +
    • class Web:

      +
    • +
    • class WebReader:

      +
      +

      class Tokenizer:

      +

      class OptionParser:

      +
      +
    • +
    • Action

      +
      +

      class Action:

      +

      class ActionSequence(Action):

      +

      class WeaveAction(Action):

      +

      class TangleAction(Action):

      +

      class LoadAction(Action):

      +
      +
    • +
    • class Application:

      +
    • +
    • class MyWeaver(HTML):

      +
    • +
    • class MyHTML(pyweb.HTML):

      +
    • +
    +

    This gives us the following outline for unit testing.

    +

    test_unit.py (1) =

    +
    +→Unit Test overheads: imports, etc. (48), →(49)
    +→Unit Test of Emitter class hierarchy (2)
    +→Unit Test of Chunk class hierarchy (11)
    +→Unit Test of Command class hierarchy (23)
    +→Unit Test of Reference class hierarchy (32)
    +→Unit Test of Web class (33)
    +→Unit Test of WebReader class (39), →(40), →(41)
    +→Unit Test of Action class hierarchy (42)
    +→Unit Test of Application class (47)
    +→Unit Test main (50)
    +
    + +
    +

    test_unit.py (1).

    +
    +
    +

    Emitter Tests

    +

    The emitter class hierarchy produces output files; either woven output +which uses templates to generate proper markup, or tangled output which +precisely follows the document structure.

    +

    Unit Test of Emitter class hierarchy (2) =

    +
    +→Unit Test Mock Chunk class (4)
    +→Unit Test of Emitter Superclass (3)
    +→Unit Test of Weaver subclass of Emitter (5)
    +→Unit Test of LaTeX subclass of Emitter (6)
    +→Unit Test of HTML subclass of Emitter (7)
    +→Unit Test of HTMLShort subclass of Emitter (8)
    +→Unit Test of Tangler subclass of Emitter (9)
    +→Unit Test of TanglerMake subclass of Emitter (10)
    +
    + +
    +

    Unit Test of Emitter class hierarchy (2). Used by: test_unit.py (1)

    +
    +

    The Emitter superclass is designed to be extended. The test +creates a subclass to exercise a few key features. The default +emitter is Tangler-like.

    +

    Unit Test of Emitter Superclass (3) =

    +
    +class EmitterExtension(pyweb.Emitter):
    +    def doOpen(self) -> None:
    +        self.theFile = io.StringIO()
    +    def doClose(self) -> None:
    +        self.theFile.flush()
    +
    +class TestEmitter(unittest.TestCase):
    +    def setUp(self) -> None:
    +        self.emitter = EmitterExtension()
    +    def test_emitter_should_open_close_write(self) -> None:
    +        self.emitter.open(Path("test.tmp"))
    +        self.emitter.write("Something")
    +        self.emitter.close()
    +        self.assertEqual("Something", self.emitter.theFile.getvalue())
    +    def test_emitter_should_codeBlock(self) -> None:
    +        self.emitter.open(Path("test.tmp"))
    +        self.emitter.codeBlock("Some")
    +        self.emitter.codeBlock(" Code")
    +        self.emitter.close()
    +        self.assertEqual("Some Code\n", self.emitter.theFile.getvalue())
    +    def test_emitter_should_indent(self) -> None:
    +        self.emitter.open(Path("test.tmp"))
    +        self.emitter.codeBlock("Begin\n")
    +        self.emitter.addIndent(4)
    +        self.emitter.codeBlock("More Code\n")
    +        self.emitter.clrIndent()
    +        self.emitter.codeBlock("End")
    +        self.emitter.close()
    +        self.assertEqual("Begin\n    More Code\nEnd\n", self.emitter.theFile.getvalue())
    +    def test_emitter_should_noindent(self) -> None:
    +        self.emitter.open(Path("test.tmp"))
    +        self.emitter.codeBlock("Begin\n")
    +        self.emitter.setIndent(0)
    +        self.emitter.codeBlock("More Code\n")
    +        self.emitter.clrIndent()
    +        self.emitter.codeBlock("End")
    +        self.emitter.close()
    +        self.assertEqual("Begin\nMore Code\nEnd\n", self.emitter.theFile.getvalue())
    +
    + +
    +

    Unit Test of Emitter Superclass (3). Used by: Unit Test of Emitter class hierarchy... (2)

    +
    +

    A mock Chunk is a Chunk-like object that we can use to test Weavers.

    +

    Some tests will create multiple chunks. To keep their state separate, +we define a function to return each mocked Chunk instance as a new Mock +object. The overall MockChunk class, uses a side effect to +invoke the the mock_chunk_instance() function.

    +

    The write_closure() is a function that calls the Tangler.write() +method. This is not consistent with best unit testing practices. +It is merely a hold-over from an older testing strategy. The mock call +history to the tangle() method of each Chunk instance is a better +test strategy.

    +

    Unit Test Mock Chunk class (4) =

    +
    +def mock_chunk_instance(name: str, seq: int, lineNumber: int) -> Mock:
    +    def write_closure(aWeb: pyweb.Web, aTangler: pyweb.Tangler) -> None:
    +        aTangler.write(name)
    +
    +    chunk = Mock(
    +        wraps=pyweb.Chunk,
    +        fullName=name,
    +        seq=seq,
    +        lineNumber=lineNumber,
    +        initial=True,
    +        commands=[],
    +        referencedBy=[],
    +        references=Mock(return_value=[]),
    +        reference_indent=Mock(),
    +        reference_dedent=Mock(),
    +        tangle=Mock(side_effect=write_closure)
    +    )
    +    chunk.name=name
    +    return chunk
    +
    +MockChunk = Mock(
    +    name="Chunk class",
    +    side_effect=mock_chunk_instance
    +)
    +
    + +
    +

    Unit Test Mock Chunk class (4). Used by: Unit Test of Emitter class hierarchy... (2)

    +
    +

    The default Weaver is an Emitter that uses templates to produce RST markup.

    +

    Unit Test of Weaver subclass of Emitter (5) =

    +
    +class TestWeaver(unittest.TestCase):
    +    def setUp(self) -> None:
    +        self.weaver = pyweb.Weaver()
    +        self.weaver.reference_style = pyweb.SimpleReference()
    +        self.filepath = Path("testweaver")
    +        self.aFileChunk = MockChunk("File", 123, 456)
    +        self.aFileChunk.referencedBy = []
    +        self.aChunk = MockChunk("Chunk", 314, 278)
    +        self.aChunk.referencedBy = [self.aFileChunk]
    +        self.aChunk.references.return_value=[(self.aFileChunk.name, self.aFileChunk.seq)]
    +
    +    def tearDown(self) -> None:
    +        try:
    +            self.filepath.with_suffix('.rst').unlink()
    +        except OSError:
    +            pass
    +
    +    def test_weaver_functions_generic(self) -> None:
    +        result = self.weaver.quote("|char| `code` *em* _em_")
    +        self.assertEqual(r"\|char\| \`code\` \*em\* \_em\_", result)
    +        result = self.weaver.references(self.aChunk)
    +        self.assertEqual("File (`123`_)", result)
    +        result = self.weaver.referenceTo("Chunk", 314)
    +        self.assertEqual(r"|srarr|\ Chunk (`314`_)", result)
    +        self.assertEqual(self.aFileChunk.mock_calls, [])
    +        self.assertEqual(self.aChunk.mock_calls, [call.references(self.weaver)])
    +
    +    def test_weaver_should_codeBegin(self) -> None:
    +        self.weaver.open(self.filepath)
    +        self.weaver.addIndent()
    +        self.weaver.codeBegin(self.aChunk)
    +        self.weaver.codeBlock(self.weaver.quote("*The* `Code`\n"))
    +        self.weaver.clrIndent()
    +        self.weaver.codeEnd(self.aChunk)
    +        self.weaver.close()
    +        txt = self.filepath.with_suffix(".rst").read_text()
    +        self.assertEqual("\n..  _`314`:\n..  rubric:: Chunk (314) =\n..  parsed-literal::\n    :class: code\n\n    \\*The\\* \\`Code\\`\n\n..\n\n    ..  class:: small\n\n        |loz| *Chunk (314)*. Used by: File (`123`_)\n", txt)
    +
    +    def test_weaver_should_fileBegin(self) -> None:
    +        self.weaver.open(self.filepath)
    +        self.weaver.fileBegin(self.aFileChunk)
    +        self.weaver.codeBlock(self.weaver.quote("*The* `Code`\n"))
    +        self.weaver.fileEnd(self.aFileChunk)
    +        self.weaver.close()
    +        txt = self.filepath.with_suffix(".rst").read_text()
    +        self.assertEqual("\n..  _`123`:\n..  rubric:: File (123) =\n..  parsed-literal::\n    :class: code\n\n    \\*The\\* \\`Code\\`\n\n..\n\n    ..  class:: small\n\n        |loz| *File (123)*.\n", txt)
    +
    +    def test_weaver_should_xref(self) -> None:
    +        self.weaver.open(self.filepath)
    +        self.weaver.xrefHead( )
    +        self.weaver.xrefLine("Chunk", [ ("Container", 123) ])
    +        self.weaver.xrefFoot( )
    +        #self.weaver.fileEnd(self.aFileChunk) # Why?
    +        self.weaver.close()
    +        txt = self.filepath.with_suffix(".rst").read_text()
    +        self.assertEqual("\n:Chunk:\n    |srarr|\\ (`('Container', 123)`_)\n\n", txt)
    +
    +    def test_weaver_should_xref_def(self) -> None:
    +        self.weaver.open(self.filepath)
    +        self.weaver.xrefHead( )
    +        # Seems to have changed to a simple list of lines??
    +        self.weaver.xrefDefLine("Chunk", 314, [ 123, 567 ])
    +        self.weaver.xrefFoot( )
    +        #self.weaver.fileEnd(self.aFileChunk) # Why?
    +        self.weaver.close()
    +        txt = self.filepath.with_suffix(".rst").read_text()
    +        self.assertEqual("\n:Chunk:\n    `123`_ [`314`_] `567`_\n\n", txt)
    +
    + +
    +

    Unit Test of Weaver subclass of Emitter (5). Used by: Unit Test of Emitter class hierarchy... (2)

    +
    +

    A significant fraction of the various subclasses of weaver are simply +expansion of templates. There's no real point in testing the template +expansion, since that's more easily tested by running a document +through pyweb and looking at the results.

    +

    We'll examine a few features of the LaTeX templates.

    +

    Unit Test of LaTeX subclass of Emitter (6) =

    +
    +class TestLaTeX(unittest.TestCase):
    +    def setUp(self) -> None:
    +        self.weaver = pyweb.LaTeX()
    +        self.weaver.reference_style = pyweb.SimpleReference()
    +        self.filepath = Path("testweaver")
    +        self.aFileChunk = MockChunk("File", 123, 456)
    +        self.aFileChunk.referencedBy = [ ]
    +        self.aChunk = MockChunk("Chunk", 314, 278)
    +        self.aChunk.referencedBy = [self.aFileChunk,]
    +        self.aChunk.references.return_value=[(self.aFileChunk.name, self.aFileChunk.seq)]
    +
    +    def tearDown(self) -> None:
    +        try:
    +            self.filepath.with_suffix(".tex").unlink()
    +        except OSError:
    +            pass
    +
    +    def test_weaver_functions_latex(self) -> None:
    +        result = self.weaver.quote("\\end{Verbatim}")
    +        self.assertEqual("\\end\\,{Verbatim}", result)
    +        result = self.weaver.references(self.aChunk)
    +        expected = textwrap.indent(
    +            textwrap.dedent("""
    +                \\footnotesize
    +                Used by:
    +                \\begin{list}{}{}
    +
    +                \\item Code example File (123) (Sect. \\ref{pyweb123}, p. \\pageref{pyweb123})
    +
    +                \\end{list}
    +                \\normalsize
    +            """),
    +        '    ')
    +        self.assertEqual(rstrip_lines(expected), rstrip_lines(result))
    +        result = self.weaver.referenceTo("Chunk", 314)
    +        self.assertEqual("$\\triangleright$ Code Example Chunk (314)", result)
    +        self.assertEqual(self.aFileChunk.mock_calls, [])
    +        self.assertEqual(self.aChunk.mock_calls, [call.references(self.weaver)])
    +
    + +
    +

    Unit Test of LaTeX subclass of Emitter (6). Used by: Unit Test of Emitter class hierarchy... (2)

    +
    +

    We'll examine a few features of the HTML templates.

    +

    Unit Test of HTML subclass of Emitter (7) =

    +
    +class TestHTML(unittest.TestCase):
    +    def setUp(self) -> None:
    +        self.weaver = pyweb.HTML( )
    +        self.weaver.reference_style = pyweb.SimpleReference()
    +        self.filepath = Path("testweaver")
    +        self.aFileChunk = MockChunk("File", 123, 456)
    +        self.aFileChunk.referencedBy = []
    +        self.aChunk = MockChunk("Chunk", 314, 278)
    +        self.aChunk.referencedBy = [self.aFileChunk,]
    +        self.aChunk.references.return_value=[(self.aFileChunk.name, self.aFileChunk.seq)]
    +
    +    def tearDown(self) -> None:
    +        try:
    +            self.filepath.with_suffix(".html").unlink()
    +        except OSError:
    +            pass
    +
    +    def test_weaver_functions_html(self) -> None:
    +        result = self.weaver.quote("a < b && c > d")
    +        self.assertEqual("a &lt; b &amp;&amp; c &gt; d", result)
    +        result = self.weaver.references(self.aChunk)
    +        self.assertEqual('  Used by <a href="#pyweb123"><em>File</em>&nbsp;(123)</a>.', result)
    +        result = self.weaver.referenceTo("Chunk", 314)
    +        self.assertEqual('<a href="#pyweb314">&rarr;<em>Chunk</em> (314)</a>', result)
    +        self.assertEqual(self.aFileChunk.mock_calls, [])
    +        self.assertEqual(self.aChunk.mock_calls, [call.references(self.weaver)])
    +
    + +
    +

    Unit Test of HTML subclass of Emitter (7). Used by: Unit Test of Emitter class hierarchy... (2)

    +
    +

    The unique feature of the HTMLShort class is a template change.

    +
    +TODO: Test HTMLShort.
    +

    Unit Test of HTMLShort subclass of Emitter (8) =

    +
    +# TODO: Finish this
    +
    + +
    +

    Unit Test of HTMLShort subclass of Emitter (8). Used by: Unit Test of Emitter class hierarchy... (2)

    +
    +

    A Tangler emits the various named source files in proper format for the desired +compiler and language.

    +

    Unit Test of Tangler subclass of Emitter (9) =

    +
    +class TestTangler(unittest.TestCase):
    +    def setUp(self) -> None:
    +        self.tangler = pyweb.Tangler()
    +        self.filepath = Path("testtangler.code")
    +        self.aFileChunk = MockChunk("File", 123, 456)
    +        #self.aFileChunk.references_list = [ ]
    +        self.aChunk = MockChunk("Chunk", 314, 278)
    +        #self.aChunk.references_list = [ ("Container", 123) ]
    +    def tearDown(self) -> None:
    +        try:
    +            self.filepath.unlink()
    +        except FileNotFoundError:
    +            pass
    +
    +    def test_tangler_functions(self) -> None:
    +        result = self.tangler.quote(string.printable)
    +        self.assertEqual(string.printable, result)
    +
    +    def test_tangler_should_codeBegin(self) -> None:
    +        self.tangler.open(self.filepath)
    +        self.tangler.codeBegin(self.aChunk)
    +        self.tangler.codeBlock(self.tangler.quote("*The* `Code`\n"))
    +        self.tangler.codeEnd(self.aChunk)
    +        self.tangler.close()
    +        txt = self.filepath.read_text()
    +        self.assertEqual("*The* `Code`\n", txt)
    +
    + +
    +

    Unit Test of Tangler subclass of Emitter (9). Used by: Unit Test of Emitter class hierarchy... (2)

    +
    +

    A TanglerMake uses a cheap hack to see if anything changed. +It creates a temporary file and then does a complete (slow, expensive) file difference +check. If the file is different, the old version is replaced with +the new version. If the file content is the same, the old version +is left intact with all of the operating system creation timestamps +untouched.

    +

    Unit Test of TanglerMake subclass of Emitter (10) =

    +
    +class TestTanglerMake(unittest.TestCase):
    +    def setUp(self) -> None:
    +        self.tangler = pyweb.TanglerMake()
    +        self.filepath = Path("testtangler.code")
    +        self.aChunk = MockChunk("Chunk", 314, 278)
    +        #self.aChunk.references_list = [("Container", 123)]
    +        self.tangler.open(self.filepath)
    +        self.tangler.codeBegin(self.aChunk)
    +        self.tangler.codeBlock(self.tangler.quote("*The* `Code`\n"))
    +        self.tangler.codeEnd(self.aChunk)
    +        self.tangler.close()
    +        self.time_original = self.filepath.stat().st_mtime
    +        self.original = self.filepath.stat()
    +
    +    def tearDown(self) -> None:
    +        try:
    +            self.filepath.unlink()
    +        except OSError:
    +            pass
    +
    +    def test_same_should_leave(self) -> None:
    +        self.tangler.open(self.filepath)
    +        self.tangler.codeBegin(self.aChunk)
    +        self.tangler.codeBlock(self.tangler.quote("*The* `Code`\n"))
    +        self.tangler.codeEnd(self.aChunk)
    +        self.tangler.close()
    +        self.assertTrue(os.path.samestat(self.original, self.filepath.stat()))
    +        #self.assertEqual(self.time_original, self.filepath.stat().st_mtime)
    +
    +    def test_different_should_update(self) -> None:
    +        self.tangler.open(self.filepath)
    +        self.tangler.codeBegin(self.aChunk)
    +        self.tangler.codeBlock(self.tangler.quote("*Completely Different* `Code`\n"))
    +        self.tangler.codeEnd(self.aChunk)
    +        self.tangler.close()
    +        self.assertFalse(os.path.samestat(self.original, self.filepath.stat()))
    +        #self.assertNotEqual(self.time_original, self.filepath.stat().st_mtime)
    +
    + +
    +

    Unit Test of TanglerMake subclass of Emitter (10). Used by: Unit Test of Emitter class hierarchy... (2)

    +
    +
    +
    +

    Chunk Tests

    +

    The Chunk and Command class hierarchies model the input document -- the web +of chunks that are used to produce the documentation and the source files.

    +

    Unit Test of Chunk class hierarchy (11) =

    +
    +→Unit Test of Chunk superclass (12), →(13), →(14), →(15)
    +→Unit Test of NamedChunk subclass (19)
    +→Unit Test of NamedChunk_Noindent subclass (20)
    +→Unit Test of OutputChunk subclass (21)
    +→Unit Test of NamedDocumentChunk subclass (22)
    +
    + +
    +

    Unit Test of Chunk class hierarchy (11). Used by: test_unit.py (1)

    +
    +

    In order to test the Chunk superclass, we need several mock objects. +A Chunk contains one or more commands. A Chunk is a part of a Web. +Also, a Chunk is processed by a Tangler or a Weaver. We'll need +mock objects for all of these relationships in which a Chunk participates.

    +

    A MockCommand can be attached to a Chunk.

    +

    Unit Test of Chunk superclass (12) =

    +
    +MockCommand = Mock(
    +    name="Command class",
    +    side_effect=lambda: Mock(
    +        name="Command instance",
    +        # text="",  # Only used for TextCommand.
    +        lineNumber=314,
    +        startswith=Mock(return_value=False)
    +    )
    +)
    +
    + +
    +

    Unit Test of Chunk superclass (12). Used by: Unit Test of Chunk class hierarchy... (11)

    +
    +

    A MockWeb can contain a Chunk.

    +

    Unit Test of Chunk superclass (13) +=

    +
    +def mock_web_instance() -> Mock:
    +    web = Mock(
    +        name="Web instance",
    +        chunks=[],
    +        add=Mock(return_value=None),
    +        addNamed=Mock(return_value=None),
    +        addOutput=Mock(return_value=None),
    +        fullNameFor=Mock(side_effect=lambda name: name),
    +        fileXref=Mock(return_value={'file': [1,2,3]}),
    +        chunkXref=Mock(return_value={'chunk': [4,5,6]}),
    +        userNamesXref=Mock(return_value={'name': (7, [8,9,10])}),
    +        getchunk=Mock(side_effect=lambda name: [MockChunk(name, 1, 314)]),
    +        createUsedBy=Mock(),
    +        weaveChunk=Mock(side_effect=lambda name, weaver: weaver.write(name)),
    +        weave=Mock(return_value=None),
    +        tangle=Mock(return_value=None),
    +    )
    +    return web
    +
    +MockWeb = Mock(
    +    name="Web class",
    +    side_effect=mock_web_instance
    +)
    +
    + +
    +

    Unit Test of Chunk superclass (13). Used by: Unit Test of Chunk class hierarchy... (11)

    +
    +

    A MockWeaver or MockTangler appear to process a Chunk. +We can interrogate the mock_calls to be sure the right things were done.

    +

    We need to permit __enter__() and __exit__(), +which leads to a multi-step instance. +The initial instance with __enter__() that +returns the context manager instance.

    +

    Unit Test of Chunk superclass (14) +=

    +
    +def mock_weaver_instance() -> MagicMock:
    +    context = MagicMock(
    +        name="Weaver instance context",
    +        __exit__=Mock()
    +    )
    +
    +    weaver = MagicMock(
    +        name="Weaver instance",
    +        quote=Mock(return_value="quoted"),
    +        __enter__=Mock(return_value=context)
    +    )
    +    return weaver
    +
    +MockWeaver = Mock(
    +    name="Weaver class",
    +    side_effect=mock_weaver_instance
    +)
    +
    +def mock_tangler_instance() -> MagicMock:
    +    context = MagicMock(
    +        name="Tangler instance context",
    +        __exit__=Mock()
    +    )
    +
    +    tangler = MagicMock(
    +        name="Tangler instance",
    +        __enter__=Mock(return_value=context)
    +    )
    +    return tangler
    +
    +MockTangler = Mock(
    +    name="Tangler class",
    +    side_effect=mock_tangler_instance
    +)
    +
    + +
    +

    Unit Test of Chunk superclass (14). Used by: Unit Test of Chunk class hierarchy... (11)

    +
    +

    A Chunk is built, interrogated and then emitted.

    +

    Unit Test of Chunk superclass (15) +=

    +
    +class TestChunk(unittest.TestCase):
    +    def setUp(self) -> None:
    +        self.theChunk = pyweb.Chunk()
    +
    +    →Unit Test of Chunk construction (16)
    +
    +    →Unit Test of Chunk interrogation (17)
    +
    +    →Unit Test of Chunk emission (18)
    +
    + +
    +

    Unit Test of Chunk superclass (15). Used by: Unit Test of Chunk class hierarchy... (11)

    +
    +

    Can we build a Chunk?

    +

    Unit Test of Chunk construction (16) =

    +
    +def test_append_command_should_work(self) -> None:
    +    cmd1 = MockCommand()
    +    self.theChunk.append(cmd1)
    +    self.assertEqual(1, len(self.theChunk.commands))
    +    self.assertEqual(cmd1.chunk, self.theChunk)
    +
    +    cmd2 = MockCommand()
    +    self.theChunk.append(cmd2)
    +    self.assertEqual(2, len(self.theChunk.commands))
    +    self.assertEqual(cmd2.chunk, self.theChunk)
    +
    +def test_append_initial_and_more_text_should_work(self) -> None:
    +    self.theChunk.appendText("hi mom")
    +    self.assertEqual(1, len(self.theChunk.commands))
    +    self.theChunk.appendText("&more text")
    +    self.assertEqual(1, len(self.theChunk.commands))
    +    self.assertEqual("hi mom&more text", self.theChunk.commands[0].text)
    +
    +def test_append_following_text_should_work(self) -> None:
    +    cmd1 = MockCommand()
    +    self.theChunk.append(cmd1)
    +    self.theChunk.appendText("hi mom")
    +    self.assertEqual(2, len(self.theChunk.commands))
    +    assert cmd1.chunk == self.theChunk
    +
    +def test_append_chunk_to_web_should_work(self) -> None:
    +    web = MockWeb()
    +    self.theChunk.webAdd(web)
    +    self.assertEqual(web.add.mock_calls, [call(self.theChunk)])
    +
    + +
    +

    Unit Test of Chunk construction (16). Used by: Unit Test of Chunk superclass... (15)

    +
    +

    Can we interrogate a Chunk?

    +

    Unit Test of Chunk interrogation (17) =

    +
    +def test_leading_command_should_not_find(self) -> None:
    +    self.assertFalse(self.theChunk.startswith("hi mom"))
    +    cmd1 = MockCommand()
    +    self.theChunk.append(cmd1)
    +    self.assertFalse(self.theChunk.startswith("hi mom"))
    +    self.theChunk.appendText("hi mom")
    +    self.assertEqual(2, len(self.theChunk.commands) )
    +    self.assertFalse(self.theChunk.startswith("hi mom"))
    +
    +def test_leading_text_should_not_find(self) -> None:
    +    self.assertFalse(self.theChunk.startswith("hi mom"))
    +    self.theChunk.appendText("hi mom")
    +    self.assertTrue(self.theChunk.startswith("hi mom"))
    +    cmd1 = MockCommand()
    +    self.theChunk.append(cmd1)
    +    self.assertTrue(self.theChunk.startswith("hi mom"))
    +    self.assertEqual(2, len(self.theChunk.commands) )
    +
    +def test_regexp_exists_should_find(self) -> None:
    +    self.theChunk.appendText("this chunk has many words")
    +    pat = re.compile(r"\Wchunk\W")
    +    found = self.theChunk.searchForRE(pat)
    +    self.assertTrue(found is self.theChunk)
    +
    +def test_regexp_missing_should_not_find(self):
    +    self.theChunk.appendText("this chunk has many words")
    +    pat = re.compile(r"\Warpigs\W")
    +    found = self.theChunk.searchForRE(pat)
    +    self.assertTrue(found is None)
    +
    +def test_lineNumber_should_work(self) -> None:
    +    self.assertTrue(self.theChunk.lineNumber is None)
    +    cmd1 = MockCommand()
    +    self.theChunk.append(cmd1)
    +    self.assertEqual(314, self.theChunk.lineNumber)
    +
    + +
    +

    Unit Test of Chunk interrogation (17). Used by: Unit Test of Chunk superclass... (15)

    +
    +

    Can we emit a Chunk with a weaver or tangler?

    +

    Unit Test of Chunk emission (18) =

    +
    +def test_weave_chunk_should_work(self) -> None:
    +    wvr = MockWeaver()
    +    web = MockWeb()
    +    self.theChunk.appendText("this chunk has very & many words")
    +    self.theChunk.weave(web, wvr)
    +    self.assertEqual(wvr.docBegin.mock_calls, [call(self.theChunk)])
    +    self.assertEqual(wvr.write.mock_calls, [call("this chunk has very & many words")])
    +    self.assertEqual(wvr.docEnd.mock_calls, [call(self.theChunk)])
    +
    +def test_tangle_should_fail(self) -> None:
    +    tnglr = MockTangler()
    +    web = MockWeb()
    +    self.theChunk.appendText("this chunk has very & many words")
    +    try:
    +        self.theChunk.tangle(web, tnglr)
    +        self.fail()
    +    except pyweb.Error as e:
    +        self.assertEqual("Cannot tangle an anonymous chunk", e.args[0])
    +
    + +
    +

    Unit Test of Chunk emission (18). Used by: Unit Test of Chunk superclass... (15)

    +
    +

    The NamedChunk is created by a @d command. +Since it's named, it appears in the Web's index. Also, it is woven +and tangled differently than anonymous chunks.

    +

    Unit Test of NamedChunk subclass (19) =

    +
    +class TestNamedChunk(unittest.TestCase):
    +    def setUp(self) -> None:
    +        self.theChunk = pyweb.NamedChunk("Some Name...")
    +        cmd = self.theChunk.makeContent("the words & text of this Chunk")
    +        self.theChunk.append(cmd)
    +        self.theChunk.setUserIDRefs("index terms")
    +
    +    def test_should_find_xref_words(self) -> None:
    +        self.assertEqual(2, len(self.theChunk.getUserIDRefs()))
    +        self.assertEqual("index", self.theChunk.getUserIDRefs()[0])
    +        self.assertEqual("terms", self.theChunk.getUserIDRefs()[1])
    +
    +    def test_append_named_chunk_to_web_should_work(self) -> None:
    +        web = MockWeb()
    +        self.theChunk.webAdd(web)
    +        self.assertEqual(web.addNamed.mock_calls, [call(self.theChunk)])
    +
    +    def test_weave_should_work(self) -> None:
    +        wvr = MockWeaver()
    +        web = MockWeb()
    +        self.theChunk.weave(web, wvr)
    +        self.assertEqual(wvr.codeBegin.mock_calls, [call(self.theChunk)])
    +        self.assertEqual(wvr.quote.mock_calls, [call('the words & text of this Chunk')])
    +        self.assertEqual(wvr.codeBlock.mock_calls, [call('quoted')])
    +        self.assertEqual(wvr.codeEnd.mock_calls, [call(self.theChunk)])
    +
    +    def test_tangle_should_work(self) -> None:
    +        tnglr = MockTangler()
    +        web = MockWeb()
    +        self.theChunk.tangle(web, tnglr)
    +        self.assertEqual(tnglr.codeBegin.mock_calls, [call(self.theChunk)])
    +        self.assertEqual(tnglr.codeBlock.mock_calls, [call("the words & text of this Chunk")])
    +        self.assertEqual(tnglr.codeEnd.mock_calls, [call(self.theChunk)])
    +
    + +
    +

    Unit Test of NamedChunk subclass (19). Used by: Unit Test of Chunk class hierarchy... (11)

    +
    +

    Unit Test of NamedChunk_Noindent subclass (20) =

    +
    +class TestNamedChunk_Noindent(unittest.TestCase):
    +    def setUp(self) -> None:
    +        self.theChunk = pyweb.NamedChunk_Noindent("NoIndent Name...")
    +        cmd = self.theChunk.makeContent("the words & text of this Chunk")
    +        self.theChunk.append(cmd)
    +        self.theChunk.setUserIDRefs("index terms")
    +    def test_tangle_should_work(self) -> None:
    +        tnglr = MockTangler()
    +        web = MockWeb()
    +        self.theChunk.tangle(web, tnglr)
    +
    +        self.assertEqual(tnglr.mock_calls, [
    +                call.codeBegin(self.theChunk),
    +                call.codeBlock('the words & text of this Chunk'),
    +                call.codeEnd(self.theChunk)
    +            ]
    +        )
    +
    + +
    +

    Unit Test of NamedChunk_Noindent subclass (20). Used by: Unit Test of Chunk class hierarchy... (11)

    +
    +

    The OutputChunk is created by a @o command. +Since it's named, it appears in the Web's index. Also, it is woven +and tangled differently than anonymous chunks.

    +

    Unit Test of OutputChunk subclass (21) =

    +
    +class TestOutputChunk(unittest.TestCase):
    +    def setUp(self) -> None:
    +        self.theChunk = pyweb.OutputChunk("filename", "#", "")
    +        cmd = self.theChunk.makeContent("the words & text of this Chunk")
    +        self.theChunk.append(cmd)
    +        self.theChunk.setUserIDRefs("index terms")
    +
    +    def test_append_output_chunk_to_web_should_work(self) -> None:
    +        web = MockWeb()
    +        self.theChunk.webAdd(web)
    +        self.assertEqual(web.addOutput.mock_calls, [call(self.theChunk)])
    +
    +    def test_weave_should_work(self) -> None:
    +        wvr = MockWeaver()
    +        web = MockWeb()
    +        self.theChunk.weave(web, wvr)
    +        self.assertEqual(wvr.mock_calls, [
    +                call.fileBegin(self.theChunk),
    +                call.quote('the words & text of this Chunk'),
    +                call.codeBlock('quoted'),
    +                call.fileEnd(self.theChunk)
    +            ]
    +        )
    +
    +    def test_tangle_should_work(self) -> None:
    +        tnglr = MockTangler()
    +        web = MockWeb()
    +        self.theChunk.tangle(web, tnglr)
    +        self.assertEqual(tnglr.mock_calls, [
    +                call.codeBegin(self.theChunk),
    +                call.codeBlock('the words & text of this Chunk'),
    +                call.codeEnd(self.theChunk)
    +            ]
    +        )
    +
    + +
    +

    Unit Test of OutputChunk subclass (21). Used by: Unit Test of Chunk class hierarchy... (11)

    +
    +

    The NamedDocumentChunk is a little-used feature.

    +
    +TODO Test NamedDocumentChunk.
    +

    Unit Test of NamedDocumentChunk subclass (22) =

    +
    +# TODO Test This
    +
    + +
    +

    Unit Test of NamedDocumentChunk subclass (22). Used by: Unit Test of Chunk class hierarchy... (11)

    +
    +
    +
    +

    Command Tests

    +

    Unit Test of Command class hierarchy (23) =

    +
    +→Unit Test of Command superclass (24)
    +→Unit Test of TextCommand class to contain a document text block (25)
    +→Unit Test of CodeCommand class to contain a program source code block (26)
    +→Unit Test of XrefCommand superclass for all cross-reference commands (27)
    +→Unit Test of FileXrefCommand class for an output file cross-reference (28)
    +→Unit Test of MacroXrefCommand class for a named chunk cross-reference (29)
    +→Unit Test of UserIdXrefCommand class for a user identifier cross-reference (30)
    +→Unit Test of ReferenceCommand class for chunk references (31)
    +
    + +
    +

    Unit Test of Command class hierarchy (23). Used by: test_unit.py (1)

    +
    +

    This Command superclass is essentially an inteface definition, it +has no real testable features.

    +

    Unit Test of Command superclass (24) =

    +
    +# No Tests
    +
    + +
    +

    Unit Test of Command superclass (24). Used by: Unit Test of Command class hierarchy... (23)

    +
    +

    A TextCommand object must be constructed, interrogated and emitted.

    +

    Unit Test of TextCommand class to contain a document text block (25) =

    +
    +class TestTextCommand(unittest.TestCase):
    +    def setUp(self) -> None:
    +        self.cmd = pyweb.TextCommand("Some text & words in the document\n    ", 314)
    +        self.cmd2 = pyweb.TextCommand("No Indent\n", 314)
    +    def test_methods_should_work(self) -> None:
    +        self.assertTrue(self.cmd.startswith("Some"))
    +        self.assertFalse(self.cmd.startswith("text"))
    +        pat1 = re.compile(r"\Wthe\W")
    +        self.assertTrue(self.cmd.searchForRE(pat1) is not None)
    +        pat2 = re.compile(r"\Wnothing\W")
    +        self.assertTrue(self.cmd.searchForRE(pat2) is None)
    +        self.assertEqual(4, self.cmd.indent())
    +        self.assertEqual(0, self.cmd2.indent())
    +
    +    def test_weave_should_work(self) -> None:
    +        wvr = MockWeaver()
    +        web = MockWeb()
    +        self.cmd.weave(web, wvr)
    +        self.assertEqual(wvr.write.mock_calls, [call('Some text & words in the document\n    ')])
    +
    +    def test_tangle_should_work(self) -> None:
    +        tnglr = MockTangler()
    +        web = MockWeb()
    +        self.cmd.tangle(web, tnglr)
    +        self.assertEqual(tnglr.write.mock_calls, [call('Some text & words in the document\n    ')])
    +
    + +
    +

    Unit Test of TextCommand class to contain a document text block (25). Used by: Unit Test of Command class hierarchy... (23)

    +
    +

    A CodeCommand object is a TextCommand with different processing for being emitted.

    +

    Unit Test of CodeCommand class to contain a program source code block (26) =

    +
    +class TestCodeCommand(unittest.TestCase):
    +    def setUp(self) -> None:
    +        self.cmd = pyweb.CodeCommand("Some text & words in the document\n    ", 314)
    +
    +    def test_weave_should_work(self) -> None:
    +        wvr = MockWeaver()
    +        web = MockWeb()
    +        self.cmd.weave(web, wvr)
    +        self.assertEqual(wvr.codeBlock.mock_calls, [call('quoted')])
    +
    +    def test_tangle_should_work(self) -> None:
    +        tnglr = MockTangler()
    +        web = MockWeb()
    +        self.cmd.tangle(web, tnglr)
    +        self.assertEqual(tnglr.codeBlock.mock_calls, [call('Some text & words in the document\n    ')])
    +
    + +
    +

    Unit Test of CodeCommand class to contain a program source code block (26). Used by: Unit Test of Command class hierarchy... (23)

    +
    +

    The XrefCommand class is largely abstract.

    +

    Unit Test of XrefCommand superclass for all cross-reference commands (27) =

    +
    +# No Tests
    +
    + +
    +

    Unit Test of XrefCommand superclass for all cross-reference commands (27). Used by: Unit Test of Command class hierarchy... (23)

    +
    +

    The FileXrefCommand command is expanded by a weaver to a list of @o +locations.

    +

    Unit Test of FileXrefCommand class for an output file cross-reference (28) =

    +
    +class TestFileXRefCommand(unittest.TestCase):
    +    def setUp(self) -> None:
    +        self.cmd = pyweb.FileXrefCommand(314)
    +
    +    def test_weave_should_work(self) -> None:
    +        wvr = MockWeaver()
    +        web = MockWeb()
    +        self.cmd.weave(web, wvr)
    +        self.assertEqual(wvr.mock_calls, [call.xrefHead(), call.xrefLine('file', [1, 2, 3]), call.xrefFoot()])
    +
    +    def test_tangle_should_fail(self) -> None:
    +        tnglr = MockTangler()
    +        web = MockWeb()
    +        try:
    +            self.cmd.tangle(web, tnglr)
    +            self.fail()
    +        except pyweb.Error:
    +            pass
    +
    + +
    +

    Unit Test of FileXrefCommand class for an output file cross-reference (28). Used by: Unit Test of Command class hierarchy... (23)

    +
    +

    The MacroXrefCommand command is expanded by a weaver to a list of all @d +locations.

    +

    Unit Test of MacroXrefCommand class for a named chunk cross-reference (29) =

    +
    +class TestMacroXRefCommand(unittest.TestCase):
    +    def setUp(self) -> None:
    +        self.cmd = pyweb.MacroXrefCommand(314)
    +
    +    def test_weave_should_work(self) -> None:
    +        wvr = MockWeaver()
    +        web = MockWeb()
    +        self.cmd.weave(web, wvr)
    +        self.assertEqual(wvr.mock_calls, [call.xrefHead(), call.xrefLine('chunk', [4, 5, 6]), call.xrefFoot()])
    +
    +    def test_tangle_should_fail(self) -> None:
    +        tnglr = MockTangler()
    +        web = MockWeb()
    +        try:
    +            self.cmd.tangle(web, tnglr)
    +            self.fail()
    +        except pyweb.Error:
    +            pass
    +
    + +
    +

    Unit Test of MacroXrefCommand class for a named chunk cross-reference (29). Used by: Unit Test of Command class hierarchy... (23)

    +
    +

    The UserIdXrefCommand command is expanded by a weaver to a list of all @| +names.

    +

    Unit Test of UserIdXrefCommand class for a user identifier cross-reference (30) =

    +
    +class TestUserIdXrefCommand(unittest.TestCase):
    +    def setUp(self) -> None:
    +        self.cmd = pyweb.UserIdXrefCommand(314)
    +
    +    def test_weave_should_work(self) -> None:
    +        wvr = MockWeaver()
    +        web = MockWeb()
    +        self.cmd.weave(web, wvr)
    +        self.assertEqual(wvr.mock_calls, [call.xrefHead(), call.xrefDefLine('name', 7, [8, 9, 10]), call.xrefFoot()])
    +
    +    def test_tangle_should_fail(self) -> None:
    +        tnglr = MockTangler()
    +        web = MockWeb()
    +        try:
    +            self.cmd.tangle(web, tnglr)
    +            self.fail()
    +        except pyweb.Error:
    +            pass
    +
    + +
    +

    Unit Test of UserIdXrefCommand class for a user identifier cross-reference (30). Used by: Unit Test of Command class hierarchy... (23)

    +
    +

    Reference commands require a context when tangling. +The context helps provide the required indentation. +They can't be simply tangled.

    +

    Unit Test of ReferenceCommand class for chunk references (31) =

    +
    +class TestReferenceCommand(unittest.TestCase):
    +    def setUp(self) -> None:
    +        self.chunk = MockChunk("Owning Chunk", 123, 456)
    +        self.cmd = pyweb.ReferenceCommand("Some Name", 314)
    +        self.cmd.chunk = self.chunk
    +        self.chunk.commands.append(self.cmd)
    +        self.chunk.previous_command = pyweb.TextCommand("", self.chunk.commands[0].lineNumber)
    +
    +    def test_weave_should_work(self) -> None:
    +        wvr = MockWeaver()
    +        web = MockWeb()
    +        self.cmd.weave(web, wvr)
    +        self.assertEqual(wvr.write.mock_calls, [call('Some Name')])
    +
    +    def test_tangle_should_work(self) -> None:
    +        tnglr = MockTangler()
    +        web = MockWeb()
    +        web.add(self.chunk)
    +        self.cmd.tangle(web, tnglr)
    +        self.assertEqual(tnglr.write.mock_calls, [call('Some Name')])
    +
    + +
    +

    Unit Test of ReferenceCommand class for chunk references (31). Used by: Unit Test of Command class hierarchy... (23)

    +
    +
    +
    +

    Reference Tests

    +

    The Reference class implements one of two search strategies for +cross-references. Either simple (or "immediate") or transitive.

    +

    The superclass is little more than an interface definition, +it's completely abstract. The two subclasses differ in +a single method.

    +

    Unit Test of Reference class hierarchy (32) =

    +
    +class TestReference(unittest.TestCase):
    +    def setUp(self) -> None:
    +        self.web = MockWeb()
    +        self.main = MockChunk("Main", 1, 11)
    +        self.parent = MockChunk("Parent", 2, 22)
    +        self.parent.referencedBy = [ self.main ]
    +        self.chunk = MockChunk("Sub", 3, 33)
    +        self.chunk.referencedBy = [ self.parent ]
    +    def test_simple_should_find_one(self) -> None:
    +        self.reference = pyweb.SimpleReference()
    +        theList = self.reference.chunkReferencedBy(self.chunk)
    +        self.assertEqual(1, len(theList))
    +        self.assertEqual(self.parent, theList[0])
    +    def test_transitive_should_find_all(self) -> None:
    +        self.reference = pyweb.TransitiveReference()
    +        theList = self.reference.chunkReferencedBy(self.chunk)
    +        self.assertEqual(2, len(theList))
    +        self.assertEqual(self.parent, theList[0])
    +        self.assertEqual(self.main, theList[1])
    +
    + +
    +

    Unit Test of Reference class hierarchy (32). Used by: test_unit.py (1)

    +
    +
    +
    +

    Web Tests

    +

    This is more difficult to create mocks for.

    +

    Unit Test of Web class (33) =

    +
    +class TestWebConstruction(unittest.TestCase):
    +    def setUp(self) -> None:
    +        self.web = pyweb.Web()
    +    →Unit Test Web class construction methods (34)
    +
    +class TestWebProcessing(unittest.TestCase):
    +    def setUp(self) -> None:
    +        self.web = pyweb.Web()
    +        self.web.web_path = Path("TestWebProcessing.w")
    +        self.chunk = pyweb.Chunk()
    +        self.chunk.appendText("some text")
    +        self.chunk.webAdd(self.web)
    +        self.out = pyweb.OutputChunk("A File")
    +        self.out.appendText("some code")
    +        nm = self.web.addDefName("A Chunk")
    +        self.out.append(pyweb.ReferenceCommand(nm))
    +        self.out.webAdd(self.web)
    +        self.named = pyweb.NamedChunk("A Chunk...")
    +        self.named.appendText("some user2a code")
    +        self.named.setUserIDRefs("user1")
    +        nm = self.web.addDefName("Another Chunk")
    +        self.named.append(pyweb.ReferenceCommand(nm))
    +        self.named.webAdd(self.web)
    +        self.named2 = pyweb.NamedChunk("Another Chunk...")
    +        self.named2.appendText("some user1 code")
    +        self.named2.setUserIDRefs("user2a user2b")
    +        self.named2.webAdd(self.web)
    +    →Unit Test Web class name resolution methods (35)
    +    →Unit Test Web class chunk cross-reference (36)
    +    →Unit Test Web class tangle (37)
    +    →Unit Test Web class weave (38)
    +
    + +
    +

    Unit Test of Web class (33). Used by: test_unit.py (1)

    +
    +

    Unit Test Web class construction methods (34) =

    +
    +def test_names_definition_should_resolve(self) -> None:
    +    name1 = self.web.addDefName("A Chunk...")
    +    self.assertTrue(name1 is None)
    +    self.assertEqual(0, len(self.web.named))
    +    name2 = self.web.addDefName("A Chunk Of Code")
    +    self.assertEqual("A Chunk Of Code", name2)
    +    self.assertEqual(1, len(self.web.named))
    +    name3 = self.web.addDefName("A Chunk...")
    +    self.assertEqual("A Chunk Of Code", name3)
    +    self.assertEqual(1, len(self.web.named))
    +
    +def test_chunks_should_add_and_index(self) -> None:
    +    chunk = pyweb.Chunk()
    +    chunk.appendText("some text")
    +    chunk.webAdd(self.web)
    +    self.assertEqual(1, len(self.web.chunkSeq))
    +    self.assertEqual(0, len(self.web.named))
    +    self.assertEqual(0, len(self.web.output))
    +    named = pyweb.NamedChunk("A Chunk")
    +    named.appendText("some code")
    +    named.webAdd(self.web)
    +    self.assertEqual(2, len(self.web.chunkSeq))
    +    self.assertEqual(1, len(self.web.named))
    +    self.assertEqual(0, len(self.web.output))
    +    out = pyweb.OutputChunk("A File")
    +    out.appendText("some code")
    +    out.webAdd(self.web)
    +    self.assertEqual(3, len(self.web.chunkSeq))
    +    self.assertEqual(1, len(self.web.named))
    +    self.assertEqual(1, len(self.web.output))
    +
    + +
    +

    Unit Test Web class construction methods (34). Used by: Unit Test of Web class... (33)

    +
    +

    Unit Test Web class name resolution methods (35) =

    +
    +def test_name_queries_should_resolve(self) -> None:
    +    self.assertEqual("A Chunk", self.web.fullNameFor("A C..."))
    +    self.assertEqual("A Chunk", self.web.fullNameFor("A Chunk"))
    +    self.assertNotEqual("A Chunk", self.web.fullNameFor("A File"))
    +    self.assertTrue(self.named is self.web.getchunk("A C...")[0])
    +    self.assertTrue(self.named is self.web.getchunk("A Chunk")[0])
    +    try:
    +        self.assertTrue(None is not self.web.getchunk("A File"))
    +        self.fail()
    +    except pyweb.Error as e:
    +        self.assertTrue(e.args[0].startswith("Cannot resolve 'A File'"))
    +
    + +
    +

    Unit Test Web class name resolution methods (35). Used by: Unit Test of Web class... (33)

    +
    +

    Unit Test Web class chunk cross-reference (36) =

    +
    +def test_valid_web_should_createUsedBy(self) -> None:
    +    self.web.createUsedBy()
    +    # If it raises an exception, the web structure is damaged
    +
    +def test_valid_web_should_createFileXref(self) -> None:
    +    file_xref = self.web.fileXref()
    +    self.assertEqual(1, len(file_xref))
    +    self.assertTrue("A File" in file_xref)
    +    self.assertTrue(1, len(file_xref["A File"]))
    +
    +def test_valid_web_should_createChunkXref(self) -> None:
    +    chunk_xref = self.web.chunkXref()
    +    self.assertEqual(2, len(chunk_xref))
    +    self.assertTrue("A Chunk" in chunk_xref)
    +    self.assertEqual(1, len(chunk_xref["A Chunk"]))
    +    self.assertTrue("Another Chunk" in chunk_xref)
    +    self.assertEqual(1, len(chunk_xref["Another Chunk"]))
    +    self.assertFalse("Not A Real Chunk" in chunk_xref)
    +
    +def test_valid_web_should_create_userNamesXref(self) -> None:
    +    user_xref = self.web.userNamesXref()
    +    self.assertEqual(3, len(user_xref))
    +    self.assertTrue("user1" in user_xref)
    +    defn, reflist = user_xref["user1"]
    +    self.assertEqual(1, len(reflist), "did not find user1")
    +    self.assertTrue("user2a" in user_xref)
    +    defn, reflist = user_xref["user2a"]
    +    self.assertEqual(1, len(reflist), "did not find user2a")
    +    self.assertTrue("user2b" in user_xref)
    +    defn, reflist = user_xref["user2b"]
    +    self.assertEqual(0, len(reflist))
    +    self.assertFalse("Not A User Symbol" in user_xref)
    +
    + +
    +

    Unit Test Web class chunk cross-reference (36). Used by: Unit Test of Web class... (33)

    +
    +

    Unit Test Web class tangle (37) =

    +
    +def test_valid_web_should_tangle(self) -> None:
    +    tangler = MockTangler()
    +    self.web.tangle(tangler)
    +    self.assertEqual(tangler.codeBlock.mock_calls, [
    +            call('some code'),
    +            call('some user2a code'),
    +            call('some user1 code'),
    +        ]
    +    )
    +
    + +
    +

    Unit Test Web class tangle (37). Used by: Unit Test of Web class... (33)

    +
    +

    Unit Test Web class weave (38) =

    +
    +def test_valid_web_should_weave(self) -> None:
    +    weaver = MockWeaver()
    +    self.web.weave(weaver)
    +    self.assertEqual(weaver.write.mock_calls, [
    +            call('some text'),
    +        ]
    +    )
    +    self.assertEqual(weaver.quote.mock_calls, [
    +            call('some code'),
    +            call('some user2a code'),
    +            call('some user1 code'),
    +        ]
    +    )
    +
    + +
    +

    Unit Test Web class weave (38). Used by: Unit Test of Web class... (33)

    +
    +
    +
    +

    WebReader Tests

    +

    Generally, this is tested separately through the functional tests. +Those tests each present source files to be processed by the +WebReader.

    +

    We should test this through some clever mocks that produce the +proper sequence of tokens to parse the various kinds of Commands.

    +

    Unit Test of WebReader class (39) =

    +
    +# Tested via functional tests
    +
    + +
    +

    Unit Test of WebReader class (39). Used by: test_unit.py (1)

    +
    +

    Some lower-level units: specifically the tokenizer and the option parser.

    +

    Unit Test of WebReader class (40) +=

    +
    +class TestTokenizer(unittest.TestCase):
    +    def test_should_split_tokens(self) -> None:
    +        input = io.StringIO("@@ word @{ @[ @< @>\n@] @} @i @| @m @f @u\n")
    +        self.tokenizer = pyweb.Tokenizer(input)
    +        tokens = list(self.tokenizer)
    +        self.assertEqual(24, len(tokens))
    +        self.assertEqual( ['@@', ' word ', '@{', ' ', '@[', ' ', '@<', ' ',
    +        '@>', '\n', '@]', ' ', '@}', ' ', '@i', ' ', '@|', ' ', '@m', ' ',
    +        '@f', ' ', '@u', '\n'], tokens )
    +        self.assertEqual(2, self.tokenizer.lineNumber)
    +
    + +
    +

    Unit Test of WebReader class (40). Used by: test_unit.py (1)

    +
    +

    Unit Test of WebReader class (41) +=

    +
    +class TestOptionParser_OutputChunk(unittest.TestCase):
    +    def setUp(self) -> None:
    +        self.option_parser = pyweb.OptionParser(
    +            pyweb.OptionDef("-start", nargs=1, default=None),
    +            pyweb.OptionDef("-end", nargs=1, default=""),
    +            pyweb.OptionDef("argument", nargs='*'),
    +        )
    +    def test_with_options_should_parse(self) -> None:
    +        text1 = " -start /* -end */ something.css "
    +        options1 = self.option_parser.parse(text1)
    +        self.assertEqual({'-end': ['*/'], '-start': ['/*'], 'argument': ['something.css']}, options1)
    +    def test_without_options_should_parse(self) -> None:
    +        text2 = " something.py "
    +        options2 = self.option_parser.parse(text2)
    +        self.assertEqual({'argument': ['something.py']}, options2)
    +
    +class TestOptionParser_NamedChunk(unittest.TestCase):
    +    def setUp(self) -> None:
    +        self.option_parser = pyweb.OptionParser(        pyweb.OptionDef( "-indent", nargs=0),
    +        pyweb.OptionDef("-noindent", nargs=0),
    +        pyweb.OptionDef("argument", nargs='*'),
    +        )
    +    def test_with_options_should_parse(self) -> None:
    +        text1 = " -indent the name of test1 chunk... "
    +        options1 = self.option_parser.parse(text1)
    +        self.assertEqual({'-indent': [], 'argument': ['the', 'name', 'of', 'test1', 'chunk...']}, options1)
    +    def test_without_options_should_parse(self) -> None:
    +        text2 = " the name of test2 chunk... "
    +        options2 = self.option_parser.parse(text2)
    +        self.assertEqual({'argument': ['the', 'name', 'of', 'test2', 'chunk...']}, options2)
    +
    + +
    +

    Unit Test of WebReader class (41). Used by: test_unit.py (1)

    +
    +
    +
    +

    Action Tests

    +

    Each class is tested separately. Sequence of some mocks, +load, tangle, weave.

    +

    Unit Test of Action class hierarchy (42) =

    +
    +→Unit test of Action Sequence class (43)
    +→Unit test of LoadAction class (46)
    +→Unit test of TangleAction class (45)
    +→Unit test of WeaverAction class (44)
    +
    + +
    +

    Unit Test of Action class hierarchy (42). Used by: test_unit.py (1)

    +
    +

    TODO: Replace with Mock

    +

    Unit test of Action Sequence class (43) =

    +
    +class TestActionSequence(unittest.TestCase):
    +    def setUp(self) -> None:
    +        self.web = MockWeb()
    +        self.a1 = MagicMock(name="Action1")
    +        self.a2 = MagicMock(name="Action2")
    +        self.action = pyweb.ActionSequence("TwoSteps", [self.a1, self.a2])
    +        self.action.web = self.web
    +        self.action.options = argparse.Namespace()
    +    def test_should_execute_both(self) -> None:
    +        self.action()
    +        self.assertEqual(self.a1.call_count, 1)
    +        self.assertEqual(self.a2.call_count, 1)
    +
    + +
    +

    Unit test of Action Sequence class (43). Used by: Unit Test of Action class hierarchy... (42)

    +
    +

    Unit test of WeaverAction class (44) =

    +
    +class TestWeaveAction(unittest.TestCase):
    +    def setUp(self) -> None:
    +        self.web = MockWeb()
    +        self.action = pyweb.WeaveAction()
    +        self.weaver = MockWeaver()
    +        self.action.web = self.web
    +        self.action.options = argparse.Namespace(
    +            theWeaver=self.weaver,
    +            reference_style=pyweb.SimpleReference(),
    +            output=Path.cwd(),
    +        )
    +    def test_should_execute_weaving(self) -> None:
    +        self.action()
    +        self.assertEqual(self.web.weave.mock_calls, [call(self.weaver)])
    +
    + +
    +

    Unit test of WeaverAction class (44). Used by: Unit Test of Action class hierarchy... (42)

    +
    +

    Unit test of TangleAction class (45) =

    +
    +class TestTangleAction(unittest.TestCase):
    +    def setUp(self) -> None:
    +        self.web = MockWeb()
    +        self.action = pyweb.TangleAction()
    +        self.tangler = MockTangler()
    +        self.action.web = self.web
    +        self.action.options = argparse.Namespace(
    +            theTangler = self.tangler,
    +            tangler_line_numbers = False,
    +            output=Path.cwd()
    +        )
    +    def test_should_execute_tangling(self) -> None:
    +        self.action()
    +        self.assertEqual(self.web.tangle.mock_calls, [call(self.tangler)])
    +
    + +
    +

    Unit test of TangleAction class (45). Used by: Unit Test of Action class hierarchy... (42)

    +
    +

    The mocked WebReader must provide an errors property to the LoadAction instance.

    +

    Unit test of LoadAction class (46) =

    +
    +class TestLoadAction(unittest.TestCase):
    +    def setUp(self) -> None:
    +        self.web = MockWeb()
    +        self.action = pyweb.LoadAction()
    +        self.webReader = Mock(
    +            name="WebReader",
    +            errors=0,
    +        )
    +        self.action.web = self.web
    +        self.source_path = Path("TestLoadAction.w")
    +        self.action.options = argparse.Namespace(
    +            webReader = self.webReader,
    +            source_path=self.source_path,
    +            command="@",
    +            permitList = [],
    +            output=Path.cwd(),
    +        )
    +        Path("TestLoadAction.w").write_text("")
    +    def tearDown(self) -> None:
    +        try:
    +            Path("TestLoadAction.w").unlink()
    +        except IOError:
    +            pass
    +    def test_should_execute_loading(self) -> None:
    +        self.action()
    +        # Old: self.assertEqual(1, self.webReader.count)
    +        print(self.webReader.load.mock_calls)
    +        self.assertEqual(self.webReader.load.mock_calls, [call(self.web, self.source_path)])
    +        self.webReader.web.assert_not_called()  # Deprecated
    +        self.webReader.source.assert_not_called()  # Deprecated
    +
    + +
    +

    Unit test of LoadAction class (46). Used by: Unit Test of Action class hierarchy... (42)

    +
    +
    +
    +

    Application Tests

    +

    As with testing WebReader, this requires extensive mocking. +It's easier to simply run the various use cases.

    +

    TODO: Test Application class

    +

    Unit Test of Application class (47) =

    +
    +# TODO Test Application class
    +
    + +
    +

    Unit Test of Application class (47). Used by: test_unit.py (1)

    +
    +
    +
    +

    Overheads and Main Script

    +

    The boilerplate code for unit testing is the following.

    +

    Unit Test overheads: imports, etc. (48) =

    +
    +"""Unit tests."""
    +import argparse
    +import io
    +import logging
    +import os
    +from pathlib import Path
    +import re
    +import string
    +import sys
    +import textwrap
    +import time
    +from typing import Any, TextIO
    +import unittest
    +from unittest.mock import Mock, call, MagicMock, sentinel
    +import warnings
    +
    +import pyweb
    +
    + +
    +

    Unit Test overheads: imports, etc. (48). Used by: test_unit.py (1)

    +
    +

    One more overhead is a function we can inject into selected subclasses +of unittest.TestCase. This is monkeypatch feature that seems useful.

    +

    Unit Test overheads: imports, etc. (49) +=

    +
    +def rstrip_lines(source: str) -> list[str]:
    +    return list(l.rstrip() for l in source.splitlines())
    +
    + +
    +

    Unit Test overheads: imports, etc. (49). Used by: test_unit.py (1)

    +
    +

    Unit Test main (50) =

    +
    +if __name__ == "__main__":
    +    logging.basicConfig(stream=sys.stdout, level=logging.WARN)
    +    unittest.main()
    +
    + +
    +

    Unit Test main (50). Used by: test_unit.py (1)

    +
    +

    We run the default unittest.main() to execute the entire suite of tests.

    +
    +
    +
    +

    Functional Testing

    + +

    There are three broad areas of functional testing.

    + +

    There are a total of 11 test cases.

    +
    +

    Tests for Loading

    +

    We need to be able to load a web from one or more source files.

    +

    test_loader.py (51) =

    +
    +→Load Test overheads: imports, etc. (53), →(58)
    +
    +→Load Test superclass to refactor common setup (52)
    +
    +→Load Test error handling with a few common syntax errors (54)
    +
    +→Load Test include processing with syntax errors (56)
    +
    +→Load Test main program (59)
    +
    + +
    +

    test_loader.py (51).

    +
    +

    Parsing test cases have a common setup shown in this superclass.

    +

    By using some class-level variables text, +file_path, we can simply provide a file-like +input object to the WebReader instance.

    +

    Load Test superclass to refactor common setup (52) =

    +
    +class ParseTestcase(unittest.TestCase):
    +    text: ClassVar[str]
    +    file_path: ClassVar[Path]
    +
    +    def setUp(self) -> None:
    +        self.source = io.StringIO(self.text)
    +        self.web = pyweb.Web()
    +        self.rdr = pyweb.WebReader()
    +
    + +
    +

    Load Test superclass to refactor common setup (52). Used by: test_loader.py (51)

    +
    +

    There are a lot of specific parsing exceptions which can be thrown. +We'll cover most of the cases with a quick check for a failure to +find an expected next token.

    +

    Load Test overheads: imports, etc. (53) =

    +
    +import logging.handlers
    +from pathlib import Path
    +from typing import ClassVar
    +
    + +
    +

    Load Test overheads: imports, etc. (53). Used by: test_loader.py (51)

    +
    +

    Load Test error handling with a few common syntax errors (54) =

    +
    +→Sample Document 1 with correct and incorrect syntax (55)
    +
    +class Test_ParseErrors(ParseTestcase):
    +    text = test1_w
    +    file_path = Path("test1.w")
    +    def test_error_should_count_1(self) -> None:
    +        with self.assertLogs('WebReader', level='WARN') as log_capture:
    +            self.rdr.load(self.web, self.file_path, self.source)
    +        self.assertEqual(3, self.rdr.errors)
    +        self.assertEqual(log_capture.output,
    +            [
    +                "ERROR:WebReader:At ('test1.w', 8): expected ('@{',), found '@o'",
    +                "ERROR:WebReader:Extra '@{' (possibly missing chunk name) near ('test1.w', 9)",
    +                "ERROR:WebReader:Extra '@{' (possibly missing chunk name) near ('test1.w', 9)"
    +            ]
    +        )
    +
    + +
    +

    Load Test error handling with a few common syntax errors (54). Used by: test_loader.py (51)

    +
    +

    Sample Document 1 with correct and incorrect syntax (55) =

    +
    +test1_w = """Some anonymous chunk
    +@o test1.tmp
    +@{@<part1@>
    +@<part2@>
    +@}@@
    +@d part1 @{This is part 1.@}
    +Okay, now for an error.
    +@o show how @o commands work
    +@{ @{ @] @]
    +"""
    +
    + +
    +

    Sample Document 1 with correct and incorrect syntax (55). Used by: Load Test error handling... (54)

    +
    +

    All of the parsing exceptions should be correctly identified with +any included file. +We'll cover most of the cases with a quick check for a failure to +find an expected next token.

    +

    In order to test the include file processing, we have to actually +create a temporary file. It's hard to mock the include processing, +since it's a nested instance of the tokenizer.

    +

    Load Test include processing with syntax errors (56) =

    +
    +→Sample Document 8 and the file it includes (57)
    +
    +class Test_IncludeParseErrors(ParseTestcase):
    +    text = test8_w
    +    file_path = Path("test8.w")
    +    def setUp(self) -> None:
    +        super().setUp()
    +        Path('test8_inc.tmp').write_text(test8_inc_w)
    +    def test_error_should_count_2(self) -> None:
    +        with self.assertLogs('WebReader', level='WARN') as log_capture:
    +            self.rdr.load(self.web, self.file_path, self.source)
    +        self.assertEqual(1, self.rdr.errors)
    +        self.assertEqual(log_capture.output,
    +            [
    +                "ERROR:WebReader:At ('test8_inc.tmp', 4): end of input, ('@{', '@[') not found",
    +                "ERROR:WebReader:Errors in included file 'test8_inc.tmp', output is incomplete."
    +            ]
    +        )
    +    def tearDown(self) -> None:
    +        super().tearDown()
    +        Path('test8_inc.tmp').unlink()
    +
    + +
    +

    Load Test include processing with syntax errors (56). Used by: test_loader.py (51)

    +
    +

    The sample document must reference the correct name that will +be given to the included document by setUp.

    +

    Sample Document 8 and the file it includes (57) =

    +
    +test8_w = """Some anonymous chunk.
    +@d title @[the title of this document, defined with @@[ and @@]@]
    +A reference to @<title@>.
    +@i test8_inc.tmp
    +A final anonymous chunk from test8.w
    +"""
    +
    +test8_inc_w="""A chunk from test8a.w
    +And now for an error - incorrect syntax in an included file!
    +@d yap
    +"""
    +
    + +
    +

    Sample Document 8 and the file it includes (57). Used by: Load Test include... (56)

    +
    +

    <p>The overheads for a Python unittest.</p>

    +

    Load Test overheads: imports, etc. (58) +=

    +
    +"""Loader and parsing tests."""
    +import io
    +import logging
    +import os
    +from pathlib import Path
    +import string
    +import sys
    +import types
    +import unittest
    +
    +import pyweb
    +
    + +
    +

    Load Test overheads: imports, etc. (58). Used by: test_loader.py (51)

    +
    +

    A main program that configures logging and then runs the test.

    +

    Load Test main program (59) =

    +
    +if __name__ == "__main__":
    +    logging.basicConfig(stream=sys.stdout, level=logging.WARN)
    +    unittest.main()
    +
    + +
    +

    Load Test main program (59). Used by: test_loader.py (51)

    +
    +
    +
    +

    Tests for Tangling

    +

    We need to be able to tangle a web.

    +

    test_tangler.py (60) =

    +
    +→Tangle Test overheads: imports, etc. (74)
    +→Tangle Test superclass to refactor common setup (61)
    +→Tangle Test semantic error 2 (62)
    +→Tangle Test semantic error 3 (64)
    +→Tangle Test semantic error 4 (66)
    +→Tangle Test semantic error 5 (68)
    +→Tangle Test semantic error 6 (70)
    +→Tangle Test include error 7 (72)
    +→Tangle Test main program (75)
    +
    + +
    +

    test_tangler.py (60).

    +
    +

    Tangling test cases have a common setup and teardown shown in this superclass. +Since tangling must produce a file, it's helpful to remove the file that gets created. +The essential test case is to load and attempt to tangle, checking the +exceptions raised.

    +

    Tangle Test superclass to refactor common setup (61) =

    +
    +class TangleTestcase(unittest.TestCase):
    +    text: ClassVar[str]
    +    error: ClassVar[str]
    +    file_path: ClassVar[Path]
    +
    +    def setUp(self) -> None:
    +        self.source = io.StringIO(self.text)
    +        self.web = pyweb.Web()
    +        self.rdr = pyweb.WebReader()
    +        self.tangler = pyweb.Tangler()
    +
    +    def tangle_and_check_exception(self, exception_text: str) -> None:
    +        try:
    +            self.rdr.load(self.web, self.file_path, self.source)
    +            self.web.tangle(self.tangler)
    +            self.web.createUsedBy()
    +            self.fail("Should not tangle")
    +        except pyweb.Error as e:
    +            self.assertEqual(exception_text, e.args[0])
    +
    +    def tearDown(self) -> None:
    +        try:
    +            self.file_path.with_suffix(".tmp").unlink()
    +        except FileNotFoundError:
    +            pass  # If the test fails, nothing to remove...
    +
    + +
    +

    Tangle Test superclass to refactor common setup (61). Used by: test_tangler.py (60)

    +
    +

    Tangle Test semantic error 2 (62) =

    +
    +→Sample Document 2 (63)
    +
    +class Test_SemanticError_2(TangleTestcase):
    +    text = test2_w
    +    file_path = Path("test2.w")
    +    def test_should_raise_undefined(self) -> None:
    +        self.tangle_and_check_exception("Attempt to tangle an undefined Chunk, part2.")
    +
    + +
    +

    Tangle Test semantic error 2 (62). Used by: test_tangler.py (60)

    +
    +

    Sample Document 2 (63) =

    +
    +test2_w = """Some anonymous chunk
    +@o test2.tmp
    +@{@<part1@>
    +@<part2@>
    +@}@@
    +@d part1 @{This is part 1.@}
    +Okay, now for some errors: no part2!
    +"""
    +
    + +
    +

    Sample Document 2 (63). Used by: Tangle Test semantic error 2... (62)

    +
    +

    Tangle Test semantic error 3 (64) =

    +
    +→Sample Document 3 (65)
    +
    +class Test_SemanticError_3(TangleTestcase):
    +    text = test3_w
    +    file_path = Path("test3.w")
    +    def test_should_raise_bad_xref(self) -> None:
    +        self.tangle_and_check_exception("Illegal tangling of a cross reference command.")
    +
    + +
    +

    Tangle Test semantic error 3 (64). Used by: test_tangler.py (60)

    +
    +

    Sample Document 3 (65) =

    +
    +test3_w = """Some anonymous chunk
    +@o test3.tmp
    +@{@<part1@>
    +@<part2@>
    +@}@@
    +@d part1 @{This is part 1.@}
    +@d part2 @{This is part 2, with an illegal: @f.@}
    +Okay, now for some errors: attempt to tangle a cross-reference!
    +"""
    +
    + +
    +

    Sample Document 3 (65). Used by: Tangle Test semantic error 3... (64)

    +
    +

    Tangle Test semantic error 4 (66) =

    +
    +→Sample Document 4 (67)
    +
    +class Test_SemanticError_4(TangleTestcase):
    +    text = test4_w
    +    file_path = Path("test4.w")
    +    def test_should_raise_noFullName(self) -> None:
    +        self.tangle_and_check_exception("No full name for 'part1...'")
    +
    + +
    +

    Tangle Test semantic error 4 (66). Used by: test_tangler.py (60)

    +
    +

    Sample Document 4 (67) =

    +
    +test4_w = """Some anonymous chunk
    +@o test4.tmp
    +@{@<part1...@>
    +@<part2@>
    +@}@@
    +@d part1... @{This is part 1.@}
    +@d part2 @{This is part 2.@}
    +Okay, now for some errors: attempt to weave but no full name for part1....
    +"""
    +
    + +
    +

    Sample Document 4 (67). Used by: Tangle Test semantic error 4... (66)

    +
    +

    Tangle Test semantic error 5 (68) =

    +
    +→Sample Document 5 (69)
    +
    +class Test_SemanticError_5(TangleTestcase):
    +    text = test5_w
    +    file_path = Path("test5.w")
    +    def test_should_raise_ambiguous(self) -> None:
    +        self.tangle_and_check_exception("Ambiguous abbreviation 'part1...', matches ['part1a', 'part1b']")
    +
    + +
    +

    Tangle Test semantic error 5 (68). Used by: test_tangler.py (60)

    +
    +

    Sample Document 5 (69) =

    +
    +test5_w = """
    +Some anonymous chunk
    +@o test5.tmp
    +@{@<part1...@>
    +@<part2@>
    +@}@@
    +@d part1a @{This is part 1 a.@}
    +@d part1b @{This is part 1 b.@}
    +@d part2 @{This is part 2.@}
    +Okay, now for some errors: part1... is ambiguous
    +"""
    +
    + +
    +

    Sample Document 5 (69). Used by: Tangle Test semantic error 5... (68)

    +
    +

    Tangle Test semantic error 6 (70) =

    +
    +→Sample Document 6 (71)
    +
    +class Test_SemanticError_6(TangleTestcase):
    +    text = test6_w
    +    file_path = Path("test6.w")
    +    def test_should_warn(self) -> None:
    +        self.rdr.load(self.web, self.file_path, self.source)
    +        self.web.tangle(self.tangler)
    +        self.web.createUsedBy()
    +        self.assertEqual(1, len(self.web.no_reference()))
    +        self.assertEqual(1, len(self.web.multi_reference()))
    +        self.assertEqual(0, len(self.web.no_definition()))
    +
    + +
    +

    Tangle Test semantic error 6 (70). Used by: test_tangler.py (60)

    +
    +

    Sample Document 6 (71) =

    +
    +test6_w = """Some anonymous chunk
    +@o test6.tmp
    +@{@<part1...@>
    +@<part1a@>
    +@}@@
    +@d part1a @{This is part 1 a.@}
    +@d part2 @{This is part 2.@}
    +Okay, now for some warnings:
    +- part1 has multiple references.
    +- part2 is unreferenced.
    +"""
    +
    + +
    +

    Sample Document 6 (71). Used by: Tangle Test semantic error 6... (70)

    +
    +

    Tangle Test include error 7 (72) =

    +
    +→Sample Document 7 and it's included file (73)
    +
    +class Test_IncludeError_7(TangleTestcase):
    +    text = test7_w
    +    file_path = Path("test7.w")
    +    def setUp(self) -> None:
    +        Path('test7_inc.tmp').write_text(test7_inc_w)
    +        super().setUp()
    +    def test_should_include(self) -> None:
    +        self.rdr.load(self.web, self.file_path, self.source)
    +        self.web.tangle(self.tangler)
    +        self.web.createUsedBy()
    +        self.assertEqual(5, len(self.web.chunkSeq))
    +        self.assertEqual(test7_inc_w, self.web.chunkSeq[3].commands[0].text)
    +    def tearDown(self) -> None:
    +        Path('test7_inc.tmp').unlink()
    +        super().tearDown()
    +
    + +
    +

    Tangle Test include error 7 (72). Used by: test_tangler.py (60)

    +
    +

    Sample Document 7 and it's included file (73) =

    +
    +test7_w = """
    +Some anonymous chunk.
    +@d title @[the title of this document, defined with @@[ and @@]@]
    +A reference to @<title@>.
    +@i test7_inc.tmp
    +A final anonymous chunk from test7.w
    +"""
    +
    +test7_inc_w = """The test7a.tmp chunk for test7.w
    +"""
    +
    + +
    +

    Sample Document 7 and it's included file (73). Used by: Tangle Test include error 7... (72)

    +
    +

    Tangle Test overheads: imports, etc. (74) =

    +
    +"""Tangler tests exercise various semantic features."""
    +import io
    +import logging
    +import os
    +from pathlib import Path
    +from typing import ClassVar
    +import unittest
    +
    +import pyweb
    +
    + +
    +

    Tangle Test overheads: imports, etc. (74). Used by: test_tangler.py (60)

    +
    +

    Tangle Test main program (75) =

    +
    +if __name__ == "__main__":
    +    import sys
    +    logging.basicConfig(stream=sys.stdout, level=logging.WARN)
    +    unittest.main()
    +
    + +
    +

    Tangle Test main program (75). Used by: test_tangler.py (60)

    +
    +
    +
    +

    Tests for Weaving

    +

    We need to be able to weave a document from one or more source files.

    +

    test_weaver.py (76) =

    +
    +→Weave Test overheads: imports, etc. (83)
    +→Weave Test superclass to refactor common setup (77)
    +→Weave Test references and definitions (78)
    +→Weave Test evaluation of expressions (81)
    +→Weave Test main program (84)
    +
    + +
    +

    test_weaver.py (76).

    +
    +

    Weaving test cases have a common setup shown in this superclass.

    +

    Weave Test superclass to refactor common setup (77) =

    +
    +class WeaveTestcase(unittest.TestCase):
    +    text: ClassVar[str]
    +    error: ClassVar[str]
    +    file_path: ClassVar[Path]
    +
    +    def setUp(self) -> None:
    +        self.source = io.StringIO(self.text)
    +        self.web = pyweb.Web()
    +        self.rdr = pyweb.WebReader()
    +
    +    def tearDown(self) -> None:
    +        try:
    +            self.file_path.with_suffix(".html").unlink()
    +        except FileNotFoundError:
    +            pass  # if the test failed, nothing to remove
    +
    + +
    +

    Weave Test superclass to refactor common setup (77). Used by: test_weaver.py (76)

    +
    +

    Weave Test references and definitions (78) =

    +
    +→Sample Document 0 (79)
    +→Expected Output 0 (80)
    +
    +class Test_RefDefWeave(WeaveTestcase):
    +    text = test0_w
    +    file_path = Path("test0.w")
    +    def test_load_should_createChunks(self) -> None:
    +        self.rdr.load(self.web, self.file_path, self.source)
    +        self.assertEqual(3, len(self.web.chunkSeq))
    +    def test_weave_should_createFile(self) -> None:
    +        self.rdr.load(self.web, self.file_path, self.source)
    +        doc = pyweb.HTML()
    +        doc.reference_style = pyweb.SimpleReference()
    +        self.web.weave(doc)
    +        actual = self.file_path.with_suffix(".html").read_text()
    +        self.maxDiff = None
    +        self.assertEqual(test0_expected, actual)
    +
    + +
    +

    Weave Test references and definitions (78). Used by: test_weaver.py (76)

    +
    +

    Sample Document 0 (79) =

    +
    +test0_w = """<html>
    +<head>
    +    <link rel="StyleSheet" href="pyweb.css" type="text/css" />
    +</head>
    +<body>
    +@<some code@>
    +
    +@d some code
    +@{
    +def fastExp(n, p):
    +    r = 1
    +    while p > 0:
    +        if p%2 == 1: return n*fastExp(n,p-1)
    +    return n*n*fastExp(n,p/2)
    +
    +for i in range(24):
    +    fastExp(2,i)
    +@}
    +</body>
    +</html>
    +"""
    +
    + +
    +

    Sample Document 0 (79). Used by: Weave Test references... (78)

    +
    +

    Expected Output 0 (80) =

    +
    +test0_expected = """<html>
    +<head>
    +    <link rel="StyleSheet" href="pyweb.css" type="text/css" />
    +</head>
    +<body>
    +<a href="#pyweb1">&rarr;<em>some code</em> (1)</a>
    +
    +
    +    <a name="pyweb1"></a>
    +    <!--line number 10-->
    +    <p><em>some code</em> (1)&nbsp;=</p>
    +    <pre><code>
    +
    +def fastExp(n, p):
    +    r = 1
    +    while p &gt; 0:
    +        if p%2 == 1: return n*fastExp(n,p-1)
    +    return n*n*fastExp(n,p/2)
    +
    +for i in range(24):
    +    fastExp(2,i)
    +
    +    </code></pre>
    +    <p>&loz; <em>some code</em> (1).
    +
    +    </p>
    +
    +</body>
    +</html>
    +"""
    +
    + +
    +

    Expected Output 0 (80). Used by: Weave Test references... (78)

    +
    +

    Note that this really requires a mocked time module in order +to properly provide a consistent output from time.asctime().

    +

    Weave Test evaluation of expressions (81) =

    +
    +→Sample Document 9 (82)
    +
    +from unittest.mock import Mock
    +
    +class TestEvaluations(WeaveTestcase):
    +    text = test9_w
    +    file_path = Path("test9.w")
    +    def setUp(self):
    +        super().setUp()
    +        self.mock_time = Mock(asctime=Mock(return_value="mocked time"))
    +    def test_should_evaluate(self) -> None:
    +        self.rdr.load(self.web, self.file_path, self.source)
    +        doc = pyweb.HTML( )
    +        doc.reference_style = pyweb.SimpleReference()
    +        self.web.weave(doc)
    +        actual = self.file_path.with_suffix(".html").read_text().splitlines()
    +        #print(actual)
    +        self.assertEqual("An anonymous chunk.", actual[0])
    +        self.assertTrue("Time = mocked time", actual[1])
    +        self.assertEqual("File = ('test9.w', 3)", actual[2])
    +        self.assertEqual('Version = 3.1', actual[3])
    +        self.assertEqual(f'CWD = {os.getcwd()}', actual[4])
    +
    + +
    +

    Weave Test evaluation of expressions (81). Used by: test_weaver.py (76)

    +
    +

    Sample Document 9 (82) =

    +
    +test9_w= """An anonymous chunk.
    +Time = @(time.asctime()@)
    +File = @(theLocation@)
    +Version = @(__version__@)
    +CWD = @(os.path.realpath('.')@)
    +"""
    +
    + +
    +

    Sample Document 9 (82). Used by: Weave Test evaluation... (81)

    +
    +

    Weave Test overheads: imports, etc. (83) =

    +
    +"""Weaver tests exercise various weaving features."""
    +import io
    +import logging
    +import os
    +from pathlib import Path
    +import string
    +import sys
    +from typing import ClassVar
    +import unittest
    +
    +import pyweb
    +
    + +
    +

    Weave Test overheads: imports, etc. (83). Used by: test_weaver.py (76)

    +
    +

    Weave Test main program (84) =

    +
    +if __name__ == "__main__":
    +    logging.basicConfig(stream=sys.stderr, level=logging.WARN)
    +    unittest.main()
    +
    + +
    +

    Weave Test main program (84). Used by: test_weaver.py (76)

    +
    +
    +
    +
    +

    Additional Scripts Testing

    + +

    We provide these two additional scripts; effectively command-line short-cuts:

    +
      +
    • tangle.py
    • +
    • weave.py
    • +
    +

    These need their own test cases.

    +

    This gives us the following outline for the script testing.

    +

    test_scripts.py (85) =

    +
    +→Script Test overheads: imports, etc. (90)
    +
    +→Sample web file to test with (86)
    +
    +→Superclass for test cases (87)
    +
    +→Test of weave.py (88)
    +
    +→Test of tangle.py (89)
    +
    +→Scripts Test main (91)
    +
    + +
    +

    test_scripts.py (85).

    +
    +
    +

    Sample Web File

    +

    This is a web .w file to create a document and tangle a small file.

    +

    Sample web file to test with (86) =

    +
    +sample = textwrap.dedent("""
    +    <!doctype html>
    +    <html lang="en">
    +      <head>
    +        <meta charset="utf-8">
    +        <meta name="viewport" content="width=device-width, initial-scale=1">
    +        <title>Sample HTML web file</title>
    +      </head>
    +      <body>
    +        <h1>Sample HTML web file</h1>
    +        <p>We're avoiding using Python specifically.
    +        This hints at other languages being tangled by this tool.</p>
    +
    +    @o sample_tangle.code
    +    @{
    +    @<preamble@>
    +    @<body@>
    +    @}
    +
    +    @d preamble
    +    @{
    +    #include <stdio.h>
    +    @}
    +
    +    @d body
    +    @{
    +    int main() {
    +        println("Hello, World!")
    +    }
    +    @}
    +
    +      </body>
    +    </html>
    +    """)
    +
    + +
    +

    Sample web file to test with (86). Used by: test_scripts.py (85)

    +
    +
    +
    +

    Superclass for test cases

    +

    This superclass definition creates a consistent test fixture for both test cases. +The sample test_sample.w file is created and removed after the test.

    +

    Superclass for test cases (87) =

    +
    +class SampleWeb(unittest.TestCase):
    +    def setUp(self) -> None:
    +        self.sample_path = Path("test_sample.w")
    +        self.sample_path.write_text(sample)
    +    def tearDown(self) -> None:
    +        self.sample_path.unlink()
    +
    + +
    +

    Superclass for test cases (87). Used by: test_scripts.py (85)

    +
    +
    +
    +

    Weave Script Test

    +

    We check the weave output to be sure it's what we expected. +This could be altered to check a few features of the weave file rather than compare the entire file.

    +

    Test of weave.py (88) =

    +
    +expected_weave = textwrap.dedent("""
    +    <!doctype html>
    +    <html lang="en">
    +      <head>
    +        <meta charset="utf-8">
    +        <meta name="viewport" content="width=device-width, initial-scale=1">
    +        <title>Sample HTML web file</title>
    +      </head>
    +      <body>
    +        <h1>Sample HTML web file</h1>
    +        <p>We're avoiding using Python specifically.
    +        This hints at other languages being tangled by this tool.</p>
    +
    +    <a name="pyweb1"></a>
    +        <!--line number 16-->
    +        <p>``sample_tangle.code`` (1)&nbsp;=</p>
    +        <pre><code>
    +
    +    <a href="#pyweb2">&rarr;<em>preamble</em>&nbsp;(2)</a>
    +    <a href="#pyweb3">&rarr;<em>body</em>&nbsp;(3)</a>
    +    </code></pre>
    +        <p>&loz; ``sample_tangle.code`` (1).
    +        []
    +        </p>
    +
    +
    +    <a name="pyweb2"></a>
    +        <!--line number 22-->
    +        <p><em>preamble</em> (2)&nbsp;=</p>
    +        <pre><code>
    +
    +    #include &lt;stdio.h&gt;
    +
    +        </code></pre>
    +        <p>&loz; <em>preamble</em> (2).
    +          Used by <a href="#pyweb1"><em>sample_tangle.code</em>&nbsp;(1)</a>.
    +        </p>
    +
    +
    +    <a name="pyweb3"></a>
    +        <!--line number 27-->
    +        <p><em>body</em> (3)&nbsp;=</p>
    +        <pre><code>
    +
    +    int main() {
    +        println(&quot;Hello, World!&quot;)
    +    }
    +
    +        </code></pre>
    +        <p>&loz; <em>body</em> (3).
    +          Used by <a href="#pyweb1"><em>sample_tangle.code</em>&nbsp;(1)</a>.
    +        </p>
    +
    +
    +      </body>
    +    </html>
    +    """)
    +
    +class TestWeave(SampleWeb):
    +    def setUp(self) -> None:
    +        super().setUp()
    +        self.output = self.sample_path.with_suffix(".html")
    +    def test(self) -> None:
    +        weave.main(self.sample_path)
    +        result = self.output.read_text()
    +        self.assertEqual(result, expected_weave)
    +    def tearDown(self) -> None:
    +        super().tearDown()
    +        self.output.unlink()
    +
    + +
    +

    Test of weave.py (88). Used by: test_scripts.py (85)

    +
    +
    +
    +

    Tangle Script Test

    +

    We check the tangle output to be sure it's what we expected.

    +

    Test of tangle.py (89) =

    +
    +expected_tangle = textwrap.dedent("""
    +
    +    #include <stdio.h>
    +
    +
    +    int main() {
    +        println("Hello, World!")
    +    }
    +
    +    """)
    +
    +class TestTangle(SampleWeb):
    +    def setUp(self) -> None:
    +        super().setUp()
    +        self.output = Path("sample_tangle.code")
    +    def test(self) -> None:
    +        tangle.main(self.sample_path)
    +        result = self.output.read_text()
    +        self.assertEqual(result, expected_tangle)
    +    def tearDown(self) -> None:
    +        super().tearDown()
    +        self.output.unlink()
    +
    + +
    +

    Test of tangle.py (89). Used by: test_scripts.py (85)

    +
    +
    +
    +

    Overheads and Main Script

    +

    This is typical of the other test modules. We provide a unittest runner +here in case we want to run these tests in isolation.

    +

    Script Test overheads: imports, etc. (90) =

    +
    +"""Script tests."""
    +import logging
    +from pathlib import Path
    +import sys
    +import textwrap
    +import unittest
    +
    +import tangle
    +import weave
    +
    + +
    +

    Script Test overheads: imports, etc. (90). Used by: test_scripts.py (85)

    +
    +

    Scripts Test main (91) =

    +
    +if __name__ == "__main__":
    +    logging.basicConfig(stream=sys.stdout, level=logging.WARN)
    +    unittest.main()
    +
    + +
    +

    Scripts Test main (91). Used by: test_scripts.py (85)

    +
    +

    We run the default unittest.main() to execute the entire suite of tests.

    +

    No Longer supported: @i runner.w, using pytest seems better.

    +
    +
    +
    +

    Additional Files

    +

    To get the RST to look good, there are two additional files. +These are clones of what's in the src directory.

    +
    +
    docutils.conf defines two CSS files to use.
    +
    The default CSS file may need to be customized.
    +
    +

    docutils.conf (92) =

    +
    +# docutils.conf
    +
    +[html4css1 writer]
    +stylesheet-path: /Users/slott/miniconda3/envs/pywebtool/lib/python3.10/site-packages/docutils/writers/html4css1/html4css1.css,
    +    page-layout.css
    +syntax-highlight: long
    +
    + +
    +

    docutils.conf (92).

    +
    +

    page-layout.css This tweaks one CSS to be sure that +the resulting HTML pages are easier to read. These are minor +tweaks to the default CSS.

    +

    page-layout.css (93) =

    +
    +/* Page layout tweaks */
    +div.document { width: 7in; }
    +.small { font-size: smaller; }
    +.code
    +{
    +    color: #101080;
    +    display: block;
    +    border-color: black;
    +    border-width: thin;
    +    border-style: solid;
    +    background-color: #E0FFFF;
    +    /*#99FFFF*/
    +    padding: 0 0 0 1%;
    +    margin: 0 6% 0 6%;
    +    text-align: left;
    +    font-size: smaller;
    +}
    +
    + +
    +

    page-layout.css (93).

    +
    +
    +
    +

    Indices

    +
    +

    Files

    + +++ + + + + + + + + + + + + + + + + + + +
    docutils.conf:→(92)
    page-layout.css:
     →(93)
    test_loader.py:→(51)
    test_scripts.py:
     →(85)
    test_tangler.py:
     →(60)
    test_unit.py:→(1)
    test_weaver.py:→(76)
    +
    +
    +

    Macros

    + +++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Expected Output 0:
     →(80)
    Load Test error handling with a few common syntax errors:
     →(54)
    Load Test include processing with syntax errors:
     →(56)
    Load Test main program:
     →(59)
    Load Test overheads:
     imports, etc.: +→(53) →(58)
    Load Test superclass to refactor common setup:
     →(52)
    Sample Document 0:
     →(79)
    Sample Document 1 with correct and incorrect syntax:
     →(55)
    Sample Document 2:
     →(63)
    Sample Document 3:
     →(65)
    Sample Document 4:
     →(67)
    Sample Document 5:
     →(69)
    Sample Document 6:
     →(71)
    Sample Document 7 and it's included file:
     →(73)
    Sample Document 8 and the file it includes:
     →(57)
    Sample Document 9:
     →(82)
    Sample web file to test with:
     →(86)
    Script Test overheads:
     imports, etc.: +→(90)
    Scripts Test main:
     →(91)
    Superclass for test cases:
     →(87)
    Tangle Test include error 7:
     →(72)
    Tangle Test main program:
     →(75)
    Tangle Test overheads:
     imports, etc.: +→(74)
    Tangle Test semantic error 2:
     →(62)
    Tangle Test semantic error 3:
     →(64)
    Tangle Test semantic error 4:
     →(66)
    Tangle Test semantic error 5:
     →(68)
    Tangle Test semantic error 6:
     →(70)
    Tangle Test superclass to refactor common setup:
     →(61)
    Test of tangle.py:
     →(89)
    Test of weave.py:
     →(88)
    Unit Test Mock Chunk class:
     →(4)
    Unit Test Web class chunk cross-reference:
     →(36)
    Unit Test Web class construction methods:
     →(34)
    Unit Test Web class name resolution methods:
     →(35)
    Unit Test Web class tangle:
     →(37)
    Unit Test Web class weave:
     →(38)
    Unit Test main:→(50)
    Unit Test of Action class hierarchy:
     →(42)
    Unit Test of Application class:
     →(47)
    Unit Test of Chunk class hierarchy:
     →(11)
    Unit Test of Chunk construction:
     →(16)
    Unit Test of Chunk emission:
     →(18)
    Unit Test of Chunk interrogation:
     →(17)
    Unit Test of Chunk superclass:
     →(12) →(13) →(14) →(15)
    Unit Test of CodeCommand class to contain a program source code block:
     →(26)
    Unit Test of Command class hierarchy:
     →(23)
    Unit Test of Command superclass:
     →(24)
    Unit Test of Emitter Superclass:
     →(3)
    Unit Test of Emitter class hierarchy:
     →(2)
    Unit Test of FileXrefCommand class for an output file cross-reference:
     →(28)
    Unit Test of HTML subclass of Emitter:
     →(7)
    Unit Test of HTMLShort subclass of Emitter:
     →(8)
    Unit Test of LaTeX subclass of Emitter:
     →(6)
    Unit Test of MacroXrefCommand class for a named chunk cross-reference:
     →(29)
    Unit Test of NamedChunk subclass:
     →(19)
    Unit Test of NamedChunk_Noindent subclass:
     →(20)
    Unit Test of NamedDocumentChunk subclass:
     →(22)
    Unit Test of OutputChunk subclass:
     →(21)
    Unit Test of Reference class hierarchy:
     →(32)
    Unit Test of ReferenceCommand class for chunk references:
     →(31)
    Unit Test of Tangler subclass of Emitter:
     →(9)
    Unit Test of TanglerMake subclass of Emitter:
     →(10)
    Unit Test of TextCommand class to contain a document text block:
     →(25)
    Unit Test of UserIdXrefCommand class for a user identifier cross-reference:
     →(30)
    Unit Test of Weaver subclass of Emitter:
     →(5)
    Unit Test of Web class:
     →(33)
    Unit Test of WebReader class:
     →(39) →(40) →(41)
    Unit Test of XrefCommand superclass for all cross-reference commands:
     →(27)
    Unit Test overheads:
     imports, etc.: +→(48) →(49)
    Unit test of Action Sequence class:
     →(43)
    Unit test of LoadAction class:
     →(46)
    Unit test of TangleAction class:
     →(45)
    Unit test of WeaverAction class:
     →(44)
    Weave Test evaluation of expressions:
     →(81)
    Weave Test main program:
     →(84)
    Weave Test overheads:
     imports, etc.: +→(83)
    Weave Test references and definitions:
     →(78)
    Weave Test superclass to refactor common setup:
     →(77)
    +
    +
    +

    User Identifiers

    +

    (None)

    +
    +
    +Created by src/pyweb.py at Mon Jun 13 13:20:15 2022.
    +

    Source tests/pyweb_test.w modified Sun Jun 12 19:10:38 2022.

    +
    +

    pyweb.__version__ '3.1'.

    +

    Working directory '/Users/slott/Documents/Projects/py-web-tool'.

    +
    +
    +
    +
    + + diff --git a/tests/pyweb_test.rst b/tests/pyweb_test.rst new file mode 100644 index 0000000..5ce40d4 --- /dev/null +++ b/tests/pyweb_test.rst @@ -0,0 +1,3571 @@ +############################################ +pyWeb Literate Programming 3.1 - Test Suite +############################################ + + +================================================= +Yet Another Literate Programming Tool +================================================= + +.. include:: +.. include:: + +.. contents:: + + +Introduction +============ + +.. test/intro.w + +There are two levels of testing in this document. + +- `Unit Testing`_ + +- `Functional Testing`_ + +Other testing, like performance or security, is possible. +But for this application, not very interesting. + +This doument builds a complete test suite, ``test.py``. + +.. parsed-literal:: + + MacBookPro-SLott:test slott$ python3.3 ../pyweb.py pyweb_test.w + INFO:Application:Setting root log level to 'INFO' + INFO:Application:Setting command character to '@' + INFO:Application:Weaver RST + INFO:Application:load, tangle and weave 'pyweb_test.w' + INFO:LoadAction:Starting Load + INFO:WebReader:Including 'intro.w' + WARNING:WebReader:Unknown @-command in input: "@'" + INFO:WebReader:Including 'unit.w' + INFO:WebReader:Including 'func.w' + INFO:WebReader:Including 'combined.w' + INFO:TangleAction:Starting Tangle + INFO:TanglerMake:Tangling 'test_unit.py' + INFO:TanglerMake:No change to 'test_unit.py' + INFO:TanglerMake:Tangling 'test_loader.py' + INFO:TanglerMake:No change to 'test_loader.py' + INFO:TanglerMake:Tangling 'test.py' + INFO:TanglerMake:No change to 'test.py' + INFO:TanglerMake:Tangling 'page-layout.css' + INFO:TanglerMake:No change to 'page-layout.css' + INFO:TanglerMake:Tangling 'docutils.conf' + INFO:TanglerMake:No change to 'docutils.conf' + INFO:TanglerMake:Tangling 'test_tangler.py' + INFO:TanglerMake:No change to 'test_tangler.py' + INFO:TanglerMake:Tangling 'test_weaver.py' + INFO:TanglerMake:No change to 'test_weaver.py' + INFO:WeaveAction:Starting Weave + INFO:RST:Weaving 'pyweb_test.rst' + INFO:RST:Wrote 3173 lines to 'pyweb_test.rst' + INFO:WeaveAction:Finished Normally + INFO:Application:Load 1911 lines from 5 files in 0.05 sec., Tangle 138 lines in 0.03 sec., Weave 3173 lines in 0.02 sec. + MacBookPro-SLott:test slott$ PYTHONPATH=.. python3.3 test.py + ERROR:WebReader:At ('test8_inc.tmp', 4): end of input, ('@{', '@[') not found + ERROR:WebReader:Errors in included file test8_inc.tmp, output is incomplete. + .ERROR:WebReader:At ('test1.w', 8): expected ('@{',), found '@o' + ERROR:WebReader:Extra '@{' (possibly missing chunk name) near ('test1.w', 9) + ERROR:WebReader:Extra '@{' (possibly missing chunk name) near ('test1.w', 9) + ............................................................................. + ---------------------------------------------------------------------- + Ran 78 tests in 0.025s + + OK + MacBookPro-SLott:test slott$ rst2html.py pyweb_test.rst pyweb_test.html + + +Unit Testing +============ + +.. test/func.w + +There are several broad areas of unit testing. There are the 34 classes in this application. +However, it isn't really necessary to test everyone single one of these classes. +We'll decompose these into several hierarchies. + + +- Emitters + + class Emitter: + + class Weaver(Emitter): + + class LaTeX(Weaver): + + class HTML(Weaver): + + class HTMLShort(HTML): + + class Tangler(Emitter): + + class TanglerMake(Tangler): + + +- Structure: Chunk, Command + + class Chunk: + + class NamedChunk(Chunk): + + class NamedChunk_Noindent(Chunk): + + class OutputChunk(NamedChunk): + + class NamedDocumentChunk(NamedChunk): + + class Command: + + class TextCommand(Command): + + class CodeCommand(TextCommand): + + class XrefCommand(Command): + + class FileXrefCommand(XrefCommand): + + class MacroXrefCommand(XrefCommand): + + class UserIdXrefCommand(XrefCommand): + + class ReferenceCommand(Command): + + +- class Error(Exception): + +- Reference Handling + + class Reference: + + class SimpleReference(Reference): + + class TransitiveReference(Reference): + + +- class Web: + +- class WebReader: + + class Tokenizer: + + class OptionParser: + +- Action + + class Action: + + class ActionSequence(Action): + + class WeaveAction(Action): + + class TangleAction(Action): + + class LoadAction(Action): + + +- class Application: + +- class MyWeaver(HTML): + +- class MyHTML(pyweb.HTML): + + +This gives us the following outline for unit testing. + + +.. _`1`: +.. rubric:: test_unit.py (1) = +.. parsed-literal:: + :class: code + + |srarr|\ Unit Test overheads: imports, etc. (`48`_), |srarr|\ (`49`_) + |srarr|\ Unit Test of Emitter class hierarchy (`2`_) + |srarr|\ Unit Test of Chunk class hierarchy (`11`_) + |srarr|\ Unit Test of Command class hierarchy (`23`_) + |srarr|\ Unit Test of Reference class hierarchy (`32`_) + |srarr|\ Unit Test of Web class (`33`_) + |srarr|\ Unit Test of WebReader class (`39`_), |srarr|\ (`40`_), |srarr|\ (`41`_) + |srarr|\ Unit Test of Action class hierarchy (`42`_) + |srarr|\ Unit Test of Application class (`47`_) + |srarr|\ Unit Test main (`50`_) + +.. + + .. class:: small + + |loz| *test_unit.py (1)*. + + +Emitter Tests +------------- + +The emitter class hierarchy produces output files; either woven output +which uses templates to generate proper markup, or tangled output which +precisely follows the document structure. + + + +.. _`2`: +.. rubric:: Unit Test of Emitter class hierarchy (2) = +.. parsed-literal:: + :class: code + + + |srarr|\ Unit Test Mock Chunk class (`4`_) + |srarr|\ Unit Test of Emitter Superclass (`3`_) + |srarr|\ Unit Test of Weaver subclass of Emitter (`5`_) + |srarr|\ Unit Test of LaTeX subclass of Emitter (`6`_) + |srarr|\ Unit Test of HTML subclass of Emitter (`7`_) + |srarr|\ Unit Test of HTMLShort subclass of Emitter (`8`_) + |srarr|\ Unit Test of Tangler subclass of Emitter (`9`_) + |srarr|\ Unit Test of TanglerMake subclass of Emitter (`10`_) + +.. + + .. class:: small + + |loz| *Unit Test of Emitter class hierarchy (2)*. Used by: test_unit.py (`1`_) + + +The Emitter superclass is designed to be extended. The test +creates a subclass to exercise a few key features. The default +emitter is Tangler-like. + + +.. _`3`: +.. rubric:: Unit Test of Emitter Superclass (3) = +.. parsed-literal:: + :class: code + + + class EmitterExtension(pyweb.Emitter): + def doOpen(self) -> None: + self.theFile = io.StringIO() + def doClose(self) -> None: + self.theFile.flush() + + class TestEmitter(unittest.TestCase): + def setUp(self) -> None: + self.emitter = EmitterExtension() + def test\_emitter\_should\_open\_close\_write(self) -> None: + self.emitter.open(Path("test.tmp")) + self.emitter.write("Something") + self.emitter.close() + self.assertEqual("Something", self.emitter.theFile.getvalue()) + def test\_emitter\_should\_codeBlock(self) -> None: + self.emitter.open(Path("test.tmp")) + self.emitter.codeBlock("Some") + self.emitter.codeBlock(" Code") + self.emitter.close() + self.assertEqual("Some Code\\n", self.emitter.theFile.getvalue()) + def test\_emitter\_should\_indent(self) -> None: + self.emitter.open(Path("test.tmp")) + self.emitter.codeBlock("Begin\\n") + self.emitter.addIndent(4) + self.emitter.codeBlock("More Code\\n") + self.emitter.clrIndent() + self.emitter.codeBlock("End") + self.emitter.close() + self.assertEqual("Begin\\n More Code\\nEnd\\n", self.emitter.theFile.getvalue()) + def test\_emitter\_should\_noindent(self) -> None: + self.emitter.open(Path("test.tmp")) + self.emitter.codeBlock("Begin\\n") + self.emitter.setIndent(0) + self.emitter.codeBlock("More Code\\n") + self.emitter.clrIndent() + self.emitter.codeBlock("End") + self.emitter.close() + self.assertEqual("Begin\\nMore Code\\nEnd\\n", self.emitter.theFile.getvalue()) + +.. + + .. class:: small + + |loz| *Unit Test of Emitter Superclass (3)*. Used by: Unit Test of Emitter class hierarchy... (`2`_) + + +A mock Chunk is a Chunk-like object that we can use to test Weavers. + +Some tests will create multiple chunks. To keep their state separate, +we define a function to return each mocked ``Chunk`` instance as a new Mock +object. The overall ``MockChunk`` class, uses a side effect to +invoke the the ``mock_chunk_instance()`` function. + +The ``write_closure()`` is a function that calls the ``Tangler.write()`` +method. This is *not* consistent with best unit testing practices. +It is merely a hold-over from an older testing strategy. The mock call +history to the ``tangle()`` method of each ``Chunk`` instance is a better +test strategy. + + + +.. _`4`: +.. rubric:: Unit Test Mock Chunk class (4) = +.. parsed-literal:: + :class: code + + + def mock\_chunk\_instance(name: str, seq: int, lineNumber: int) -> Mock: + def write\_closure(aWeb: pyweb.Web, aTangler: pyweb.Tangler) -> None: + aTangler.write(name) + + chunk = Mock( + wraps=pyweb.Chunk, + fullName=name, + seq=seq, + lineNumber=lineNumber, + initial=True, + commands=[], + referencedBy=[], + references=Mock(return\_value=[]), + reference\_indent=Mock(), + reference\_dedent=Mock(), + tangle=Mock(side\_effect=write\_closure) + ) + chunk.name=name + return chunk + + MockChunk = Mock( + name="Chunk class", + side\_effect=mock\_chunk\_instance + ) + +.. + + .. class:: small + + |loz| *Unit Test Mock Chunk class (4)*. Used by: Unit Test of Emitter class hierarchy... (`2`_) + + +The default Weaver is an Emitter that uses templates to produce RST markup. + + +.. _`5`: +.. rubric:: Unit Test of Weaver subclass of Emitter (5) = +.. parsed-literal:: + :class: code + + + class TestWeaver(unittest.TestCase): + def setUp(self) -> None: + self.weaver = pyweb.Weaver() + self.weaver.reference\_style = pyweb.SimpleReference() + self.filepath = Path("testweaver") + self.aFileChunk = MockChunk("File", 123, 456) + self.aFileChunk.referencedBy = [] + self.aChunk = MockChunk("Chunk", 314, 278) + self.aChunk.referencedBy = [self.aFileChunk] + self.aChunk.references.return\_value=[(self.aFileChunk.name, self.aFileChunk.seq)] + + def tearDown(self) -> None: + try: + self.filepath.with\_suffix('.rst').unlink() + except OSError: + pass + + def test\_weaver\_functions\_generic(self) -> None: + result = self.weaver.quote("\|char\| \`code\` \*em\* \_em\_") + self.assertEqual(r"\\\|char\\\| \\\`code\\\` \\\*em\\\* \\\_em\\\_", result) + result = self.weaver.references(self.aChunk) + self.assertEqual("File (\`123\`\_)", result) + result = self.weaver.referenceTo("Chunk", 314) + self.assertEqual(r"\|srarr\|\\ Chunk (\`314\`\_)", result) + self.assertEqual(self.aFileChunk.mock\_calls, []) + self.assertEqual(self.aChunk.mock\_calls, [call.references(self.weaver)]) + + def test\_weaver\_should\_codeBegin(self) -> None: + self.weaver.open(self.filepath) + self.weaver.addIndent() + self.weaver.codeBegin(self.aChunk) + self.weaver.codeBlock(self.weaver.quote("\*The\* \`Code\`\\n")) + self.weaver.clrIndent() + self.weaver.codeEnd(self.aChunk) + self.weaver.close() + txt = self.filepath.with\_suffix(".rst").read\_text() + self.assertEqual("\\n.. \_\`314\`:\\n.. rubric:: Chunk (314) =\\n.. parsed-literal::\\n :class: code\\n\\n \\\\\*The\\\\\* \\\\\`Code\\\\\`\\n\\n..\\n\\n .. class:: small\\n\\n \|loz\| \*Chunk (314)\*. Used by: File (\`123\`\_)\\n", txt) + + def test\_weaver\_should\_fileBegin(self) -> None: + self.weaver.open(self.filepath) + self.weaver.fileBegin(self.aFileChunk) + self.weaver.codeBlock(self.weaver.quote("\*The\* \`Code\`\\n")) + self.weaver.fileEnd(self.aFileChunk) + self.weaver.close() + txt = self.filepath.with\_suffix(".rst").read\_text() + self.assertEqual("\\n.. \_\`123\`:\\n.. rubric:: File (123) =\\n.. parsed-literal::\\n :class: code\\n\\n \\\\\*The\\\\\* \\\\\`Code\\\\\`\\n\\n..\\n\\n .. class:: small\\n\\n \|loz\| \*File (123)\*.\\n", txt) + + def test\_weaver\_should\_xref(self) -> None: + self.weaver.open(self.filepath) + self.weaver.xrefHead( ) + self.weaver.xrefLine("Chunk", [ ("Container", 123) ]) + self.weaver.xrefFoot( ) + #self.weaver.fileEnd(self.aFileChunk) # Why? + self.weaver.close() + txt = self.filepath.with\_suffix(".rst").read\_text() + self.assertEqual("\\n:Chunk:\\n \|srarr\|\\\\ (\`('Container', 123)\`\_)\\n\\n", txt) + + def test\_weaver\_should\_xref\_def(self) -> None: + self.weaver.open(self.filepath) + self.weaver.xrefHead( ) + # Seems to have changed to a simple list of lines?? + self.weaver.xrefDefLine("Chunk", 314, [ 123, 567 ]) + self.weaver.xrefFoot( ) + #self.weaver.fileEnd(self.aFileChunk) # Why? + self.weaver.close() + txt = self.filepath.with\_suffix(".rst").read\_text() + self.assertEqual("\\n:Chunk:\\n \`123\`\_ [\`314\`\_] \`567\`\_\\n\\n", txt) + +.. + + .. class:: small + + |loz| *Unit Test of Weaver subclass of Emitter (5)*. Used by: Unit Test of Emitter class hierarchy... (`2`_) + + +A significant fraction of the various subclasses of weaver are simply +expansion of templates. There's no real point in testing the template +expansion, since that's more easily tested by running a document +through pyweb and looking at the results. + +We'll examine a few features of the LaTeX templates. + + +.. _`6`: +.. rubric:: Unit Test of LaTeX subclass of Emitter (6) = +.. parsed-literal:: + :class: code + + + class TestLaTeX(unittest.TestCase): + def setUp(self) -> None: + self.weaver = pyweb.LaTeX() + self.weaver.reference\_style = pyweb.SimpleReference() + self.filepath = Path("testweaver") + self.aFileChunk = MockChunk("File", 123, 456) + self.aFileChunk.referencedBy = [ ] + self.aChunk = MockChunk("Chunk", 314, 278) + self.aChunk.referencedBy = [self.aFileChunk,] + self.aChunk.references.return\_value=[(self.aFileChunk.name, self.aFileChunk.seq)] + + def tearDown(self) -> None: + try: + self.filepath.with\_suffix(".tex").unlink() + except OSError: + pass + + def test\_weaver\_functions\_latex(self) -> None: + result = self.weaver.quote("\\\\end{Verbatim}") + self.assertEqual("\\\\end\\\\,{Verbatim}", result) + result = self.weaver.references(self.aChunk) + expected = textwrap.indent( + textwrap.dedent(""" + \\\\footnotesize + Used by: + \\\\begin{list}{}{} + + \\\\item Code example File (123) (Sect. \\\\ref{pyweb123}, p. \\\\pageref{pyweb123}) + + \\\\end{list} + \\\\normalsize + """), + ' ') + self.assertEqual(rstrip\_lines(expected), rstrip\_lines(result)) + result = self.weaver.referenceTo("Chunk", 314) + self.assertEqual("$\\\\triangleright$ Code Example Chunk (314)", result) + self.assertEqual(self.aFileChunk.mock\_calls, []) + self.assertEqual(self.aChunk.mock\_calls, [call.references(self.weaver)]) + +.. + + .. class:: small + + |loz| *Unit Test of LaTeX subclass of Emitter (6)*. Used by: Unit Test of Emitter class hierarchy... (`2`_) + + +We'll examine a few features of the HTML templates. + + +.. _`7`: +.. rubric:: Unit Test of HTML subclass of Emitter (7) = +.. parsed-literal:: + :class: code + + + class TestHTML(unittest.TestCase): + def setUp(self) -> None: + self.weaver = pyweb.HTML( ) + self.weaver.reference\_style = pyweb.SimpleReference() + self.filepath = Path("testweaver") + self.aFileChunk = MockChunk("File", 123, 456) + self.aFileChunk.referencedBy = [] + self.aChunk = MockChunk("Chunk", 314, 278) + self.aChunk.referencedBy = [self.aFileChunk,] + self.aChunk.references.return\_value=[(self.aFileChunk.name, self.aFileChunk.seq)] + + def tearDown(self) -> None: + try: + self.filepath.with\_suffix(".html").unlink() + except OSError: + pass + + def test\_weaver\_functions\_html(self) -> None: + result = self.weaver.quote("a < b && c > d") + self.assertEqual("a < b && c > d", result) + result = self.weaver.references(self.aChunk) + self.assertEqual(' Used by File (123).', result) + result = self.weaver.referenceTo("Chunk", 314) + self.assertEqual('Chunk (314)', result) + self.assertEqual(self.aFileChunk.mock\_calls, []) + self.assertEqual(self.aChunk.mock\_calls, [call.references(self.weaver)]) + + +.. + + .. class:: small + + |loz| *Unit Test of HTML subclass of Emitter (7)*. Used by: Unit Test of Emitter class hierarchy... (`2`_) + + +The unique feature of the ``HTMLShort`` class is a template change. + + **TODO:** Test ``HTMLShort``. + + +.. _`8`: +.. rubric:: Unit Test of HTMLShort subclass of Emitter (8) = +.. parsed-literal:: + :class: code + + # TODO: Finish this +.. + + .. class:: small + + |loz| *Unit Test of HTMLShort subclass of Emitter (8)*. Used by: Unit Test of Emitter class hierarchy... (`2`_) + + +A Tangler emits the various named source files in proper format for the desired +compiler and language. + + +.. _`9`: +.. rubric:: Unit Test of Tangler subclass of Emitter (9) = +.. parsed-literal:: + :class: code + + + class TestTangler(unittest.TestCase): + def setUp(self) -> None: + self.tangler = pyweb.Tangler() + self.filepath = Path("testtangler.code") + self.aFileChunk = MockChunk("File", 123, 456) + #self.aFileChunk.references\_list = [ ] + self.aChunk = MockChunk("Chunk", 314, 278) + #self.aChunk.references\_list = [ ("Container", 123) ] + def tearDown(self) -> None: + try: + self.filepath.unlink() + except FileNotFoundError: + pass + + def test\_tangler\_functions(self) -> None: + result = self.tangler.quote(string.printable) + self.assertEqual(string.printable, result) + + def test\_tangler\_should\_codeBegin(self) -> None: + self.tangler.open(self.filepath) + self.tangler.codeBegin(self.aChunk) + self.tangler.codeBlock(self.tangler.quote("\*The\* \`Code\`\\n")) + self.tangler.codeEnd(self.aChunk) + self.tangler.close() + txt = self.filepath.read\_text() + self.assertEqual("\*The\* \`Code\`\\n", txt) + +.. + + .. class:: small + + |loz| *Unit Test of Tangler subclass of Emitter (9)*. Used by: Unit Test of Emitter class hierarchy... (`2`_) + + +A TanglerMake uses a cheap hack to see if anything changed. +It creates a temporary file and then does a complete (slow, expensive) file difference +check. If the file is different, the old version is replaced with +the new version. If the file content is the same, the old version +is left intact with all of the operating system creation timestamps +untouched. + + + + + +.. _`10`: +.. rubric:: Unit Test of TanglerMake subclass of Emitter (10) = +.. parsed-literal:: + :class: code + + + class TestTanglerMake(unittest.TestCase): + def setUp(self) -> None: + self.tangler = pyweb.TanglerMake() + self.filepath = Path("testtangler.code") + self.aChunk = MockChunk("Chunk", 314, 278) + #self.aChunk.references\_list = [("Container", 123)] + self.tangler.open(self.filepath) + self.tangler.codeBegin(self.aChunk) + self.tangler.codeBlock(self.tangler.quote("\*The\* \`Code\`\\n")) + self.tangler.codeEnd(self.aChunk) + self.tangler.close() + self.time\_original = self.filepath.stat().st\_mtime + self.original = self.filepath.stat() + + def tearDown(self) -> None: + try: + self.filepath.unlink() + except OSError: + pass + + def test\_same\_should\_leave(self) -> None: + self.tangler.open(self.filepath) + self.tangler.codeBegin(self.aChunk) + self.tangler.codeBlock(self.tangler.quote("\*The\* \`Code\`\\n")) + self.tangler.codeEnd(self.aChunk) + self.tangler.close() + self.assertTrue(os.path.samestat(self.original, self.filepath.stat())) + #self.assertEqual(self.time\_original, self.filepath.stat().st\_mtime) + + def test\_different\_should\_update(self) -> None: + self.tangler.open(self.filepath) + self.tangler.codeBegin(self.aChunk) + self.tangler.codeBlock(self.tangler.quote("\*Completely Different\* \`Code\`\\n")) + self.tangler.codeEnd(self.aChunk) + self.tangler.close() + self.assertFalse(os.path.samestat(self.original, self.filepath.stat())) + #self.assertNotEqual(self.time\_original, self.filepath.stat().st\_mtime) + +.. + + .. class:: small + + |loz| *Unit Test of TanglerMake subclass of Emitter (10)*. Used by: Unit Test of Emitter class hierarchy... (`2`_) + + +Chunk Tests +------------ + +The Chunk and Command class hierarchies model the input document -- the web +of chunks that are used to produce the documentation and the source files. + + + +.. _`11`: +.. rubric:: Unit Test of Chunk class hierarchy (11) = +.. parsed-literal:: + :class: code + + + |srarr|\ Unit Test of Chunk superclass (`12`_), |srarr|\ (`13`_), |srarr|\ (`14`_), |srarr|\ (`15`_) + |srarr|\ Unit Test of NamedChunk subclass (`19`_) + |srarr|\ Unit Test of NamedChunk_Noindent subclass (`20`_) + |srarr|\ Unit Test of OutputChunk subclass (`21`_) + |srarr|\ Unit Test of NamedDocumentChunk subclass (`22`_) + +.. + + .. class:: small + + |loz| *Unit Test of Chunk class hierarchy (11)*. Used by: test_unit.py (`1`_) + + +In order to test the Chunk superclass, we need several mock objects. +A Chunk contains one or more commands. A Chunk is a part of a Web. +Also, a Chunk is processed by a Tangler or a Weaver. We'll need +mock objects for all of these relationships in which a Chunk participates. + +A MockCommand can be attached to a Chunk. + + +.. _`12`: +.. rubric:: Unit Test of Chunk superclass (12) = +.. parsed-literal:: + :class: code + + + MockCommand = Mock( + name="Command class", + side\_effect=lambda: Mock( + name="Command instance", + # text="", # Only used for TextCommand. + lineNumber=314, + startswith=Mock(return\_value=False) + ) + ) + +.. + + .. class:: small + + |loz| *Unit Test of Chunk superclass (12)*. Used by: Unit Test of Chunk class hierarchy... (`11`_) + + +A MockWeb can contain a Chunk. + + +.. _`13`: +.. rubric:: Unit Test of Chunk superclass (13) += +.. parsed-literal:: + :class: code + + + + def mock\_web\_instance() -> Mock: + web = Mock( + name="Web instance", + chunks=[], + add=Mock(return\_value=None), + addNamed=Mock(return\_value=None), + addOutput=Mock(return\_value=None), + fullNameFor=Mock(side\_effect=lambda name: name), + fileXref=Mock(return\_value={'file': [1,2,3]}), + chunkXref=Mock(return\_value={'chunk': [4,5,6]}), + userNamesXref=Mock(return\_value={'name': (7, [8,9,10])}), + getchunk=Mock(side\_effect=lambda name: [MockChunk(name, 1, 314)]), + createUsedBy=Mock(), + weaveChunk=Mock(side\_effect=lambda name, weaver: weaver.write(name)), + weave=Mock(return\_value=None), + tangle=Mock(return\_value=None), + ) + return web + + MockWeb = Mock( + name="Web class", + side\_effect=mock\_web\_instance + ) + +.. + + .. class:: small + + |loz| *Unit Test of Chunk superclass (13)*. Used by: Unit Test of Chunk class hierarchy... (`11`_) + + +A MockWeaver or MockTangler appear to process a Chunk. +We can interrogate the ``mock_calls`` to be sure the right things were done. + +We need to permit ``__enter__()`` and ``__exit__()``, +which leads to a multi-step instance. +The initial instance with ``__enter__()`` that +returns the context manager instance. + + + +.. _`14`: +.. rubric:: Unit Test of Chunk superclass (14) += +.. parsed-literal:: + :class: code + + + def mock\_weaver\_instance() -> MagicMock: + context = MagicMock( + name="Weaver instance context", + \_\_exit\_\_=Mock() + ) + + weaver = MagicMock( + name="Weaver instance", + quote=Mock(return\_value="quoted"), + \_\_enter\_\_=Mock(return\_value=context) + ) + return weaver + + MockWeaver = Mock( + name="Weaver class", + side\_effect=mock\_weaver\_instance + ) + + def mock\_tangler\_instance() -> MagicMock: + context = MagicMock( + name="Tangler instance context", + \_\_exit\_\_=Mock() + ) + + tangler = MagicMock( + name="Tangler instance", + \_\_enter\_\_=Mock(return\_value=context) + ) + return tangler + + MockTangler = Mock( + name="Tangler class", + side\_effect=mock\_tangler\_instance + ) + + +.. + + .. class:: small + + |loz| *Unit Test of Chunk superclass (14)*. Used by: Unit Test of Chunk class hierarchy... (`11`_) + + +A Chunk is built, interrogated and then emitted. + + +.. _`15`: +.. rubric:: Unit Test of Chunk superclass (15) += +.. parsed-literal:: + :class: code + + + class TestChunk(unittest.TestCase): + def setUp(self) -> None: + self.theChunk = pyweb.Chunk() + + |srarr|\ Unit Test of Chunk construction (`16`_) + + |srarr|\ Unit Test of Chunk interrogation (`17`_) + + |srarr|\ Unit Test of Chunk emission (`18`_) + +.. + + .. class:: small + + |loz| *Unit Test of Chunk superclass (15)*. Used by: Unit Test of Chunk class hierarchy... (`11`_) + + +Can we build a Chunk? + + +.. _`16`: +.. rubric:: Unit Test of Chunk construction (16) = +.. parsed-literal:: + :class: code + + + def test\_append\_command\_should\_work(self) -> None: + cmd1 = MockCommand() + self.theChunk.append(cmd1) + self.assertEqual(1, len(self.theChunk.commands)) + self.assertEqual(cmd1.chunk, self.theChunk) + + cmd2 = MockCommand() + self.theChunk.append(cmd2) + self.assertEqual(2, len(self.theChunk.commands)) + self.assertEqual(cmd2.chunk, self.theChunk) + + def test\_append\_initial\_and\_more\_text\_should\_work(self) -> None: + self.theChunk.appendText("hi mom") + self.assertEqual(1, len(self.theChunk.commands)) + self.theChunk.appendText("&more text") + self.assertEqual(1, len(self.theChunk.commands)) + self.assertEqual("hi mom&more text", self.theChunk.commands[0].text) + + def test\_append\_following\_text\_should\_work(self) -> None: + cmd1 = MockCommand() + self.theChunk.append(cmd1) + self.theChunk.appendText("hi mom") + self.assertEqual(2, len(self.theChunk.commands)) + assert cmd1.chunk == self.theChunk + + def test\_append\_chunk\_to\_web\_should\_work(self) -> None: + web = MockWeb() + self.theChunk.webAdd(web) + self.assertEqual(web.add.mock\_calls, [call(self.theChunk)]) + +.. + + .. class:: small + + |loz| *Unit Test of Chunk construction (16)*. Used by: Unit Test of Chunk superclass... (`15`_) + + +Can we interrogate a Chunk? + + +.. _`17`: +.. rubric:: Unit Test of Chunk interrogation (17) = +.. parsed-literal:: + :class: code + + + def test\_leading\_command\_should\_not\_find(self) -> None: + self.assertFalse(self.theChunk.startswith("hi mom")) + cmd1 = MockCommand() + self.theChunk.append(cmd1) + self.assertFalse(self.theChunk.startswith("hi mom")) + self.theChunk.appendText("hi mom") + self.assertEqual(2, len(self.theChunk.commands) ) + self.assertFalse(self.theChunk.startswith("hi mom")) + + def test\_leading\_text\_should\_not\_find(self) -> None: + self.assertFalse(self.theChunk.startswith("hi mom")) + self.theChunk.appendText("hi mom") + self.assertTrue(self.theChunk.startswith("hi mom")) + cmd1 = MockCommand() + self.theChunk.append(cmd1) + self.assertTrue(self.theChunk.startswith("hi mom")) + self.assertEqual(2, len(self.theChunk.commands) ) + + def test\_regexp\_exists\_should\_find(self) -> None: + self.theChunk.appendText("this chunk has many words") + pat = re.compile(r"\\Wchunk\\W") + found = self.theChunk.searchForRE(pat) + self.assertTrue(found is self.theChunk) + + def test\_regexp\_missing\_should\_not\_find(self): + self.theChunk.appendText("this chunk has many words") + pat = re.compile(r"\\Warpigs\\W") + found = self.theChunk.searchForRE(pat) + self.assertTrue(found is None) + + def test\_lineNumber\_should\_work(self) -> None: + self.assertTrue(self.theChunk.lineNumber is None) + cmd1 = MockCommand() + self.theChunk.append(cmd1) + self.assertEqual(314, self.theChunk.lineNumber) + +.. + + .. class:: small + + |loz| *Unit Test of Chunk interrogation (17)*. Used by: Unit Test of Chunk superclass... (`15`_) + + +Can we emit a Chunk with a weaver or tangler? + + +.. _`18`: +.. rubric:: Unit Test of Chunk emission (18) = +.. parsed-literal:: + :class: code + + + def test\_weave\_chunk\_should\_work(self) -> None: + wvr = MockWeaver() + web = MockWeb() + self.theChunk.appendText("this chunk has very & many words") + self.theChunk.weave(web, wvr) + self.assertEqual(wvr.docBegin.mock\_calls, [call(self.theChunk)]) + self.assertEqual(wvr.write.mock\_calls, [call("this chunk has very & many words")]) + self.assertEqual(wvr.docEnd.mock\_calls, [call(self.theChunk)]) + + def test\_tangle\_should\_fail(self) -> None: + tnglr = MockTangler() + web = MockWeb() + self.theChunk.appendText("this chunk has very & many words") + try: + self.theChunk.tangle(web, tnglr) + self.fail() + except pyweb.Error as e: + self.assertEqual("Cannot tangle an anonymous chunk", e.args[0]) + +.. + + .. class:: small + + |loz| *Unit Test of Chunk emission (18)*. Used by: Unit Test of Chunk superclass... (`15`_) + + +The ``NamedChunk`` is created by a ``@d`` command. +Since it's named, it appears in the Web's index. Also, it is woven +and tangled differently than anonymous chunks. + + +.. _`19`: +.. rubric:: Unit Test of NamedChunk subclass (19) = +.. parsed-literal:: + :class: code + + + class TestNamedChunk(unittest.TestCase): + def setUp(self) -> None: + self.theChunk = pyweb.NamedChunk("Some Name...") + cmd = self.theChunk.makeContent("the words & text of this Chunk") + self.theChunk.append(cmd) + self.theChunk.setUserIDRefs("index terms") + + def test\_should\_find\_xref\_words(self) -> None: + self.assertEqual(2, len(self.theChunk.getUserIDRefs())) + self.assertEqual("index", self.theChunk.getUserIDRefs()[0]) + self.assertEqual("terms", self.theChunk.getUserIDRefs()[1]) + + def test\_append\_named\_chunk\_to\_web\_should\_work(self) -> None: + web = MockWeb() + self.theChunk.webAdd(web) + self.assertEqual(web.addNamed.mock\_calls, [call(self.theChunk)]) + + def test\_weave\_should\_work(self) -> None: + wvr = MockWeaver() + web = MockWeb() + self.theChunk.weave(web, wvr) + self.assertEqual(wvr.codeBegin.mock\_calls, [call(self.theChunk)]) + self.assertEqual(wvr.quote.mock\_calls, [call('the words & text of this Chunk')]) + self.assertEqual(wvr.codeBlock.mock\_calls, [call('quoted')]) + self.assertEqual(wvr.codeEnd.mock\_calls, [call(self.theChunk)]) + + def test\_tangle\_should\_work(self) -> None: + tnglr = MockTangler() + web = MockWeb() + self.theChunk.tangle(web, tnglr) + self.assertEqual(tnglr.codeBegin.mock\_calls, [call(self.theChunk)]) + self.assertEqual(tnglr.codeBlock.mock\_calls, [call("the words & text of this Chunk")]) + self.assertEqual(tnglr.codeEnd.mock\_calls, [call(self.theChunk)]) + +.. + + .. class:: small + + |loz| *Unit Test of NamedChunk subclass (19)*. Used by: Unit Test of Chunk class hierarchy... (`11`_) + + + +.. _`20`: +.. rubric:: Unit Test of NamedChunk_Noindent subclass (20) = +.. parsed-literal:: + :class: code + + + class TestNamedChunk\_Noindent(unittest.TestCase): + def setUp(self) -> None: + self.theChunk = pyweb.NamedChunk\_Noindent("NoIndent Name...") + cmd = self.theChunk.makeContent("the words & text of this Chunk") + self.theChunk.append(cmd) + self.theChunk.setUserIDRefs("index terms") + def test\_tangle\_should\_work(self) -> None: + tnglr = MockTangler() + web = MockWeb() + self.theChunk.tangle(web, tnglr) + + self.assertEqual(tnglr.mock\_calls, [ + call.codeBegin(self.theChunk), + call.codeBlock('the words & text of this Chunk'), + call.codeEnd(self.theChunk) + ] + ) + +.. + + .. class:: small + + |loz| *Unit Test of NamedChunk_Noindent subclass (20)*. Used by: Unit Test of Chunk class hierarchy... (`11`_) + + + +The ``OutputChunk`` is created by a ``@o`` command. +Since it's named, it appears in the Web's index. Also, it is woven +and tangled differently than anonymous chunks. + + +.. _`21`: +.. rubric:: Unit Test of OutputChunk subclass (21) = +.. parsed-literal:: + :class: code + + + class TestOutputChunk(unittest.TestCase): + def setUp(self) -> None: + self.theChunk = pyweb.OutputChunk("filename", "#", "") + cmd = self.theChunk.makeContent("the words & text of this Chunk") + self.theChunk.append(cmd) + self.theChunk.setUserIDRefs("index terms") + + def test\_append\_output\_chunk\_to\_web\_should\_work(self) -> None: + web = MockWeb() + self.theChunk.webAdd(web) + self.assertEqual(web.addOutput.mock\_calls, [call(self.theChunk)]) + + def test\_weave\_should\_work(self) -> None: + wvr = MockWeaver() + web = MockWeb() + self.theChunk.weave(web, wvr) + self.assertEqual(wvr.mock\_calls, [ + call.fileBegin(self.theChunk), + call.quote('the words & text of this Chunk'), + call.codeBlock('quoted'), + call.fileEnd(self.theChunk) + ] + ) + + def test\_tangle\_should\_work(self) -> None: + tnglr = MockTangler() + web = MockWeb() + self.theChunk.tangle(web, tnglr) + self.assertEqual(tnglr.mock\_calls, [ + call.codeBegin(self.theChunk), + call.codeBlock('the words & text of this Chunk'), + call.codeEnd(self.theChunk) + ] + ) + +.. + + .. class:: small + + |loz| *Unit Test of OutputChunk subclass (21)*. Used by: Unit Test of Chunk class hierarchy... (`11`_) + + +The ``NamedDocumentChunk`` is a little-used feature. + + **TODO** Test ``NamedDocumentChunk``. + + +.. _`22`: +.. rubric:: Unit Test of NamedDocumentChunk subclass (22) = +.. parsed-literal:: + :class: code + + # TODO Test This +.. + + .. class:: small + + |loz| *Unit Test of NamedDocumentChunk subclass (22)*. Used by: Unit Test of Chunk class hierarchy... (`11`_) + + +Command Tests +--------------- + + +.. _`23`: +.. rubric:: Unit Test of Command class hierarchy (23) = +.. parsed-literal:: + :class: code + + + |srarr|\ Unit Test of Command superclass (`24`_) + |srarr|\ Unit Test of TextCommand class to contain a document text block (`25`_) + |srarr|\ Unit Test of CodeCommand class to contain a program source code block (`26`_) + |srarr|\ Unit Test of XrefCommand superclass for all cross-reference commands (`27`_) + |srarr|\ Unit Test of FileXrefCommand class for an output file cross-reference (`28`_) + |srarr|\ Unit Test of MacroXrefCommand class for a named chunk cross-reference (`29`_) + |srarr|\ Unit Test of UserIdXrefCommand class for a user identifier cross-reference (`30`_) + |srarr|\ Unit Test of ReferenceCommand class for chunk references (`31`_) + +.. + + .. class:: small + + |loz| *Unit Test of Command class hierarchy (23)*. Used by: test_unit.py (`1`_) + + +This Command superclass is essentially an inteface definition, it +has no real testable features. + + +.. _`24`: +.. rubric:: Unit Test of Command superclass (24) = +.. parsed-literal:: + :class: code + + # No Tests +.. + + .. class:: small + + |loz| *Unit Test of Command superclass (24)*. Used by: Unit Test of Command class hierarchy... (`23`_) + + +A TextCommand object must be constructed, interrogated and emitted. + + +.. _`25`: +.. rubric:: Unit Test of TextCommand class to contain a document text block (25) = +.. parsed-literal:: + :class: code + + + class TestTextCommand(unittest.TestCase): + def setUp(self) -> None: + self.cmd = pyweb.TextCommand("Some text & words in the document\\n ", 314) + self.cmd2 = pyweb.TextCommand("No Indent\\n", 314) + def test\_methods\_should\_work(self) -> None: + self.assertTrue(self.cmd.startswith("Some")) + self.assertFalse(self.cmd.startswith("text")) + pat1 = re.compile(r"\\Wthe\\W") + self.assertTrue(self.cmd.searchForRE(pat1) is not None) + pat2 = re.compile(r"\\Wnothing\\W") + self.assertTrue(self.cmd.searchForRE(pat2) is None) + self.assertEqual(4, self.cmd.indent()) + self.assertEqual(0, self.cmd2.indent()) + + def test\_weave\_should\_work(self) -> None: + wvr = MockWeaver() + web = MockWeb() + self.cmd.weave(web, wvr) + self.assertEqual(wvr.write.mock\_calls, [call('Some text & words in the document\\n ')]) + + def test\_tangle\_should\_work(self) -> None: + tnglr = MockTangler() + web = MockWeb() + self.cmd.tangle(web, tnglr) + self.assertEqual(tnglr.write.mock\_calls, [call('Some text & words in the document\\n ')]) + +.. + + .. class:: small + + |loz| *Unit Test of TextCommand class to contain a document text block (25)*. Used by: Unit Test of Command class hierarchy... (`23`_) + + +A CodeCommand object is a TextCommand with different processing for being emitted. + + +.. _`26`: +.. rubric:: Unit Test of CodeCommand class to contain a program source code block (26) = +.. parsed-literal:: + :class: code + + + class TestCodeCommand(unittest.TestCase): + def setUp(self) -> None: + self.cmd = pyweb.CodeCommand("Some text & words in the document\\n ", 314) + + def test\_weave\_should\_work(self) -> None: + wvr = MockWeaver() + web = MockWeb() + self.cmd.weave(web, wvr) + self.assertEqual(wvr.codeBlock.mock\_calls, [call('quoted')]) + + def test\_tangle\_should\_work(self) -> None: + tnglr = MockTangler() + web = MockWeb() + self.cmd.tangle(web, tnglr) + self.assertEqual(tnglr.codeBlock.mock\_calls, [call('Some text & words in the document\\n ')]) + +.. + + .. class:: small + + |loz| *Unit Test of CodeCommand class to contain a program source code block (26)*. Used by: Unit Test of Command class hierarchy... (`23`_) + + +The XrefCommand class is largely abstract. + + +.. _`27`: +.. rubric:: Unit Test of XrefCommand superclass for all cross-reference commands (27) = +.. parsed-literal:: + :class: code + + # No Tests +.. + + .. class:: small + + |loz| *Unit Test of XrefCommand superclass for all cross-reference commands (27)*. Used by: Unit Test of Command class hierarchy... (`23`_) + + +The FileXrefCommand command is expanded by a weaver to a list of ``@o`` +locations. + + +.. _`28`: +.. rubric:: Unit Test of FileXrefCommand class for an output file cross-reference (28) = +.. parsed-literal:: + :class: code + + + class TestFileXRefCommand(unittest.TestCase): + def setUp(self) -> None: + self.cmd = pyweb.FileXrefCommand(314) + + def test\_weave\_should\_work(self) -> None: + wvr = MockWeaver() + web = MockWeb() + self.cmd.weave(web, wvr) + self.assertEqual(wvr.mock\_calls, [call.xrefHead(), call.xrefLine('file', [1, 2, 3]), call.xrefFoot()]) + + def test\_tangle\_should\_fail(self) -> None: + tnglr = MockTangler() + web = MockWeb() + try: + self.cmd.tangle(web, tnglr) + self.fail() + except pyweb.Error: + pass + +.. + + .. class:: small + + |loz| *Unit Test of FileXrefCommand class for an output file cross-reference (28)*. Used by: Unit Test of Command class hierarchy... (`23`_) + + +The MacroXrefCommand command is expanded by a weaver to a list of all ``@d`` +locations. + + +.. _`29`: +.. rubric:: Unit Test of MacroXrefCommand class for a named chunk cross-reference (29) = +.. parsed-literal:: + :class: code + + + class TestMacroXRefCommand(unittest.TestCase): + def setUp(self) -> None: + self.cmd = pyweb.MacroXrefCommand(314) + + def test\_weave\_should\_work(self) -> None: + wvr = MockWeaver() + web = MockWeb() + self.cmd.weave(web, wvr) + self.assertEqual(wvr.mock\_calls, [call.xrefHead(), call.xrefLine('chunk', [4, 5, 6]), call.xrefFoot()]) + + def test\_tangle\_should\_fail(self) -> None: + tnglr = MockTangler() + web = MockWeb() + try: + self.cmd.tangle(web, tnglr) + self.fail() + except pyweb.Error: + pass + +.. + + .. class:: small + + |loz| *Unit Test of MacroXrefCommand class for a named chunk cross-reference (29)*. Used by: Unit Test of Command class hierarchy... (`23`_) + + +The UserIdXrefCommand command is expanded by a weaver to a list of all ``@|`` +names. + + +.. _`30`: +.. rubric:: Unit Test of UserIdXrefCommand class for a user identifier cross-reference (30) = +.. parsed-literal:: + :class: code + + + class TestUserIdXrefCommand(unittest.TestCase): + def setUp(self) -> None: + self.cmd = pyweb.UserIdXrefCommand(314) + + def test\_weave\_should\_work(self) -> None: + wvr = MockWeaver() + web = MockWeb() + self.cmd.weave(web, wvr) + self.assertEqual(wvr.mock\_calls, [call.xrefHead(), call.xrefDefLine('name', 7, [8, 9, 10]), call.xrefFoot()]) + + def test\_tangle\_should\_fail(self) -> None: + tnglr = MockTangler() + web = MockWeb() + try: + self.cmd.tangle(web, tnglr) + self.fail() + except pyweb.Error: + pass + +.. + + .. class:: small + + |loz| *Unit Test of UserIdXrefCommand class for a user identifier cross-reference (30)*. Used by: Unit Test of Command class hierarchy... (`23`_) + + +Reference commands require a context when tangling. +The context helps provide the required indentation. +They can't be simply tangled. + + +.. _`31`: +.. rubric:: Unit Test of ReferenceCommand class for chunk references (31) = +.. parsed-literal:: + :class: code + + + class TestReferenceCommand(unittest.TestCase): + def setUp(self) -> None: + self.chunk = MockChunk("Owning Chunk", 123, 456) + self.cmd = pyweb.ReferenceCommand("Some Name", 314) + self.cmd.chunk = self.chunk + self.chunk.commands.append(self.cmd) + self.chunk.previous\_command = pyweb.TextCommand("", self.chunk.commands[0].lineNumber) + + def test\_weave\_should\_work(self) -> None: + wvr = MockWeaver() + web = MockWeb() + self.cmd.weave(web, wvr) + self.assertEqual(wvr.write.mock\_calls, [call('Some Name')]) + + def test\_tangle\_should\_work(self) -> None: + tnglr = MockTangler() + web = MockWeb() + web.add(self.chunk) + self.cmd.tangle(web, tnglr) + self.assertEqual(tnglr.write.mock\_calls, [call('Some Name')]) + + +.. + + .. class:: small + + |loz| *Unit Test of ReferenceCommand class for chunk references (31)*. Used by: Unit Test of Command class hierarchy... (`23`_) + + +Reference Tests +---------------- + +The Reference class implements one of two search strategies for +cross-references. Either simple (or "immediate") or transitive. + +The superclass is little more than an interface definition, +it's completely abstract. The two subclasses differ in +a single method. + + + +.. _`32`: +.. rubric:: Unit Test of Reference class hierarchy (32) = +.. parsed-literal:: + :class: code + + + class TestReference(unittest.TestCase): + def setUp(self) -> None: + self.web = MockWeb() + self.main = MockChunk("Main", 1, 11) + self.parent = MockChunk("Parent", 2, 22) + self.parent.referencedBy = [ self.main ] + self.chunk = MockChunk("Sub", 3, 33) + self.chunk.referencedBy = [ self.parent ] + def test\_simple\_should\_find\_one(self) -> None: + self.reference = pyweb.SimpleReference() + theList = self.reference.chunkReferencedBy(self.chunk) + self.assertEqual(1, len(theList)) + self.assertEqual(self.parent, theList[0]) + def test\_transitive\_should\_find\_all(self) -> None: + self.reference = pyweb.TransitiveReference() + theList = self.reference.chunkReferencedBy(self.chunk) + self.assertEqual(2, len(theList)) + self.assertEqual(self.parent, theList[0]) + self.assertEqual(self.main, theList[1]) + +.. + + .. class:: small + + |loz| *Unit Test of Reference class hierarchy (32)*. Used by: test_unit.py (`1`_) + + +Web Tests +----------- + +This is more difficult to create mocks for. + + +.. _`33`: +.. rubric:: Unit Test of Web class (33) = +.. parsed-literal:: + :class: code + + + class TestWebConstruction(unittest.TestCase): + def setUp(self) -> None: + self.web = pyweb.Web() + |srarr|\ Unit Test Web class construction methods (`34`_) + + class TestWebProcessing(unittest.TestCase): + def setUp(self) -> None: + self.web = pyweb.Web() + self.web.web\_path = Path("TestWebProcessing.w") + self.chunk = pyweb.Chunk() + self.chunk.appendText("some text") + self.chunk.webAdd(self.web) + self.out = pyweb.OutputChunk("A File") + self.out.appendText("some code") + nm = self.web.addDefName("A Chunk") + self.out.append(pyweb.ReferenceCommand(nm)) + self.out.webAdd(self.web) + self.named = pyweb.NamedChunk("A Chunk...") + self.named.appendText("some user2a code") + self.named.setUserIDRefs("user1") + nm = self.web.addDefName("Another Chunk") + self.named.append(pyweb.ReferenceCommand(nm)) + self.named.webAdd(self.web) + self.named2 = pyweb.NamedChunk("Another Chunk...") + self.named2.appendText("some user1 code") + self.named2.setUserIDRefs("user2a user2b") + self.named2.webAdd(self.web) + |srarr|\ Unit Test Web class name resolution methods (`35`_) + |srarr|\ Unit Test Web class chunk cross-reference (`36`_) + |srarr|\ Unit Test Web class tangle (`37`_) + |srarr|\ Unit Test Web class weave (`38`_) + +.. + + .. class:: small + + |loz| *Unit Test of Web class (33)*. Used by: test_unit.py (`1`_) + + + +.. _`34`: +.. rubric:: Unit Test Web class construction methods (34) = +.. parsed-literal:: + :class: code + + + def test\_names\_definition\_should\_resolve(self) -> None: + name1 = self.web.addDefName("A Chunk...") + self.assertTrue(name1 is None) + self.assertEqual(0, len(self.web.named)) + name2 = self.web.addDefName("A Chunk Of Code") + self.assertEqual("A Chunk Of Code", name2) + self.assertEqual(1, len(self.web.named)) + name3 = self.web.addDefName("A Chunk...") + self.assertEqual("A Chunk Of Code", name3) + self.assertEqual(1, len(self.web.named)) + + def test\_chunks\_should\_add\_and\_index(self) -> None: + chunk = pyweb.Chunk() + chunk.appendText("some text") + chunk.webAdd(self.web) + self.assertEqual(1, len(self.web.chunkSeq)) + self.assertEqual(0, len(self.web.named)) + self.assertEqual(0, len(self.web.output)) + named = pyweb.NamedChunk("A Chunk") + named.appendText("some code") + named.webAdd(self.web) + self.assertEqual(2, len(self.web.chunkSeq)) + self.assertEqual(1, len(self.web.named)) + self.assertEqual(0, len(self.web.output)) + out = pyweb.OutputChunk("A File") + out.appendText("some code") + out.webAdd(self.web) + self.assertEqual(3, len(self.web.chunkSeq)) + self.assertEqual(1, len(self.web.named)) + self.assertEqual(1, len(self.web.output)) + +.. + + .. class:: small + + |loz| *Unit Test Web class construction methods (34)*. Used by: Unit Test of Web class... (`33`_) + + + +.. _`35`: +.. rubric:: Unit Test Web class name resolution methods (35) = +.. parsed-literal:: + :class: code + + + def test\_name\_queries\_should\_resolve(self) -> None: + self.assertEqual("A Chunk", self.web.fullNameFor("A C...")) + self.assertEqual("A Chunk", self.web.fullNameFor("A Chunk")) + self.assertNotEqual("A Chunk", self.web.fullNameFor("A File")) + self.assertTrue(self.named is self.web.getchunk("A C...")[0]) + self.assertTrue(self.named is self.web.getchunk("A Chunk")[0]) + try: + self.assertTrue(None is not self.web.getchunk("A File")) + self.fail() + except pyweb.Error as e: + self.assertTrue(e.args[0].startswith("Cannot resolve 'A File'")) + +.. + + .. class:: small + + |loz| *Unit Test Web class name resolution methods (35)*. Used by: Unit Test of Web class... (`33`_) + + + +.. _`36`: +.. rubric:: Unit Test Web class chunk cross-reference (36) = +.. parsed-literal:: + :class: code + + + def test\_valid\_web\_should\_createUsedBy(self) -> None: + self.web.createUsedBy() + # If it raises an exception, the web structure is damaged + + def test\_valid\_web\_should\_createFileXref(self) -> None: + file\_xref = self.web.fileXref() + self.assertEqual(1, len(file\_xref)) + self.assertTrue("A File" in file\_xref) + self.assertTrue(1, len(file\_xref["A File"])) + + def test\_valid\_web\_should\_createChunkXref(self) -> None: + chunk\_xref = self.web.chunkXref() + self.assertEqual(2, len(chunk\_xref)) + self.assertTrue("A Chunk" in chunk\_xref) + self.assertEqual(1, len(chunk\_xref["A Chunk"])) + self.assertTrue("Another Chunk" in chunk\_xref) + self.assertEqual(1, len(chunk\_xref["Another Chunk"])) + self.assertFalse("Not A Real Chunk" in chunk\_xref) + + def test\_valid\_web\_should\_create\_userNamesXref(self) -> None: + user\_xref = self.web.userNamesXref() + self.assertEqual(3, len(user\_xref)) + self.assertTrue("user1" in user\_xref) + defn, reflist = user\_xref["user1"] + self.assertEqual(1, len(reflist), "did not find user1") + self.assertTrue("user2a" in user\_xref) + defn, reflist = user\_xref["user2a"] + self.assertEqual(1, len(reflist), "did not find user2a") + self.assertTrue("user2b" in user\_xref) + defn, reflist = user\_xref["user2b"] + self.assertEqual(0, len(reflist)) + self.assertFalse("Not A User Symbol" in user\_xref) + +.. + + .. class:: small + + |loz| *Unit Test Web class chunk cross-reference (36)*. Used by: Unit Test of Web class... (`33`_) + + + +.. _`37`: +.. rubric:: Unit Test Web class tangle (37) = +.. parsed-literal:: + :class: code + + + def test\_valid\_web\_should\_tangle(self) -> None: + tangler = MockTangler() + self.web.tangle(tangler) + self.assertEqual(tangler.codeBlock.mock\_calls, [ + call('some code'), + call('some user2a code'), + call('some user1 code'), + ] + ) + +.. + + .. class:: small + + |loz| *Unit Test Web class tangle (37)*. Used by: Unit Test of Web class... (`33`_) + + + +.. _`38`: +.. rubric:: Unit Test Web class weave (38) = +.. parsed-literal:: + :class: code + + + def test\_valid\_web\_should\_weave(self) -> None: + weaver = MockWeaver() + self.web.weave(weaver) + self.assertEqual(weaver.write.mock\_calls, [ + call('some text'), + ] + ) + self.assertEqual(weaver.quote.mock\_calls, [ + call('some code'), + call('some user2a code'), + call('some user1 code'), + ] + ) + +.. + + .. class:: small + + |loz| *Unit Test Web class weave (38)*. Used by: Unit Test of Web class... (`33`_) + + + +WebReader Tests +---------------- + +Generally, this is tested separately through the functional tests. +Those tests each present source files to be processed by the +WebReader. + +We should test this through some clever mocks that produce the +proper sequence of tokens to parse the various kinds of Commands. + + +.. _`39`: +.. rubric:: Unit Test of WebReader class (39) = +.. parsed-literal:: + :class: code + + + # Tested via functional tests + +.. + + .. class:: small + + |loz| *Unit Test of WebReader class (39)*. Used by: test_unit.py (`1`_) + + +Some lower-level units: specifically the tokenizer and the option parser. + + +.. _`40`: +.. rubric:: Unit Test of WebReader class (40) += +.. parsed-literal:: + :class: code + + + class TestTokenizer(unittest.TestCase): + def test\_should\_split\_tokens(self) -> None: + input = io.StringIO("@@ word @{ @[ @< @>\\n@] @} @i @\| @m @f @u\\n") + self.tokenizer = pyweb.Tokenizer(input) + tokens = list(self.tokenizer) + self.assertEqual(24, len(tokens)) + self.assertEqual( ['@@', ' word ', '@{', ' ', '@[', ' ', '@<', ' ', + '@>', '\\n', '@]', ' ', '@}', ' ', '@i', ' ', '@\|', ' ', '@m', ' ', + '@f', ' ', '@u', '\\n'], tokens ) + self.assertEqual(2, self.tokenizer.lineNumber) + +.. + + .. class:: small + + |loz| *Unit Test of WebReader class (40)*. Used by: test_unit.py (`1`_) + + + +.. _`41`: +.. rubric:: Unit Test of WebReader class (41) += +.. parsed-literal:: + :class: code + + + class TestOptionParser\_OutputChunk(unittest.TestCase): + def setUp(self) -> None: + self.option\_parser = pyweb.OptionParser( + pyweb.OptionDef("-start", nargs=1, default=None), + pyweb.OptionDef("-end", nargs=1, default=""), + pyweb.OptionDef("argument", nargs='\*'), + ) + def test\_with\_options\_should\_parse(self) -> None: + text1 = " -start /\* -end \*/ something.css " + options1 = self.option\_parser.parse(text1) + self.assertEqual({'-end': ['\*/'], '-start': ['/\*'], 'argument': ['something.css']}, options1) + def test\_without\_options\_should\_parse(self) -> None: + text2 = " something.py " + options2 = self.option\_parser.parse(text2) + self.assertEqual({'argument': ['something.py']}, options2) + + class TestOptionParser\_NamedChunk(unittest.TestCase): + def setUp(self) -> None: + self.option\_parser = pyweb.OptionParser( pyweb.OptionDef( "-indent", nargs=0), + pyweb.OptionDef("-noindent", nargs=0), + pyweb.OptionDef("argument", nargs='\*'), + ) + def test\_with\_options\_should\_parse(self) -> None: + text1 = " -indent the name of test1 chunk... " + options1 = self.option\_parser.parse(text1) + self.assertEqual({'-indent': [], 'argument': ['the', 'name', 'of', 'test1', 'chunk...']}, options1) + def test\_without\_options\_should\_parse(self) -> None: + text2 = " the name of test2 chunk... " + options2 = self.option\_parser.parse(text2) + self.assertEqual({'argument': ['the', 'name', 'of', 'test2', 'chunk...']}, options2) + +.. + + .. class:: small + + |loz| *Unit Test of WebReader class (41)*. Used by: test_unit.py (`1`_) + + + +Action Tests +------------- + +Each class is tested separately. Sequence of some mocks, +load, tangle, weave. + + +.. _`42`: +.. rubric:: Unit Test of Action class hierarchy (42) = +.. parsed-literal:: + :class: code + + + |srarr|\ Unit test of Action Sequence class (`43`_) + |srarr|\ Unit test of LoadAction class (`46`_) + |srarr|\ Unit test of TangleAction class (`45`_) + |srarr|\ Unit test of WeaverAction class (`44`_) + +.. + + .. class:: small + + |loz| *Unit Test of Action class hierarchy (42)*. Used by: test_unit.py (`1`_) + + +**TODO:** Replace with Mock + + +.. _`43`: +.. rubric:: Unit test of Action Sequence class (43) = +.. parsed-literal:: + :class: code + + + class TestActionSequence(unittest.TestCase): + def setUp(self) -> None: + self.web = MockWeb() + self.a1 = MagicMock(name="Action1") + self.a2 = MagicMock(name="Action2") + self.action = pyweb.ActionSequence("TwoSteps", [self.a1, self.a2]) + self.action.web = self.web + self.action.options = argparse.Namespace() + def test\_should\_execute\_both(self) -> None: + self.action() + self.assertEqual(self.a1.call\_count, 1) + self.assertEqual(self.a2.call\_count, 1) + +.. + + .. class:: small + + |loz| *Unit test of Action Sequence class (43)*. Used by: Unit Test of Action class hierarchy... (`42`_) + + + +.. _`44`: +.. rubric:: Unit test of WeaverAction class (44) = +.. parsed-literal:: + :class: code + + + class TestWeaveAction(unittest.TestCase): + def setUp(self) -> None: + self.web = MockWeb() + self.action = pyweb.WeaveAction() + self.weaver = MockWeaver() + self.action.web = self.web + self.action.options = argparse.Namespace( + theWeaver=self.weaver, + reference\_style=pyweb.SimpleReference(), + output=Path.cwd(), + ) + def test\_should\_execute\_weaving(self) -> None: + self.action() + self.assertEqual(self.web.weave.mock\_calls, [call(self.weaver)]) + +.. + + .. class:: small + + |loz| *Unit test of WeaverAction class (44)*. Used by: Unit Test of Action class hierarchy... (`42`_) + + + +.. _`45`: +.. rubric:: Unit test of TangleAction class (45) = +.. parsed-literal:: + :class: code + + + class TestTangleAction(unittest.TestCase): + def setUp(self) -> None: + self.web = MockWeb() + self.action = pyweb.TangleAction() + self.tangler = MockTangler() + self.action.web = self.web + self.action.options = argparse.Namespace( + theTangler = self.tangler, + tangler\_line\_numbers = False, + output=Path.cwd() + ) + def test\_should\_execute\_tangling(self) -> None: + self.action() + self.assertEqual(self.web.tangle.mock\_calls, [call(self.tangler)]) + +.. + + .. class:: small + + |loz| *Unit test of TangleAction class (45)*. Used by: Unit Test of Action class hierarchy... (`42`_) + + +The mocked ``WebReader`` must provide an ``errors`` property to the ``LoadAction`` instance. + + +.. _`46`: +.. rubric:: Unit test of LoadAction class (46) = +.. parsed-literal:: + :class: code + + + class TestLoadAction(unittest.TestCase): + def setUp(self) -> None: + self.web = MockWeb() + self.action = pyweb.LoadAction() + self.webReader = Mock( + name="WebReader", + errors=0, + ) + self.action.web = self.web + self.source\_path = Path("TestLoadAction.w") + self.action.options = argparse.Namespace( + webReader = self.webReader, + source\_path=self.source\_path, + command="@", + permitList = [], + output=Path.cwd(), + ) + Path("TestLoadAction.w").write\_text("") + def tearDown(self) -> None: + try: + Path("TestLoadAction.w").unlink() + except IOError: + pass + def test\_should\_execute\_loading(self) -> None: + self.action() + # Old: self.assertEqual(1, self.webReader.count) + print(self.webReader.load.mock\_calls) + self.assertEqual(self.webReader.load.mock\_calls, [call(self.web, self.source\_path)]) + self.webReader.web.assert\_not\_called() # Deprecated + self.webReader.source.assert\_not\_called() # Deprecated + +.. + + .. class:: small + + |loz| *Unit test of LoadAction class (46)*. Used by: Unit Test of Action class hierarchy... (`42`_) + + +Application Tests +------------------ + +As with testing WebReader, this requires extensive mocking. +It's easier to simply run the various use cases. + +**TODO:** Test Application class + + +.. _`47`: +.. rubric:: Unit Test of Application class (47) = +.. parsed-literal:: + :class: code + + # TODO Test Application class +.. + + .. class:: small + + |loz| *Unit Test of Application class (47)*. Used by: test_unit.py (`1`_) + + +Overheads and Main Script +-------------------------- + +The boilerplate code for unit testing is the following. + + +.. _`48`: +.. rubric:: Unit Test overheads: imports, etc. (48) = +.. parsed-literal:: + :class: code + + """Unit tests.""" + import argparse + import io + import logging + import os + from pathlib import Path + import re + import string + import sys + import textwrap + import time + from typing import Any, TextIO + import unittest + from unittest.mock import Mock, call, MagicMock, sentinel + import warnings + + import pyweb + +.. + + .. class:: small + + |loz| *Unit Test overheads: imports, etc. (48)*. Used by: test_unit.py (`1`_) + + +One more overhead is a function we can inject into selected subclasses +of ``unittest.TestCase``. This is monkeypatch feature that seems useful. + + +.. _`49`: +.. rubric:: Unit Test overheads: imports, etc. (49) += +.. parsed-literal:: + :class: code + + + def rstrip\_lines(source: str) -> list[str]: + return list(l.rstrip() for l in source.splitlines()) + +.. + + .. class:: small + + |loz| *Unit Test overheads: imports, etc. (49)*. Used by: test_unit.py (`1`_) + + + +.. _`50`: +.. rubric:: Unit Test main (50) = +.. parsed-literal:: + :class: code + + + if \_\_name\_\_ == "\_\_main\_\_": + logging.basicConfig(stream=sys.stdout, level=logging.WARN) + unittest.main() + +.. + + .. class:: small + + |loz| *Unit Test main (50)*. Used by: test_unit.py (`1`_) + + +We run the default ``unittest.main()`` to execute the entire suite of tests. + + +Functional Testing +================== + +.. test/func.w + +There are three broad areas of functional testing. + +- `Tests for Loading`_ + +- `Tests for Tangling`_ + +- `Tests for Weaving`_ + +There are a total of 11 test cases. + +Tests for Loading +------------------ + +We need to be able to load a web from one or more source files. + + +.. _`51`: +.. rubric:: test_loader.py (51) = +.. parsed-literal:: + :class: code + + |srarr|\ Load Test overheads: imports, etc. (`53`_), |srarr|\ (`58`_) + + |srarr|\ Load Test superclass to refactor common setup (`52`_) + + |srarr|\ Load Test error handling with a few common syntax errors (`54`_) + + |srarr|\ Load Test include processing with syntax errors (`56`_) + + |srarr|\ Load Test main program (`59`_) + +.. + + .. class:: small + + |loz| *test_loader.py (51)*. + + +Parsing test cases have a common setup shown in this superclass. + +By using some class-level variables ``text``, +``file_path``, we can simply provide a file-like +input object to the ``WebReader`` instance. + + +.. _`52`: +.. rubric:: Load Test superclass to refactor common setup (52) = +.. parsed-literal:: + :class: code + + + class ParseTestcase(unittest.TestCase): + text: ClassVar[str] + file\_path: ClassVar[Path] + + def setUp(self) -> None: + self.source = io.StringIO(self.text) + self.web = pyweb.Web() + self.rdr = pyweb.WebReader() + +.. + + .. class:: small + + |loz| *Load Test superclass to refactor common setup (52)*. Used by: test_loader.py (`51`_) + + +There are a lot of specific parsing exceptions which can be thrown. +We'll cover most of the cases with a quick check for a failure to +find an expected next token. + + +.. _`53`: +.. rubric:: Load Test overheads: imports, etc. (53) = +.. parsed-literal:: + :class: code + + + import logging.handlers + from pathlib import Path + from typing import ClassVar + +.. + + .. class:: small + + |loz| *Load Test overheads: imports, etc. (53)*. Used by: test_loader.py (`51`_) + + + +.. _`54`: +.. rubric:: Load Test error handling with a few common syntax errors (54) = +.. parsed-literal:: + :class: code + + + |srarr|\ Sample Document 1 with correct and incorrect syntax (`55`_) + + class Test\_ParseErrors(ParseTestcase): + text = test1\_w + file\_path = Path("test1.w") + def test\_error\_should\_count\_1(self) -> None: + with self.assertLogs('WebReader', level='WARN') as log\_capture: + self.rdr.load(self.web, self.file\_path, self.source) + self.assertEqual(3, self.rdr.errors) + self.assertEqual(log\_capture.output, + [ + "ERROR:WebReader:At ('test1.w', 8): expected ('@{',), found '@o'", + "ERROR:WebReader:Extra '@{' (possibly missing chunk name) near ('test1.w', 9)", + "ERROR:WebReader:Extra '@{' (possibly missing chunk name) near ('test1.w', 9)" + ] + ) + +.. + + .. class:: small + + |loz| *Load Test error handling with a few common syntax errors (54)*. Used by: test_loader.py (`51`_) + + + +.. _`55`: +.. rubric:: Sample Document 1 with correct and incorrect syntax (55) = +.. parsed-literal:: + :class: code + + + test1\_w = """Some anonymous chunk + @o test1.tmp + @{@ + @ + @}@@ + @d part1 @{This is part 1.@} + Okay, now for an error. + @o show how @o commands work + @{ @{ @] @] + """ + +.. + + .. class:: small + + |loz| *Sample Document 1 with correct and incorrect syntax (55)*. Used by: Load Test error handling... (`54`_) + + +All of the parsing exceptions should be correctly identified with +any included file. +We'll cover most of the cases with a quick check for a failure to +find an expected next token. + +In order to test the include file processing, we have to actually +create a temporary file. It's hard to mock the include processing, +since it's a nested instance of the tokenizer. + + +.. _`56`: +.. rubric:: Load Test include processing with syntax errors (56) = +.. parsed-literal:: + :class: code + + + |srarr|\ Sample Document 8 and the file it includes (`57`_) + + class Test\_IncludeParseErrors(ParseTestcase): + text = test8\_w + file\_path = Path("test8.w") + def setUp(self) -> None: + super().setUp() + Path('test8\_inc.tmp').write\_text(test8\_inc\_w) + def test\_error\_should\_count\_2(self) -> None: + with self.assertLogs('WebReader', level='WARN') as log\_capture: + self.rdr.load(self.web, self.file\_path, self.source) + self.assertEqual(1, self.rdr.errors) + self.assertEqual(log\_capture.output, + [ + "ERROR:WebReader:At ('test8\_inc.tmp', 4): end of input, ('@{', '@[') not found", + "ERROR:WebReader:Errors in included file 'test8\_inc.tmp', output is incomplete." + ] + ) + def tearDown(self) -> None: + super().tearDown() + Path('test8\_inc.tmp').unlink() + +.. + + .. class:: small + + |loz| *Load Test include processing with syntax errors (56)*. Used by: test_loader.py (`51`_) + + +The sample document must reference the correct name that will +be given to the included document by ``setUp``. + + +.. _`57`: +.. rubric:: Sample Document 8 and the file it includes (57) = +.. parsed-literal:: + :class: code + + + test8\_w = """Some anonymous chunk. + @d title @[the title of this document, defined with @@[ and @@]@] + A reference to @. + @i test8\_inc.tmp + A final anonymous chunk from test8.w + """ + + test8\_inc\_w="""A chunk from test8a.w + And now for an error - incorrect syntax in an included file! + @d yap + """ + +.. + + .. class:: small + + |loz| *Sample Document 8 and the file it includes (57)*. Used by: Load Test include... (`56`_) + + +

    The overheads for a Python unittest.

    + + +.. _`58`: +.. rubric:: Load Test overheads: imports, etc. (58) += +.. parsed-literal:: + :class: code + + + """Loader and parsing tests.""" + import io + import logging + import os + from pathlib import Path + import string + import sys + import types + import unittest + + import pyweb + +.. + + .. class:: small + + |loz| *Load Test overheads: imports, etc. (58)*. Used by: test_loader.py (`51`_) + + +A main program that configures logging and then runs the test. + + +.. _`59`: +.. rubric:: Load Test main program (59) = +.. parsed-literal:: + :class: code + + + if \_\_name\_\_ == "\_\_main\_\_": + logging.basicConfig(stream=sys.stdout, level=logging.WARN) + unittest.main() + +.. + + .. class:: small + + |loz| *Load Test main program (59)*. Used by: test_loader.py (`51`_) + + +Tests for Tangling +------------------ + +We need to be able to tangle a web. + + +.. _`60`: +.. rubric:: test_tangler.py (60) = +.. parsed-literal:: + :class: code + + |srarr|\ Tangle Test overheads: imports, etc. (`74`_) + |srarr|\ Tangle Test superclass to refactor common setup (`61`_) + |srarr|\ Tangle Test semantic error 2 (`62`_) + |srarr|\ Tangle Test semantic error 3 (`64`_) + |srarr|\ Tangle Test semantic error 4 (`66`_) + |srarr|\ Tangle Test semantic error 5 (`68`_) + |srarr|\ Tangle Test semantic error 6 (`70`_) + |srarr|\ Tangle Test include error 7 (`72`_) + |srarr|\ Tangle Test main program (`75`_) + +.. + + .. class:: small + + |loz| *test_tangler.py (60)*. + + +Tangling test cases have a common setup and teardown shown in this superclass. +Since tangling must produce a file, it's helpful to remove the file that gets created. +The essential test case is to load and attempt to tangle, checking the +exceptions raised. + + + +.. _`61`: +.. rubric:: Tangle Test superclass to refactor common setup (61) = +.. parsed-literal:: + :class: code + + + class TangleTestcase(unittest.TestCase): + text: ClassVar[str] + error: ClassVar[str] + file\_path: ClassVar[Path] + + def setUp(self) -> None: + self.source = io.StringIO(self.text) + self.web = pyweb.Web() + self.rdr = pyweb.WebReader() + self.tangler = pyweb.Tangler() + + def tangle\_and\_check\_exception(self, exception\_text: str) -> None: + try: + self.rdr.load(self.web, self.file\_path, self.source) + self.web.tangle(self.tangler) + self.web.createUsedBy() + self.fail("Should not tangle") + except pyweb.Error as e: + self.assertEqual(exception\_text, e.args[0]) + + def tearDown(self) -> None: + try: + self.file\_path.with\_suffix(".tmp").unlink() + except FileNotFoundError: + pass # If the test fails, nothing to remove... + +.. + + .. class:: small + + |loz| *Tangle Test superclass to refactor common setup (61)*. Used by: test_tangler.py (`60`_) + + + +.. _`62`: +.. rubric:: Tangle Test semantic error 2 (62) = +.. parsed-literal:: + :class: code + + + |srarr|\ Sample Document 2 (`63`_) + + class Test\_SemanticError\_2(TangleTestcase): + text = test2\_w + file\_path = Path("test2.w") + def test\_should\_raise\_undefined(self) -> None: + self.tangle\_and\_check\_exception("Attempt to tangle an undefined Chunk, part2.") + +.. + + .. class:: small + + |loz| *Tangle Test semantic error 2 (62)*. Used by: test_tangler.py (`60`_) + + + +.. _`63`: +.. rubric:: Sample Document 2 (63) = +.. parsed-literal:: + :class: code + + + test2\_w = """Some anonymous chunk + @o test2.tmp + @{@ + @ + @}@@ + @d part1 @{This is part 1.@} + Okay, now for some errors: no part2! + """ + +.. + + .. class:: small + + |loz| *Sample Document 2 (63)*. Used by: Tangle Test semantic error 2... (`62`_) + + + +.. _`64`: +.. rubric:: Tangle Test semantic error 3 (64) = +.. parsed-literal:: + :class: code + + + |srarr|\ Sample Document 3 (`65`_) + + class Test\_SemanticError\_3(TangleTestcase): + text = test3\_w + file\_path = Path("test3.w") + def test\_should\_raise\_bad\_xref(self) -> None: + self.tangle\_and\_check\_exception("Illegal tangling of a cross reference command.") + +.. + + .. class:: small + + |loz| *Tangle Test semantic error 3 (64)*. Used by: test_tangler.py (`60`_) + + + +.. _`65`: +.. rubric:: Sample Document 3 (65) = +.. parsed-literal:: + :class: code + + + test3\_w = """Some anonymous chunk + @o test3.tmp + @{@ + @ + @}@@ + @d part1 @{This is part 1.@} + @d part2 @{This is part 2, with an illegal: @f.@} + Okay, now for some errors: attempt to tangle a cross-reference! + """ + +.. + + .. class:: small + + |loz| *Sample Document 3 (65)*. Used by: Tangle Test semantic error 3... (`64`_) + + + + +.. _`66`: +.. rubric:: Tangle Test semantic error 4 (66) = +.. parsed-literal:: + :class: code + + + |srarr|\ Sample Document 4 (`67`_) + + class Test\_SemanticError\_4(TangleTestcase): + text = test4\_w + file\_path = Path("test4.w") + def test\_should\_raise\_noFullName(self) -> None: + self.tangle\_and\_check\_exception("No full name for 'part1...'") + +.. + + .. class:: small + + |loz| *Tangle Test semantic error 4 (66)*. Used by: test_tangler.py (`60`_) + + + +.. _`67`: +.. rubric:: Sample Document 4 (67) = +.. parsed-literal:: + :class: code + + + test4\_w = """Some anonymous chunk + @o test4.tmp + @{@ + @ + @}@@ + @d part1... @{This is part 1.@} + @d part2 @{This is part 2.@} + Okay, now for some errors: attempt to weave but no full name for part1.... + """ + +.. + + .. class:: small + + |loz| *Sample Document 4 (67)*. Used by: Tangle Test semantic error 4... (`66`_) + + + +.. _`68`: +.. rubric:: Tangle Test semantic error 5 (68) = +.. parsed-literal:: + :class: code + + + |srarr|\ Sample Document 5 (`69`_) + + class Test\_SemanticError\_5(TangleTestcase): + text = test5\_w + file\_path = Path("test5.w") + def test\_should\_raise\_ambiguous(self) -> None: + self.tangle\_and\_check\_exception("Ambiguous abbreviation 'part1...', matches ['part1a', 'part1b']") + +.. + + .. class:: small + + |loz| *Tangle Test semantic error 5 (68)*. Used by: test_tangler.py (`60`_) + + + +.. _`69`: +.. rubric:: Sample Document 5 (69) = +.. parsed-literal:: + :class: code + + + test5\_w = """ + Some anonymous chunk + @o test5.tmp + @{@ + @ + @}@@ + @d part1a @{This is part 1 a.@} + @d part1b @{This is part 1 b.@} + @d part2 @{This is part 2.@} + Okay, now for some errors: part1... is ambiguous + """ + +.. + + .. class:: small + + |loz| *Sample Document 5 (69)*. Used by: Tangle Test semantic error 5... (`68`_) + + + +.. _`70`: +.. rubric:: Tangle Test semantic error 6 (70) = +.. parsed-literal:: + :class: code + + + |srarr|\ Sample Document 6 (`71`_) + + class Test\_SemanticError\_6(TangleTestcase): + text = test6\_w + file\_path = Path("test6.w") + def test\_should\_warn(self) -> None: + self.rdr.load(self.web, self.file\_path, self.source) + self.web.tangle(self.tangler) + self.web.createUsedBy() + self.assertEqual(1, len(self.web.no\_reference())) + self.assertEqual(1, len(self.web.multi\_reference())) + self.assertEqual(0, len(self.web.no\_definition())) + +.. + + .. class:: small + + |loz| *Tangle Test semantic error 6 (70)*. Used by: test_tangler.py (`60`_) + + + +.. _`71`: +.. rubric:: Sample Document 6 (71) = +.. parsed-literal:: + :class: code + + + test6\_w = """Some anonymous chunk + @o test6.tmp + @{@ + @ + @}@@ + @d part1a @{This is part 1 a.@} + @d part2 @{This is part 2.@} + Okay, now for some warnings: + - part1 has multiple references. + - part2 is unreferenced. + """ + +.. + + .. class:: small + + |loz| *Sample Document 6 (71)*. Used by: Tangle Test semantic error 6... (`70`_) + + + +.. _`72`: +.. rubric:: Tangle Test include error 7 (72) = +.. parsed-literal:: + :class: code + + + |srarr|\ Sample Document 7 and it's included file (`73`_) + + class Test\_IncludeError\_7(TangleTestcase): + text = test7\_w + file\_path = Path("test7.w") + def setUp(self) -> None: + Path('test7\_inc.tmp').write\_text(test7\_inc\_w) + super().setUp() + def test\_should\_include(self) -> None: + self.rdr.load(self.web, self.file\_path, self.source) + self.web.tangle(self.tangler) + self.web.createUsedBy() + self.assertEqual(5, len(self.web.chunkSeq)) + self.assertEqual(test7\_inc\_w, self.web.chunkSeq[3].commands[0].text) + def tearDown(self) -> None: + Path('test7\_inc.tmp').unlink() + super().tearDown() + +.. + + .. class:: small + + |loz| *Tangle Test include error 7 (72)*. Used by: test_tangler.py (`60`_) + + + +.. _`73`: +.. rubric:: Sample Document 7 and it's included file (73) = +.. parsed-literal:: + :class: code + + + test7\_w = """ + Some anonymous chunk. + @d title @[the title of this document, defined with @@[ and @@]@] + A reference to @. + @i test7\_inc.tmp + A final anonymous chunk from test7.w + """ + + test7\_inc\_w = """The test7a.tmp chunk for test7.w + """ + +.. + + .. class:: small + + |loz| *Sample Document 7 and it's included file (73)*. Used by: Tangle Test include error 7... (`72`_) + + + +.. _`74`: +.. rubric:: Tangle Test overheads: imports, etc. (74) = +.. parsed-literal:: + :class: code + + + """Tangler tests exercise various semantic features.""" + import io + import logging + import os + from pathlib import Path + from typing import ClassVar + import unittest + + import pyweb + +.. + + .. class:: small + + |loz| *Tangle Test overheads: imports, etc. (74)*. Used by: test_tangler.py (`60`_) + + + +.. _`75`: +.. rubric:: Tangle Test main program (75) = +.. parsed-literal:: + :class: code + + + if \_\_name\_\_ == "\_\_main\_\_": + import sys + logging.basicConfig(stream=sys.stdout, level=logging.WARN) + unittest.main() + +.. + + .. class:: small + + |loz| *Tangle Test main program (75)*. Used by: test_tangler.py (`60`_) + + + +Tests for Weaving +----------------- + +We need to be able to weave a document from one or more source files. + + +.. _`76`: +.. rubric:: test_weaver.py (76) = +.. parsed-literal:: + :class: code + + |srarr|\ Weave Test overheads: imports, etc. (`83`_) + |srarr|\ Weave Test superclass to refactor common setup (`77`_) + |srarr|\ Weave Test references and definitions (`78`_) + |srarr|\ Weave Test evaluation of expressions (`81`_) + |srarr|\ Weave Test main program (`84`_) + +.. + + .. class:: small + + |loz| *test_weaver.py (76)*. + + +Weaving test cases have a common setup shown in this superclass. + + +.. _`77`: +.. rubric:: Weave Test superclass to refactor common setup (77) = +.. parsed-literal:: + :class: code + + + class WeaveTestcase(unittest.TestCase): + text: ClassVar[str] + error: ClassVar[str] + file\_path: ClassVar[Path] + + def setUp(self) -> None: + self.source = io.StringIO(self.text) + self.web = pyweb.Web() + self.rdr = pyweb.WebReader() + + def tearDown(self) -> None: + try: + self.file\_path.with\_suffix(".html").unlink() + except FileNotFoundError: + pass # if the test failed, nothing to remove + +.. + + .. class:: small + + |loz| *Weave Test superclass to refactor common setup (77)*. Used by: test_weaver.py (`76`_) + + + +.. _`78`: +.. rubric:: Weave Test references and definitions (78) = +.. parsed-literal:: + :class: code + + + |srarr|\ Sample Document 0 (`79`_) + |srarr|\ Expected Output 0 (`80`_) + + class Test\_RefDefWeave(WeaveTestcase): + text = test0\_w + file\_path = Path("test0.w") + def test\_load\_should\_createChunks(self) -> None: + self.rdr.load(self.web, self.file\_path, self.source) + self.assertEqual(3, len(self.web.chunkSeq)) + def test\_weave\_should\_createFile(self) -> None: + self.rdr.load(self.web, self.file\_path, self.source) + doc = pyweb.HTML() + doc.reference\_style = pyweb.SimpleReference() + self.web.weave(doc) + actual = self.file\_path.with\_suffix(".html").read\_text() + self.maxDiff = None + self.assertEqual(test0\_expected, actual) + + +.. + + .. class:: small + + |loz| *Weave Test references and definitions (78)*. Used by: test_weaver.py (`76`_) + + + +.. _`79`: +.. rubric:: Sample Document 0 (79) = +.. parsed-literal:: + :class: code + + + test0\_w = """ + + + + + @ + + @d some code + @{ + def fastExp(n, p): + r = 1 + while p > 0: + if p%2 == 1: return n\*fastExp(n,p-1) + return n\*n\*fastExp(n,p/2) + + for i in range(24): + fastExp(2,i) + @} + + + """ + +.. + + .. class:: small + + |loz| *Sample Document 0 (79)*. Used by: Weave Test references... (`78`_) + + + +.. _`80`: +.. rubric:: Expected Output 0 (80) = +.. parsed-literal:: + :class: code + + + test0\_expected = """ + + + + + some code (1) + + + + +

    some code (1) =

    +
    
    +    
    +    def fastExp(n, p):
    +        r = 1
    +        while p > 0:
    +            if p%2 == 1: return n\*fastExp(n,p-1)
    +        return n\*n\*fastExp(n,p/2)
    +    
    +    for i in range(24):
    +        fastExp(2,i)
    +    
    +        
    +

    some code (1). + +

    + + + + """ + +.. + + .. class:: small + + |loz| *Expected Output 0 (80)*. Used by: Weave Test references... (`78`_) + + +Note that this really requires a mocked ``time`` module in order +to properly provide a consistent output from ``time.asctime()``. + + +.. _`81`: +.. rubric:: Weave Test evaluation of expressions (81) = +.. parsed-literal:: + :class: code + + + |srarr|\ Sample Document 9 (`82`_) + + from unittest.mock import Mock + + class TestEvaluations(WeaveTestcase): + text = test9\_w + file\_path = Path("test9.w") + def setUp(self): + super().setUp() + self.mock\_time = Mock(asctime=Mock(return\_value="mocked time")) + def test\_should\_evaluate(self) -> None: + self.rdr.load(self.web, self.file\_path, self.source) + doc = pyweb.HTML( ) + doc.reference\_style = pyweb.SimpleReference() + self.web.weave(doc) + actual = self.file\_path.with\_suffix(".html").read\_text().splitlines() + #print(actual) + self.assertEqual("An anonymous chunk.", actual[0]) + self.assertTrue("Time = mocked time", actual[1]) + self.assertEqual("File = ('test9.w', 3)", actual[2]) + self.assertEqual('Version = 3.1', actual[3]) + self.assertEqual(f'CWD = {os.getcwd()}', actual[4]) + +.. + + .. class:: small + + |loz| *Weave Test evaluation of expressions (81)*. Used by: test_weaver.py (`76`_) + + + +.. _`82`: +.. rubric:: Sample Document 9 (82) = +.. parsed-literal:: + :class: code + + + test9\_w= """An anonymous chunk. + Time = @(time.asctime()@) + File = @(theLocation@) + Version = @(\_\_version\_\_@) + CWD = @(os.path.realpath('.')@) + """ + +.. + + .. class:: small + + |loz| *Sample Document 9 (82)*. Used by: Weave Test evaluation... (`81`_) + + + +.. _`83`: +.. rubric:: Weave Test overheads: imports, etc. (83) = +.. parsed-literal:: + :class: code + + + """Weaver tests exercise various weaving features.""" + import io + import logging + import os + from pathlib import Path + import string + import sys + from typing import ClassVar + import unittest + + import pyweb + +.. + + .. class:: small + + |loz| *Weave Test overheads: imports, etc. (83)*. Used by: test_weaver.py (`76`_) + + + +.. _`84`: +.. rubric:: Weave Test main program (84) = +.. parsed-literal:: + :class: code + + + if \_\_name\_\_ == "\_\_main\_\_": + logging.basicConfig(stream=sys.stderr, level=logging.WARN) + unittest.main() + +.. + + .. class:: small + + |loz| *Weave Test main program (84)*. Used by: test_weaver.py (`76`_) + + + +Additional Scripts Testing +========================== + +.. test/scripts.w + +We provide these two additional scripts; effectively command-line short-cuts: + +- ``tangle.py`` + +- ``weave.py`` + +These need their own test cases. + + +This gives us the following outline for the script testing. + + +.. _`85`: +.. rubric:: test_scripts.py (85) = +.. parsed-literal:: + :class: code + + |srarr|\ Script Test overheads: imports, etc. (`90`_) + + |srarr|\ Sample web file to test with (`86`_) + + |srarr|\ Superclass for test cases (`87`_) + + |srarr|\ Test of weave.py (`88`_) + + |srarr|\ Test of tangle.py (`89`_) + + |srarr|\ Scripts Test main (`91`_) + +.. + + .. class:: small + + |loz| *test_scripts.py (85)*. + + +Sample Web File +--------------- + +This is a web ``.w`` file to create a document and tangle a small file. + + +.. _`86`: +.. rubric:: Sample web file to test with (86) = +.. parsed-literal:: + :class: code + + + sample = textwrap.dedent(""" + + + + + + Sample HTML web file + + +

    Sample HTML web file

    +

    We're avoiding using Python specifically. + This hints at other languages being tangled by this tool.

    + + @o sample\_tangle.code + @{ + @ + @ + @} + + @d preamble + @{ + #include + @} + + @d body + @{ + int main() { + println("Hello, World!") + } + @} + + + + """) + +.. + + .. class:: small + + |loz| *Sample web file to test with (86)*. Used by: test_scripts.py (`85`_) + + +Superclass for test cases +------------------------- + +This superclass definition creates a consistent test fixture for both test cases. +The sample ``test_sample.w`` file is created and removed after the test. + + +.. _`87`: +.. rubric:: Superclass for test cases (87) = +.. parsed-literal:: + :class: code + + + class SampleWeb(unittest.TestCase): + def setUp(self) -> None: + self.sample\_path = Path("test\_sample.w") + self.sample\_path.write\_text(sample) + def tearDown(self) -> None: + self.sample\_path.unlink() + + +.. + + .. class:: small + + |loz| *Superclass for test cases (87)*. Used by: test_scripts.py (`85`_) + + +Weave Script Test +----------------- + +We check the weave output to be sure it's what we expected. +This could be altered to check a few features of the weave file rather than compare the entire file. + + +.. _`88`: +.. rubric:: Test of weave.py (88) = +.. parsed-literal:: + :class: code + + + expected\_weave = textwrap.dedent(""" + + + + + + Sample HTML web file + + +

    Sample HTML web file

    +

    We're avoiding using Python specifically. + This hints at other languages being tangled by this tool.

    + + + +

    \`\`sample\_tangle.code\`\` (1) =

    +
    
    +        
    +        preamble (2)
    +        body (3)
    +        
    +

    ◊ \`\`sample\_tangle.code\`\` (1). + [] +

    + + + + +

    preamble (2) =

    +
    
    +        
    +        #include <stdio.h>
    +        
    +            
    +

    preamble (2). + Used by sample\_tangle.code (1). +

    + + + + +

    body (3) =

    +
    
    +        
    +        int main() {
    +            println("Hello, World!")
    +        }
    +        
    +            
    +

    body (3). + Used by sample\_tangle.code (1). +

    + + + + + """) + + class TestWeave(SampleWeb): + def setUp(self) -> None: + super().setUp() + self.output = self.sample\_path.with\_suffix(".html") + def test(self) -> None: + weave.main(self.sample\_path) + result = self.output.read\_text() + self.assertEqual(result, expected\_weave) + def tearDown(self) -> None: + super().tearDown() + self.output.unlink() + +.. + + .. class:: small + + |loz| *Test of weave.py (88)*. Used by: test_scripts.py (`85`_) + + +Tangle Script Test +------------------ + +We check the tangle output to be sure it's what we expected. + + +.. _`89`: +.. rubric:: Test of tangle.py (89) = +.. parsed-literal:: + :class: code + + + + expected\_tangle = textwrap.dedent(""" + + #include + + + int main() { + println("Hello, World!") + } + + """) + + class TestTangle(SampleWeb): + def setUp(self) -> None: + super().setUp() + self.output = Path("sample\_tangle.code") + def test(self) -> None: + tangle.main(self.sample\_path) + result = self.output.read\_text() + self.assertEqual(result, expected\_tangle) + def tearDown(self) -> None: + super().tearDown() + self.output.unlink() + +.. + + .. class:: small + + |loz| *Test of tangle.py (89)*. Used by: test_scripts.py (`85`_) + + +Overheads and Main Script +-------------------------- + +This is typical of the other test modules. We provide a unittest runner +here in case we want to run these tests in isolation. + + +.. _`90`: +.. rubric:: Script Test overheads: imports, etc. (90) = +.. parsed-literal:: + :class: code + + """Script tests.""" + import logging + from pathlib import Path + import sys + import textwrap + import unittest + + import tangle + import weave + +.. + + .. class:: small + + |loz| *Script Test overheads: imports, etc. (90)*. Used by: test_scripts.py (`85`_) + + + +.. _`91`: +.. rubric:: Scripts Test main (91) = +.. parsed-literal:: + :class: code + + + if \_\_name\_\_ == "\_\_main\_\_": + logging.basicConfig(stream=sys.stdout, level=logging.WARN) + unittest.main() + +.. + + .. class:: small + + |loz| *Scripts Test main (91)*. Used by: test_scripts.py (`85`_) + + +We run the default ``unittest.main()`` to execute the entire suite of tests. + + +No Longer supported: @i runner.w, using **pytest** seems better. + +Additional Files +================= + +To get the RST to look good, there are two additional files. +These are clones of what's in the ``src`` directory. + +``docutils.conf`` defines two CSS files to use. + The default CSS file may need to be customized. + + +.. _`92`: +.. rubric:: docutils.conf (92) = +.. parsed-literal:: + :class: code + + # docutils.conf + + [html4css1 writer] + stylesheet-path: /Users/slott/miniconda3/envs/pywebtool/lib/python3.10/site-packages/docutils/writers/html4css1/html4css1.css, + page-layout.css + syntax-highlight: long + +.. + + .. class:: small + + |loz| *docutils.conf (92)*. + + +``page-layout.css`` This tweaks one CSS to be sure that +the resulting HTML pages are easier to read. These are minor +tweaks to the default CSS. + + +.. _`93`: +.. rubric:: page-layout.css (93) = +.. parsed-literal:: + :class: code + + /\* Page layout tweaks \*/ + div.document { width: 7in; } + .small { font-size: smaller; } + .code + { + color: #101080; + display: block; + border-color: black; + border-width: thin; + border-style: solid; + background-color: #E0FFFF; + /\*#99FFFF\*/ + padding: 0 0 0 1%; + margin: 0 6% 0 6%; + text-align: left; + font-size: smaller; + } + +.. + + .. class:: small + + |loz| *page-layout.css (93)*. + + + +Indices +======= + +Files +----- + + +:docutils.conf: + |srarr|\ (`92`_) +:page-layout.css: + |srarr|\ (`93`_) +:test_loader.py: + |srarr|\ (`51`_) +:test_scripts.py: + |srarr|\ (`85`_) +:test_tangler.py: + |srarr|\ (`60`_) +:test_unit.py: + |srarr|\ (`1`_) +:test_weaver.py: + |srarr|\ (`76`_) + + + +Macros +------ + + +:Expected Output 0: + |srarr|\ (`80`_) +:Load Test error handling with a few common syntax errors: + |srarr|\ (`54`_) +:Load Test include processing with syntax errors: + |srarr|\ (`56`_) +:Load Test main program: + |srarr|\ (`59`_) +:Load Test overheads: imports, etc.: + |srarr|\ (`53`_) |srarr|\ (`58`_) +:Load Test superclass to refactor common setup: + |srarr|\ (`52`_) +:Sample Document 0: + |srarr|\ (`79`_) +:Sample Document 1 with correct and incorrect syntax: + |srarr|\ (`55`_) +:Sample Document 2: + |srarr|\ (`63`_) +:Sample Document 3: + |srarr|\ (`65`_) +:Sample Document 4: + |srarr|\ (`67`_) +:Sample Document 5: + |srarr|\ (`69`_) +:Sample Document 6: + |srarr|\ (`71`_) +:Sample Document 7 and it's included file: + |srarr|\ (`73`_) +:Sample Document 8 and the file it includes: + |srarr|\ (`57`_) +:Sample Document 9: + |srarr|\ (`82`_) +:Sample web file to test with: + |srarr|\ (`86`_) +:Script Test overheads: imports, etc.: + |srarr|\ (`90`_) +:Scripts Test main: + |srarr|\ (`91`_) +:Superclass for test cases: + |srarr|\ (`87`_) +:Tangle Test include error 7: + |srarr|\ (`72`_) +:Tangle Test main program: + |srarr|\ (`75`_) +:Tangle Test overheads: imports, etc.: + |srarr|\ (`74`_) +:Tangle Test semantic error 2: + |srarr|\ (`62`_) +:Tangle Test semantic error 3: + |srarr|\ (`64`_) +:Tangle Test semantic error 4: + |srarr|\ (`66`_) +:Tangle Test semantic error 5: + |srarr|\ (`68`_) +:Tangle Test semantic error 6: + |srarr|\ (`70`_) +:Tangle Test superclass to refactor common setup: + |srarr|\ (`61`_) +:Test of tangle.py: + |srarr|\ (`89`_) +:Test of weave.py: + |srarr|\ (`88`_) +:Unit Test Mock Chunk class: + |srarr|\ (`4`_) +:Unit Test Web class chunk cross-reference: + |srarr|\ (`36`_) +:Unit Test Web class construction methods: + |srarr|\ (`34`_) +:Unit Test Web class name resolution methods: + |srarr|\ (`35`_) +:Unit Test Web class tangle: + |srarr|\ (`37`_) +:Unit Test Web class weave: + |srarr|\ (`38`_) +:Unit Test main: + |srarr|\ (`50`_) +:Unit Test of Action class hierarchy: + |srarr|\ (`42`_) +:Unit Test of Application class: + |srarr|\ (`47`_) +:Unit Test of Chunk class hierarchy: + |srarr|\ (`11`_) +:Unit Test of Chunk construction: + |srarr|\ (`16`_) +:Unit Test of Chunk emission: + |srarr|\ (`18`_) +:Unit Test of Chunk interrogation: + |srarr|\ (`17`_) +:Unit Test of Chunk superclass: + |srarr|\ (`12`_) |srarr|\ (`13`_) |srarr|\ (`14`_) |srarr|\ (`15`_) +:Unit Test of CodeCommand class to contain a program source code block: + |srarr|\ (`26`_) +:Unit Test of Command class hierarchy: + |srarr|\ (`23`_) +:Unit Test of Command superclass: + |srarr|\ (`24`_) +:Unit Test of Emitter Superclass: + |srarr|\ (`3`_) +:Unit Test of Emitter class hierarchy: + |srarr|\ (`2`_) +:Unit Test of FileXrefCommand class for an output file cross-reference: + |srarr|\ (`28`_) +:Unit Test of HTML subclass of Emitter: + |srarr|\ (`7`_) +:Unit Test of HTMLShort subclass of Emitter: + |srarr|\ (`8`_) +:Unit Test of LaTeX subclass of Emitter: + |srarr|\ (`6`_) +:Unit Test of MacroXrefCommand class for a named chunk cross-reference: + |srarr|\ (`29`_) +:Unit Test of NamedChunk subclass: + |srarr|\ (`19`_) +:Unit Test of NamedChunk_Noindent subclass: + |srarr|\ (`20`_) +:Unit Test of NamedDocumentChunk subclass: + |srarr|\ (`22`_) +:Unit Test of OutputChunk subclass: + |srarr|\ (`21`_) +:Unit Test of Reference class hierarchy: + |srarr|\ (`32`_) +:Unit Test of ReferenceCommand class for chunk references: + |srarr|\ (`31`_) +:Unit Test of Tangler subclass of Emitter: + |srarr|\ (`9`_) +:Unit Test of TanglerMake subclass of Emitter: + |srarr|\ (`10`_) +:Unit Test of TextCommand class to contain a document text block: + |srarr|\ (`25`_) +:Unit Test of UserIdXrefCommand class for a user identifier cross-reference: + |srarr|\ (`30`_) +:Unit Test of Weaver subclass of Emitter: + |srarr|\ (`5`_) +:Unit Test of Web class: + |srarr|\ (`33`_) +:Unit Test of WebReader class: + |srarr|\ (`39`_) |srarr|\ (`40`_) |srarr|\ (`41`_) +:Unit Test of XrefCommand superclass for all cross-reference commands: + |srarr|\ (`27`_) +:Unit Test overheads: imports, etc.: + |srarr|\ (`48`_) |srarr|\ (`49`_) +:Unit test of Action Sequence class: + |srarr|\ (`43`_) +:Unit test of LoadAction class: + |srarr|\ (`46`_) +:Unit test of TangleAction class: + |srarr|\ (`45`_) +:Unit test of WeaverAction class: + |srarr|\ (`44`_) +:Weave Test evaluation of expressions: + |srarr|\ (`81`_) +:Weave Test main program: + |srarr|\ (`84`_) +:Weave Test overheads: imports, etc.: + |srarr|\ (`83`_) +:Weave Test references and definitions: + |srarr|\ (`78`_) +:Weave Test superclass to refactor common setup: + |srarr|\ (`77`_) + + + +User Identifiers +---------------- + +(None) + + +---------- + +.. class:: small + + Created by src/pyweb.py at Mon Jun 13 13:20:15 2022. + + Source tests/pyweb_test.w modified Sun Jun 12 19:10:38 2022. + + pyweb.__version__ '3.1'. + + Working directory '/Users/slott/Documents/Projects/py-web-tool'. diff --git a/tests/pyweb_test.w b/tests/pyweb_test.w new file mode 100644 index 0000000..17c817e --- /dev/null +++ b/tests/pyweb_test.w @@ -0,0 +1,97 @@ +############################################ +pyWeb Literate Programming 3.1 - Test Suite +############################################ + + +================================================= +Yet Another Literate Programming Tool +================================================= + +.. include:: +.. include:: + +.. contents:: + + +@i intro.w + +@i unit.w + +@i func.w + +@i scripts.w + +No Longer supported: @@i runner.w, using **pytest** seems better. + +Additional Files +================= + +To get the RST to look good, there are two additional files. +These are clones of what's in the ``src`` directory. + +``docutils.conf`` defines two CSS files to use. + The default CSS file may need to be customized. + +@o docutils.conf +@{# docutils.conf + +[html4css1 writer] +stylesheet-path: /Users/slott/miniconda3/envs/pywebtool/lib/python3.10/site-packages/docutils/writers/html4css1/html4css1.css, + page-layout.css +syntax-highlight: long +@} + +``page-layout.css`` This tweaks one CSS to be sure that +the resulting HTML pages are easier to read. These are minor +tweaks to the default CSS. + +@o page-layout.css +@{/* Page layout tweaks */ +div.document { width: 7in; } +.small { font-size: smaller; } +.code +{ + color: #101080; + display: block; + border-color: black; + border-width: thin; + border-style: solid; + background-color: #E0FFFF; + /*#99FFFF*/ + padding: 0 0 0 1%; + margin: 0 6% 0 6%; + text-align: left; + font-size: smaller; +} +@} + + +Indices +======= + +Files +----- + +@f + +Macros +------ + +@m + +User Identifiers +---------------- + +@u + +---------- + +.. class:: small + + Created by @(thisApplication@) at @(datetime.datetime.now().ctime()@). + + Source @(theFile@) modified @(datetime.datetime.fromtimestamp(os.path.getmtime(theFile)).ctime()@). + + pyweb.__version__ '@(__version__@)'. + + Working directory '@(os.path.realpath('.')@)'. diff --git a/tests/runner.py b/tests/runner.py new file mode 100644 index 0000000..6097e61 --- /dev/null +++ b/tests/runner.py @@ -0,0 +1,46 @@ +"""Combined tests.""" +import argparse +import unittest +import test_loader +import test_tangler +import test_weaver +import test_unit +import logging +import sys + + + +def suite(): + s = unittest.TestSuite() + for m in (test_loader, test_tangler, test_weaver, test_unit): + s.addTests(unittest.defaultTestLoader.loadTestsFromModule(m)) + return s + + +def get_options(argv: list[str] = sys.argv[1:]) -> argparse.Namespace: + parser = argparse.ArgumentParser() + parser.add_argument("-v", "--verbose", dest="verbosity", action="store_const", const=logging.INFO) + parser.add_argument("-d", "--debug", dest="verbosity", action="store_const", const=logging.DEBUG) + parser.add_argument("-l", "--logger", dest="logger", action="store", help="comma-separated list") + defaults = argparse.Namespace( + verbosity=logging.CRITICAL, + logger="" + ) + config = parser.parse_args(argv, namespace=defaults) + return config + + +if __name__ == "__main__": + options = get_options() + logging.basicConfig(stream=sys.stderr, level=options.verbosity) + logger = logging.getLogger("test") + for logger_name in (n.strip() for n in options.logger.split(',')): + l = logging.getLogger(logger_name) + l.setLevel(options.verbosity) + logger.info(f"Setting {l}") + + tr = unittest.TextTestRunner() + result = tr.run(suite()) + logging.shutdown() + sys.exit(len(result.failures) + len(result.errors)) + diff --git a/tests/runner.w b/tests/runner.w new file mode 100644 index 0000000..6edbcd1 --- /dev/null +++ b/tests/runner.w @@ -0,0 +1,98 @@ +Combined Test Runner +===================== + +.. test/runner.w + +This is a small runner that executes all tests in all test modules. +Instead of test discovery as done by **pytest** and others, +this defines a test suite "the hard way" with an explicit list of modules. + +@o runner.py +@{@ +@ +@ +@ +@} + +The overheads import unittest and logging, because those are essential +infrastructure. Additionally, each of the test modules is also imported. + +@d Combined Test overheads... +@{"""Combined tests.""" +import argparse +import unittest +import test_loader +import test_tangler +import test_weaver +import test_unit +import logging +import sys + +@} + +The test suite is built from each of the individual test modules. + +@d Combined Test suite... +@{ +def suite(): + s = unittest.TestSuite() + for m in (test_loader, test_tangler, test_weaver, test_unit): + s.addTests(unittest.defaultTestLoader.loadTestsFromModule(m)) + return s +@} + +In order to debug failing tests, we accept some command-line +parameters to the combined testing script. + +@d Combined Test command line options... +@{ +def get_options(argv: list[str] = sys.argv[1:]) -> argparse.Namespace: + parser = argparse.ArgumentParser() + parser.add_argument("-v", "--verbose", dest="verbosity", action="store_const", const=logging.INFO) + parser.add_argument("-d", "--debug", dest="verbosity", action="store_const", const=logging.DEBUG) + parser.add_argument("-l", "--logger", dest="logger", action="store", help="comma-separated list") + defaults = argparse.Namespace( + verbosity=logging.CRITICAL, + logger="" + ) + config = parser.parse_args(argv, namespace=defaults) + return config +@} + +This means we can use ``-dlWebReader`` to debug the Web Reader. +We can use ``-d -lWebReader,TanglerMake`` to debug both +the WebReader class and the TanglerMake class. Not all classes have named loggers. +Logger names include ``Emitter``, +``indent.Emitter``, +``Chunk``, +``Command``, +``Reference``, +``Web``, +``WebReader``, +``Action``, and +``Application``. +As well as subclasses of Emitter, Chunk, Command, and Action. + +The main script initializes logging. Note that the typical setup +uses ``logging.CRITICAL`` to silence some expected warning messages. +For debugging, ``logging.WARN`` provides more information. + +Once logging is running, it executes the ``unittest.TextTestRunner`` on the test suite. + + +@d Combined Test main... +@{ +if __name__ == "__main__": + options = get_options() + logging.basicConfig(stream=sys.stderr, level=options.verbosity) + logger = logging.getLogger("test") + for logger_name in (n.strip() for n in options.logger.split(',')): + l = logging.getLogger(logger_name) + l.setLevel(options.verbosity) + logger.info(f"Setting {l}") + + tr = unittest.TextTestRunner() + result = tr.run(suite()) + logging.shutdown() + sys.exit(len(result.failures) + len(result.errors)) +@} diff --git a/tests/scripts.w b/tests/scripts.w new file mode 100644 index 0000000..893e2e8 --- /dev/null +++ b/tests/scripts.w @@ -0,0 +1,227 @@ +Additional Scripts Testing +========================== + +.. test/scripts.w + +We provide these two additional scripts; effectively command-line short-cuts: + +- ``tangle.py`` + +- ``weave.py`` + +These need their own test cases. + + +This gives us the following outline for the script testing. + +@o test_scripts.py +@{@