From fdd413681a96e5824d05f03a10b75e0675058d9b Mon Sep 17 00:00:00 2001 From: "S.Lott" Date: Fri, 10 Jun 2022 10:39:24 -0400 Subject: [PATCH 1/8] Modernize to Python 3.10 1. Add type hints and strict mypy check 2. Replace all ``.format()`` with f-strings. --- .gitignore | 13 +- Makefile | 22 + README | 27 +- additional.w | 197 +- docutils.conf | 2 +- done.w | 12 + impl.w | 1952 ++++++++-------- intro.w | 132 +- overview.w | 18 +- pyproject.toml | 21 + pyweb.py | 1813 +++++++-------- pyweb.rst | 3787 ++++++++++++++++--------------- pyweb.w | 3 +- requirements-dev.txt | 5 + setup.py | 6 +- tangle.py | 28 +- test/combined.rst | 3081 +++++++++++++++++++++++++ test/combined.w | 83 +- test/docutils.conf | 6 + test/func.w | 438 ++-- test/intro.w | 102 +- test/page-layout.css | 17 + test/pyweb_test.html | 5094 ++++++++++++++++++++++-------------------- test/pyweb_test.rst | 3351 +++++++++++++++++++++++++++ test/pyweb_test.w | 129 +- test/test.py | 38 +- test/test_latex.log | 103 + test/test_latex.w | 4 +- test/test_loader.py | 108 +- test/test_rest.tex | 207 ++ test/test_rst.log | 809 +++++++ test/test_rst.pdf | Bin 0 -> 22850 bytes test/test_rst.tex | 311 +-- test/test_rst.w | 5 +- test/test_tangler.py | 147 +- test/test_unit.py | 1351 ++++++----- test/test_weaver.py | 122 +- test/testtangler | 1 + test/testweaver.rst | 4 + test/unit.w | 1653 ++++++++------ todo.w | 35 +- weave.py | 56 +- 42 files changed, 17140 insertions(+), 8153 deletions(-) create mode 100644 Makefile create mode 100644 pyproject.toml create mode 100644 requirements-dev.txt create mode 100644 test/combined.rst create mode 100644 test/docutils.conf create mode 100644 test/page-layout.css create mode 100644 test/pyweb_test.rst create mode 100644 test/test_latex.log create mode 100644 test/test_rest.tex create mode 100644 test/test_rst.log create mode 100644 test/test_rst.pdf create mode 100644 test/testtangler create mode 100644 test/testweaver.rst diff --git a/.gitignore b/.gitignore index 5636c11..2f9d84b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,14 @@ dev.sh -.idea/* +.idea +pyweb-3.0.py +__pycache__ +test/__pycache__/*.pyc +test/.svn/* +py_web_tool.egg-info/* +*.pyc +*.aux +*.out +*.toc +v2_test +.tox diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..730a0b7 --- /dev/null +++ b/Makefile @@ -0,0 +1,22 @@ +# Makefile for py-web-tool. +# Requires a pyweb-3.0.py (untouched) to bootstrap the current version. + +SOURCE = pyweb.w intro.w overview.w impl.w tests.w additional.w todo.w done.w \ + test/pyweb_test.w test/intro.w test/unit.w test/func.w test/combined.w + +.PHONY : test build + +# Note the bootstrapping new version from version 3.0 as baseline. + +test : $(SOURCE) + python3 pyweb-3.0.py -xw pyweb.w + cd test && python3 ../pyweb.py pyweb_test.w + cd test && PYTHONPATH=.. python3 test.py + cd test && rst2html.py pyweb_test.rst pyweb_test.html + mypy --strict pyweb.py + +build : pyweb.py pyweb.html + +pyweb.py pyweb.html : $(SOURCE) + python3 pyweb-3.0.py pyweb.w + diff --git a/README b/README index 119ca3a..fd24c02 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,16 +22,24 @@ applying a weave operation to the ``.w`` file. Installation ------------- +This requires Python 3.10. + +First, downnload the distribution kit from PyPI. + :: python3 setup.py install -This will install the pyweb module. +This will install the ``pyweb`` module, and the ``weave`` and ``tangle`` applications. + +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 :: @@ -41,16 +49,16 @@ The supplied documentation uses RST markup and requires docutils. 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. 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 :: @@ -81,5 +89,6 @@ execute all tests. python3 -m pyweb pyweb_test.w PYTHONPATH=.. python3 test.py rst2html.py pyweb_test.rst pyweb_test.html + mypy --strict pyweb.py diff --git a/additional.w b/additional.w index 57fa01d..3e24495 100644 --- a/additional.w +++ b/additional.w @@ -39,27 +39,27 @@ import pyweb import logging import argparse -with pyweb.Logger( pyweb.log_config ): - logger= logging.getLogger(__file__) +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(), + 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() + w = pyweb.Web() for action in LoadAction(), TangleAction(): - action.web= w - action.options= options + action.web = w + action.options = options action() - logger.info( action.summary() ) + logger.info(action.summary()) @} @@ -89,27 +89,27 @@ import string @d weave.py custom weaver definition... @{ -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""") @@ -118,49 +118,49 @@ class MyHTML( pyweb.HTML ): '${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}') @} @d weaver.py processing... @{ -with pyweb.Logger( pyweb.log_config ): - logger= logging.getLogger(__file__) +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(), + 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() + w = pyweb.Web() for action in LoadAction(), WeaveAction(): - action.web= w - action.options= options + action.web = w + action.options = options action() - logger.info( action.summary() ) + logger.info(action.summary()) @} -The ``setup.py`` and ``MANIFEST.in`` files --------------------------------------------- +The ``setup.py``, ``requirements-dev.txt`` and ``MANIFEST.in`` files +--------------------------------------------------------------------- In order to support a pleasant installation, the ``setup.py`` file is helpful. @@ -170,9 +170,9 @@ In order to support a pleasant installation, the ``setup.py`` file is helpful. 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='pyWeb 3.1: Yet Another Literate Programming Tool', author='S. Lott', author_email='s_lott@@yahoo.com', url='http://slott-softwarearchitect.blogspot.com/', @@ -197,13 +197,23 @@ include test/*.w test/*.css test/*.html test/*.conf test/*.py include jedit/*.xml @} +In order to install dependencies, the following file is also used. + +@o requirements-dev.txt +@{ +docutils==0.18.1 +tox==3.25.0 +mypy==0.910 +pytest == 7.1.2 +@} + The ``README`` file --------------------- Here's the README file. @o README -@{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 @@ -219,7 +229,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. @@ -227,16 +237,24 @@ applying a weave operation to the ``.w`` file. Installation ------------- +This requires Python 3.10. + +First, downnload the distribution kit from PyPI. + :: python3 setup.py install -This will install the pyweb module. +This will install the ``pyweb`` module, and the ``weave`` and ``tangle`` applications. + +Produce Documentation +--------------------- + +The supplied documentation uses RST markup; it requires docutils. -Document production --------------------- +:: -The supplied documentation uses RST markup and requires docutils. + python3 -m pip install docutils :: @@ -246,16 +264,16 @@ The supplied documentation uses RST markup and requires docutils. 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. 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 :: @@ -286,6 +304,7 @@ execute all tests. python3 -m pyweb pyweb_test.w PYTHONPATH=.. python3 test.py rst2html.py pyweb_test.rst pyweb_test.html + mypy --strict pyweb.py @} @@ -303,7 +322,7 @@ installation of docutils. @{# 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 @} @@ -338,7 +357,7 @@ bug in ``NamedChunk.tangle()`` that prevents handling zero-length text. @{ @} -Finally, an ``index.html`` to redirect GitHub to the ``pyweb.html`` file. +Here's an ``index.html`` to redirect GitHub to the ``pyweb.html`` file. @o index.html @{ @@ -349,4 +368,64 @@ Finally, an ``index.html`` to redirect GitHub to the ``pyweb.html`` file. Sorry, you should have been redirected pyweb.html. -@} \ No newline at end of file +@} + + +Tox and Makefile +---------------- + +It's simpler to have a ``Makefile`` to automate testing, particularly when making changes +to **py-web-tool**. + +Note that there are tabs in this file. We bootstrap the next version from the 3.0 version. + +@o Makefile +@{# Makefile for py-web-tool. +# Requires a pyweb-3.0.py (untouched) to bootstrap the current version. + +SOURCE = pyweb.w intro.w overview.w impl.w tests.w additional.w todo.w done.w \ + test/pyweb_test.w test/intro.w test/unit.w test/func.w test/combined.w + +.PHONY : test build + +# Note the bootstrapping new version from version 3.0 as baseline. + +test : $(SOURCE) + python3 pyweb-3.0.py -xw pyweb.w + cd test && python3 ../pyweb.py pyweb_test.w + cd test && PYTHONPATH=.. python3 test.py + cd test && rst2html.py pyweb_test.rst pyweb_test.html + mypy --strict pyweb.py + +build : pyweb.py pyweb.html + +pyweb.py pyweb.html : $(SOURCE) + python3 pyweb-3.0.py pyweb.w + +@} + +**TODO:** Finish ``tox.ini`` or ``pyproject.toml``. + +@o pyproject.toml +@{ +[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 = """ +[tox] +envlist = py310 + +[testenv] +deps = + pytest == 7.1.2 + mypy == 0.910 +commands_pre = + python3 pyweb-3.0.py pyweb.w + python3 pyweb.py -o test test/pyweb_test.w +commands = + python3 test/test.py + mypy --strict pyweb.py +""" +@} 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/done.w b/done.w index 08e9ad5..3bfc99f 100644 --- a/done.w +++ b/done.w @@ -3,6 +3,18 @@ Change Log =========== +Changes for 3.1 + +- Change to Python 3.10. + +- Add type hints, f-strings, pathlib. + +- Replace some complex elif blocks with match statements + +- Remove the Jedit configuration file as an output. + +- Add a ``Makefile``, ``pyproject.toml``, ``requirements.txt`` and ``requirements-dev.txt``. + Changes for 3.0 - Move to GitHub diff --git a/impl.w b/impl.w index 2ea409f..f1235fa 100644 --- a/impl.w +++ b/impl.w @@ -134,7 +134,7 @@ 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, +- Boilerplate text to replace various **py-web-tool** constructs, - Escape rules to make source code amenable to the markup language, @@ -206,7 +206,7 @@ directs us to factor the basic open(), close() and write() methods into two step .. parsed-literal:: - def open( self ): + def open(self) -> "Emitter": *common preparation* self.doOpen() *#overridden by subclasses* return self @@ -255,19 +255,26 @@ The ``codeBlock()`` method to indent each line written. @{ 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 + + theFile: TextIO + def __init__(self) -> None: + self.fileName = "" + self.logger = logging.getLogger(self.__class__.__qualname__) + self.log_indent = logging.getLogger("indent." + self.__class__.__qualname__) + # Summary + self.linesWritten = 0 + self.totalFiles = 0 + self.totalLines = 0 + # Working State + self.lastIndent = 0 + self.fragment = False + self.context: list[int] = [] + self.readdIndent(self.code_indent) # Create context and initial lastIndent values + + def __str__(self) -> str: return self.__class__.__name__ + @ @ @ @@ -290,28 +297,32 @@ characters to the file. @d Emitter core... @{ -def open( self, aFile ): +def open(self, aFile: str) -> "Emitter": """Open a file.""" - self.fileName= aFile - self.linesWritten= 0 - self.doOpen( aFile ) + self.fileName = aFile + self.linesWritten = 0 + self.doOpen(aFile) return self + @ -def close( self ): + +def close(self) -> None: self.codeFinish() # Trailing newline for tangler only. self.doClose() self.totalFiles += 1 self.totalLines += self.linesWritten + @ -def write( self, text ): + +def write(self, text: str) -> None: if text is None: return self.linesWritten += text.count('\n') - self.theFile.write( text ) + 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 @@ -323,15 +334,16 @@ methods are overridden by the various subclasses to perform the unique operation for the subclass. @d Emitter doOpen... @{ -def doOpen( self, aFile ): - self.logger.debug( "creating {!r}".format(self.fileName) ) +def doOpen(self, aFile: str) -> None: + self.logger.debug("creating %r", self.fileName) @| doOpen @} @d Emitter doClose... @{ -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.fileName + ) @| doClose @} @@ -384,29 +396,38 @@ a NamedChunk. It's not really a general feature of emitters or even tanglers. @d Emitter write a block... @{ -def codeBlock( self, text ): +def codeBlock(self, text: str) -> None: """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 + 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: + # Last line was empty, a trailing newline. + self.logger.debug("Last (Empty) Line: indent is %d", len(rest[-1]) + indent) # Buffer a next indent - self.lastIndent= len(rest[-1]) + indent - self.fragment= False + self.lastIndent = len(rest[-1]) + indent + self.fragment = False @| codeBlock @} @@ -422,15 +443,15 @@ HTML these will not be altered. @d Emitter write a block... @{ -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 @| quote @} @@ -439,7 +460,7 @@ The ``codeFinish()`` method handles a trailing fragmentary line when tangling. @d Emitter write a block... @{ -def codeFinish( self ): +def codeFinish(self) -> None: if self.fragment: self.write('\n') @| codeFinish @@ -474,23 +495,24 @@ requires this. ``readdIndent()`` uses this initial offset for weaving. @d Emitter indent control... @{ -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) @| addIndent clrIndent readdIndent addIndent @} @@ -499,7 +521,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 @@ -547,19 +569,20 @@ Instance-level configuration values: @d Weaver subclass of Emitter... @{ -class Weaver( Emitter ): +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 - header= """\n.. include:: \n.. include:: \n""" + extension = ".rst" + code_indent = 4 + header = """\n.. include:: \n.. include:: \n""" + + reference_style : "Reference" - def __init__( self ): + def __init__(self) -> None: super().__init__() - self.reference_style= None # Must be configured. @ @@ -587,20 +610,19 @@ we're not always starting a fresh line with ``weaveReferenceTo()``. @d Weaver doOpen... @{ -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, basename: str) -> None: + self.fileName = basename + self.extension + self.logger.info("Weaving %r", self.fileName) + self.theFile = open(self.fileName, "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.fileName) +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 @| doOpen doClose addIndent codeFinish @} @@ -615,7 +637,7 @@ to look for paired RST inline markup and quote just these special character occu @d Weaver quoted characters... @{ -quoted_chars = [ +quoted_chars: list[tuple[str, str]] = [ # prevent some RST markup from being recognized ('\\',r'\\'), # Must be first. ('`',r'\`'), @@ -636,9 +658,9 @@ of possible additional processing. @d Weaver document... @{ -def docBegin( self, aChunk ): +def docBegin(self, aChunk: Chunk) -> None: pass -def docEnd( self, aChunk ): +def docEnd(self, aChunk: Chunk) -> None: pass @| docBegin docEnd @} @@ -653,16 +675,16 @@ Each code chunk includes the places where the chunk is referenced. @d Weaver reference summary... @{ -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 ) + 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 ) ) + return self.ref_template.substitute(refList=self.ref_separator.join(refList)) else: return "" @| references @@ -680,25 +702,25 @@ refer to this chunk can be emitted. @d Weaver code... @{ -cb_template = string.Template( "\n.. _`${seq}`:\n.. rubric:: ${fullName} (${seq}) ${concat}\n.. parsed-literal::\n :class: code\n\n" ) +cb_template = string.Template("\n.. _`${seq}`:\n.. rubric:: ${fullName} (${seq}) ${concat}\n.. parsed-literal::\n :class: code\n\n") -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 "+=", # RST Separator ) - self.write( txt ) + self.write(txt) -ce_template = string.Template( "\n..\n\n .. class:: small\n\n |loz| *${fullName} (${seq})*. Used by: ${references}\n" ) +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) @| codeBegin codeEnd @@ -717,27 +739,27 @@ list is always empty. @d Weaver file... @{ -fb_template = string.Template( "\n.. _`${seq}`:\n.. rubric:: ${fullName} (${seq}) ${concat}\n.. parsed-literal::\n :class: code\n\n" ) +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 +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 "+=", # 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 ) + self.write(txt) + +fe_template = string.Template("\n..\n\n .. class:: small\n\n |loz| *${fullName} (${seq})*.\n") + +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) @| fileBegin fileEnd @} @@ -755,20 +777,20 @@ a simple ``" "`` because it looks better. @d Weaver reference command... @{ -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("|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.""" 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 @| referenceTo referenceSep @@ -799,43 +821,45 @@ to change the look of the final woven document. @d Weaver cross reference... @{ -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("\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 ): +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()) @} Cross-reference definition line @d Weaver cross reference... @{ -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) @| xrefHead xrefFoot xrefLine xrefDefLine @} @@ -863,10 +887,10 @@ given to the ``weave()`` method of the Web. .. parsed-literal:: - w= Web() + w = Web() WebReader().load(w,"somefile.w") - weave_latex= LaTeX() - w.weave( weave_latex ) + 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 @@ -881,13 +905,13 @@ function pretty well in most L\ !sub:`A`\ T\ !sub:`E`\ X documents. @d LaTeX subclass... @{ -class LaTeX( Weaver ): +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""" + extension = ".tex" + code_indent = 0 + header = """\n\\usepackage{fancyvrb}\n""" @ @ @@ -926,7 +950,7 @@ indentation. @d LaTeX code chunk end @{ -ce_template= string.Template(""" +ce_template = string.Template(""" \\end{Verbatim} ${references} \\end{flushleft}\n""") # Prevent indentation @@ -941,7 +965,7 @@ start of a code chunk. @d LaTeX file output begin @{ -fb_template= cb_template +fb_template = cb_template @| fileBegin @} @@ -952,7 +976,7 @@ invokes this chunk, and restores normal indentation. @d LaTeX file output end @{ -fe_template= ce_template +fe_template = ce_template @| fileEnd @} @@ -984,7 +1008,7 @@ block. Our one compromise is a thin space if the phrase @d LaTeX write a line... @{ -quoted_chars = [ +quoted_chars: list[tuple[str, str]] = [ ("\\end{Verbatim}", "\\end\,{Verbatim}"), # Allow \end{Verbatim} ("\\{","\\\,{"), # Prevent unexpected commands in Verbatim ("$","\\$"), # Prevent unexpected math in Verbatim @@ -999,8 +1023,8 @@ the current line of code. @d LaTeX reference to... @{ -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})""") @| referenceTo @} @@ -1016,10 +1040,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 @@ -1039,11 +1063,11 @@ with abbreviated (no name) cross references at the end of the chunk. @d HTML subclass... @{ -class HTML( Weaver ): +class HTML(Weaver): """HTML formatting for XRef's and code blocks when weaving.""" - extension= ".html" - code_indent= 0 - header= "" + extension = ".html" + code_indent = 0 + header = "" @ @ @ @@ -1057,7 +1081,7 @@ class HTML( Weaver ): @d HTML subclass... @{ -class HTMLShort( HTML ): +class HTMLShort(HTML): """HTML formatting for XRef's and code blocks when weaving with short references.""" @ @| HTML @@ -1069,7 +1093,7 @@ and HTML tags necessary to set the code off visually. @d HTML code chunk begin @{ -cb_template= string.Template(""" +cb_template = string.Template("""

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

@@ -1083,7 +1107,7 @@ write the list of chunks that reference this chunk. @d HTML code chunk end @{ -ce_template= string.Template(""" +ce_template = string.Template("""

${fullName} (${seq}). ${references} @@ -1096,7 +1120,7 @@ and HTML tags necessary to set the code off visually. @d HTML output file begin @{ -fb_template= string.Template(""" +fb_template = string.Template("""

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

\n""") # Prevent indent
@@ -1109,7 +1133,7 @@ write the list of chunks that reference this chunk.
 
 @d HTML output file end
 @{
-fe_template= string.Template( """
+fe_template = string.Template( """

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

\n""") @@ -1122,10 +1146,8 @@ transitive references. @d HTML references summary... @{ -ref_item_template = string.Template( -'${fullName} (${seq})' -) -ref_template = string.Template( ' Used by ${refList}.' ) +ref_item_template = string.Template('${fullName} (${seq})') +ref_template = string.Template(' Used by ${refList}.' ) @| references @} @@ -1135,7 +1157,7 @@ as HTML. @d HTML write a line of code @{ -quoted_chars = [ +quoted_chars: list[tuple[str, str]] = [ ("&", "&"), # Must be first ("<", "<"), (">", ">"), @@ -1150,12 +1172,8 @@ surrounding source code. @d HTML reference to a chunk @{ -refto_name_template = string.Template( -'${fullName} (${seq})' -) -refto_seq_template = string.Template( -'(${seq})' -) +refto_name_template = string.Template('${fullName} (${seq})') +refto_seq_template = string.Template('(${seq})') @| referenceTo @} @@ -1170,9 +1188,9 @@ The ``xrefLine()`` method writes a line for the file or macro cross reference bl @d HTML simple cross reference markup @{ -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") @ @| xrefHead xrefFoot xrefLine @} @@ -1184,8 +1202,8 @@ is included in the correct order with the other instances, but is bold and marke @d HTML write user id cross reference line @{ -name_def_template = string.Template( '•${seq}' ) -name_ref_template = string.Template( '${seq}' ) +name_def_template = string.Template('•${seq}') +name_ref_template = string.Template('${seq}') @| xrefDefLine @} @@ -1197,7 +1215,7 @@ transitive references. @d HTML short references summary... @{ -ref_item_template = string.Template( '(${seq})' ) +ref_item_template = string.Template('(${seq})') @| references @} @@ -1209,10 +1227,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 @@ -1240,13 +1258,13 @@ There are three configurable values: @d Tangler subclass of Emitter... @{ -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 + self.comment_start: str = "#" + self.comment_end: str = "" + self.include_line_numbers = False @ @ @ @@ -1264,24 +1282,23 @@ actual file created by open. @d Tangler doOpen... @{ -def checkPath( self ): +def checkPath(self) -> None: if "/" in self.fileName: dirname, _, _ = self.fileName.rpartition("/") try: - os.makedirs( dirname ) - self.logger.info( "Creating {!r}".format(dirname) ) - except OSError as e: + os.makedirs(dirname) + self.logger.info("Creating %r", dirname) + except OSError as exc: # 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.logger.debug("Exception %r creating %r", exc, dirname) +def doOpen(self, aFile: str) -> None: + self.fileName = aFile self.checkPath() - self.theFile= open( aFile, "w" ) - self.logger.info( "Tangling {!r}".format(aFile) ) -def doClose( self ): + self.theFile = open(aFile, "w") + self.logger.info("Tangling %r", aFile) +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.fileName) @| doOpen doClose @} @@ -1291,18 +1308,14 @@ prevailing indent at the start of the ``@@<`` reference command. @d Tangler code chunk begin @{ -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) @| codeEnd @} @@ -1326,15 +1339,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. @@ -1350,12 +1363,14 @@ import filecmp @d TanglerMake subclass... @{ -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 + tempname : str + def __init__(self, *args: Any) -> None: + super().__init__(*args) + @ + @ @| TanglerMake @} @@ -1368,10 +1383,10 @@ a "touch" if the new file is the same as the original. @d TanglerMake doOpen... @{ -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, aFile: str) -> None: + fd, self.tempname = tempfile.mkstemp(dir=os.curdir) + self.theFile = os.fdopen(fd, "w") + self.logger.info("Tangling %r", aFile) @| doOpen @} @@ -1386,25 +1401,24 @@ and time) if nothing has changed. @d TanglerMake doClose... @{ -def doClose( self ): +def doClose(self) -> None: self.theFile.close() try: - same= filecmp.cmp( self.tempname, self.fileName ) + same = filecmp.cmp(self.tempname, self.fileName) 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("No change to %r", self.fileName) + os.remove(self.tempname) else: # Windows requires the original file name be removed first. self.checkPath() try: - os.remove( self.fileName ) + 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) ) + os.rename(self.tempname, self.fileName) + self.logger.info("Wrote %e lines to %r", self.linesWritten, self.fileName) @| doClose @} @@ -1469,11 +1483,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. @@ -1484,13 +1498,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. @@ -1498,7 +1512,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()``, @@ -1578,22 +1592,26 @@ The ``Chunk`` constructor initializes the following instance variables: @{ 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 ) + web : weakref.ReferenceType["Web"] + previous_command : "Command" + initial: bool + 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.fileName = '' + 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})" + @ @ @ @@ -1613,10 +1631,10 @@ The ``append()`` method simply appends a ``Command`` instance to this chunk. @d Chunk append a command @{ -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 @| append @} @@ -1632,17 +1650,17 @@ be a separate ``TextCommand`` because it will wind up indented. @d Chunk append text @{ -def appendText( self, text, lineNumber=0 ): +def appendText(self, text: str, lineNumber: int = 0) -> None: """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) ) + 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) ) + self.commands.append(self.makeContent(text,lineNumber)) @| appendText @} @@ -1654,9 +1672,9 @@ of the ``Web`` class to append an anonymous, unindexed chunk. @d Chunk add to the web @{ -def webAdd( self, web ): +def webAdd(self, web: "Web") -> None: """Add self to a Web as anonymous chunk.""" - web.add( self ) + web.add(self) @| webAdd @} @@ -1669,8 +1687,8 @@ A Named Chunk using ``@@[`` and ``@@]`` creates text. @d Chunk superclass make Content... @{ -def makeContent( self, text, lineNumber=0 ): - return TextCommand( text, lineNumber ) +def makeContent(self, text: str, lineNumber: int = 0) -> Command: + return TextCommand(text, lineNumber) @| makeContent @} @@ -1692,25 +1710,34 @@ The ``searchForRE()`` method examines each ``Command`` instance to see if it mat 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``. +@d Imports... +@{from typing import Pattern, Match, Optional, Any, Literal +@} + @d Chunk examination... @{ -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 [] @| startswith searchForRE lineNumber getUserIDRefs @} @@ -1729,11 +1756,11 @@ context information. @d Chunk generate references... @{ -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: @@ -1750,10 +1777,12 @@ The Weaver pushed it into the Web so that it is available for each ``Chunk``. @d Chunk references... @{ -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) + ] @} The ``weave()`` method weaves this chunk into the final document as follows: @@ -1773,16 +1802,16 @@ context information. @d Chunk weave... @{ -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.""") @| weave weaveReferenceTo weaveShortReferenceTo @@ -1793,9 +1822,9 @@ problem with this program or the input file. @d Chunk tangle... @{ -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) @| tangle @} @@ -1806,10 +1835,10 @@ left margin by forcing the local indent to zero. @d Chunk indent adjustments... @{ -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() @} @@ -1864,17 +1893,20 @@ This class introduces some additional attributes. @d NamedChunk class @{ -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 ) + 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) + @ @ @ @@ -1888,10 +1920,10 @@ in a ``@@d`` named chunk. These are used by the ``@@u`` cross reference generat @d NamedChunk user identifiers... @{ -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 @| setUserIDRefs getUserIDRefs @} @@ -1903,9 +1935,9 @@ of the ``Web`` class to append a named chunk. @d NamedChunk add to the web @{ -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) @| webAdd @} @@ -1934,24 +1966,24 @@ The woven references simply follow whatever preceded them on the line; the inden @d NamedChunk weave... @{ -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) @| weave weaveReferenceTo weaveShortReferenceTo @} @@ -1970,20 +2002,20 @@ context information. @d NamedChunk tangle... @{ -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) @| tangle @} @@ -1992,12 +2024,12 @@ context. It simply sets an indent at the left margin. @d NamedChunk class @{ -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() @} @@ -2021,12 +2053,12 @@ All other methods, including the tangle method are identical to ``NamedChunk``. @d OutputChunk class @{ -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 + 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 @ @ @ @@ -2040,9 +2072,9 @@ of the ``Web`` class to append a file output chunk. @d OutputChunk add to the web @{ -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) @| webAdd @} @@ -2064,13 +2096,13 @@ context information. @d OutputChunk weave @{ -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) @| weave @} @@ -2079,10 +2111,10 @@ to be sure that -- if line numbers were requested -- they can be included proper @d OutputChunk tangle @{ -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) @} NamedDocumentChunk class @@ -2107,10 +2139,12 @@ All other methods, including the tangle method are identical to ``NamedChunk``. @d NamedDocumentChunk class @{ -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 ) + + def makeContent(self, text: str, lineNumber: int = 0) -> Command: + return TextCommand(text, lineNumber) + @ @ @| NamedDocumentChunk makeContent @@ -2130,24 +2164,24 @@ to insert the entire chunk. @d NamedDocumentChunk weave @{ -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) @| weave weaveReferenceTo weaveShortReferenceTo @} @d NamedDocumentChunk tangle @{ -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 @@[.""") @| tangle @} @@ -2193,7 +2227,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 @@ -2245,12 +2279,15 @@ the command began, in ``lineNumber``. @{ 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) + 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}" + @ @ @| Command @@ -2258,22 +2295,22 @@ class Command: @d Command analysis features... @{ -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 @| startswith searchForRE @} @d Command tangle and weave... @{ -def ref( self, aWeb ): +def ref(self, aWeb: "Web") -> str | None: return None -def weave( self, aWeb, aWeaver ): +def weave(self, aWeb: "Web", aWeaver: "Weaver") -> None: pass -def tangle( self, aWeb, aTangler ): +def tangle(self, aWeb: "Web", aTangler: "Tangler") -> None: pass @| ref weave tangle @} @@ -2295,21 +2332,24 @@ 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 to snip off first 32 chars of the text. @d TextCommand class... @{ -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: @@ -2317,10 +2357,10 @@ class TextCommand( Command ): 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 startswith searchForRE weave tangle @} @@ -2346,12 +2386,12 @@ indentation is maintained. @d CodeCommand class... @{ -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 weave tangle @} @@ -2382,16 +2422,18 @@ is illegal. An exception is raised and processing stops. @d XrefCommand superclass... @{ -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 formatXref tangle @} @@ -2410,11 +2452,11 @@ the ``formatXref()`` method of the ``XrefCommand`` superclass for format this r @d FileXrefCommand class... @{ -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 weave @} @@ -2432,11 +2474,11 @@ the ``formatXref()`` method of the ``XrefCommand`` superclass method for format @d MacroXrefCommand class... @{ -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 weave @} @@ -2463,16 +2505,16 @@ algorithm, which is similar to the algorithm in the ``XrefCommand`` superclass. @d UserIdXrefCommand class... @{ -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() @@ -2509,16 +2551,18 @@ of a ``ReferenceCommand``. @d ReferenceCommand class... @{ -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) + 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}" + @ @ @ @@ -2534,10 +2578,10 @@ to the chunk. @d ReferenceCommand resolve... @{ -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) @| resolve @} @@ -2549,9 +2593,9 @@ Chinks to which it refers. @d ReferenceCommand refers to a chunk @{ -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 @| usedBy @} @@ -2563,10 +2607,10 @@ this appropriately for the document type being woven. @d ReferenceCommand weave... @{ -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) @| weave @} @@ -2580,21 +2624,21 @@ Chunk is a no-indent Chunk. @d ReferenceCommand tangle... @{ -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) @| tangle @} @@ -2620,11 +2664,11 @@ this object. @d Reference class hierarchy... @{ class Reference: - def __init__( self ): - self.logger= logging.getLogger( self.__class__.__qualname__ ) - def chunkReferencedBy( self, aChunk ): + def __init__(self) -> None: + self.logger = logging.getLogger(self.__class__.__qualname__) + def chunkReferencedBy(self, aChunk: Chunk) -> list[Chunk]: """Return a list of Chunks.""" - pass + return [] @} SimpleReference Class @@ -2635,9 +2679,9 @@ the ``Chunks`` referenced. @d Reference class hierarchy... @{ -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 @} @@ -2652,19 +2696,19 @@ This requires walking through the ``Web`` to locate "parents" of each referenced @d Reference class hierarchy... @{ -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 @} @@ -2687,7 +2731,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: @@ -2696,9 +2740,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 @@ -2708,7 +2752,7 @@ but merely creates a distinct class to facilitate writing ``except`` statements. @d Error class... @{ -class Error( Exception ): pass +class Error(Exception): pass @| Error @} The Web and WebReader Classes @@ -2775,15 +2819,17 @@ A web instance has a number of attributes. @{ 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, filename: str | None = None) -> None: + self.webFileName = filename + 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.webFileName!r}" @ @ @@ -2867,16 +2913,16 @@ uses an abbreviated name. @d Web add full chunk names... @{ -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 @| addDefName @} @@ -2887,10 +2933,10 @@ tangling. @d Web add an anonymous chunk @{ -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) @| add @} @@ -2926,21 +2972,21 @@ in the list. Otherwise, it's False. @d Web add a named macro chunk @{ -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) @| addNamed @} @@ -2973,17 +3019,17 @@ If the chunk list was empty, this is the first chunk, the @d Web add an output file definition chunk @{ -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 @| addOutput @} @@ -3017,14 +3063,14 @@ The ``fullNameFor()`` method resolves full name for a chunk as follows: @d Web Chunk name resolution... @{ -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] ) ] + 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)) ) ) + raise Error(f"Ambiguous abbreviation {name!r}, matches {list(sorted(best))!r}") elif len(best) == 1: return best[0] return name @@ -3039,16 +3085,16 @@ 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** . @d Web Chunk name resolution... @{ -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}") @| getchunk @} @@ -3085,15 +3131,15 @@ reference, it also assures that all chunks are used exactly once. @d Web Chunk cross reference methods... @{ -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 @ @| createUsedBy @@ -3106,26 +3152,26 @@ a Chunk or unreferenced chunks. @d Web Chunk check... @{ 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 @} -The one-pass version +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) ) + 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 @@ -3139,12 +3185,12 @@ is a list of chunks referenced but not defined. @d Web Chunk cross reference methods... @{ -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] @| no_reference multi_reference no_definition @} @@ -3158,15 +3204,15 @@ but applies it to the ``named`` mapping. @d Web Chunk cross reference methods... @{ -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 @| fileXref chunkXref @} @@ -3178,7 +3224,7 @@ 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. @@ -3195,16 +3241,18 @@ This works in two passes: @d Web Chunk cross reference methods... @{ -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 ): + +def _gatherUserId(self, chunkMap: dict[str, list[Chunk]], ux: dict[str, tuple[int, list[int]]]) -> None: @ -def _updateUserId( self, chunkMap, ux ): + +def _updateUserId(self, chunkMap: dict[str, list[Chunk]], ux: dict[str, tuple[int, list[int]]]) -> None: @ @| userNamesXref _gatherUserId _updateUserId @} @@ -3222,7 +3270,7 @@ 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, []) @} User identifiers are cross-referenced by visiting @@ -3236,12 +3284,12 @@ this is appended to the sequence of chunks that reference the original user iden @{ # 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) @} Loop Detection @@ -3295,11 +3343,11 @@ Everything else is probably RST. @d Web determination of the language... @{ -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('\\'): @@ -3315,11 +3363,11 @@ the file be composed of material from each ``Chunk``, in order. @d Web tangle... @{ -def tangle( self, aTangler ): +def tangle(self, aTangler: "Tangler") -> None: for f, c in self.output.items(): with aTangler.open(f): for p in c: - p.tangle( self, aTangler ) + p.tangle(self, aTangler) @| tangle @} @@ -3339,21 +3387,24 @@ The decision is delegated to the referenced chunk. @d Web weave... @{ -def weave( self, aWeaver ): - self.logger.debug( "Weaving file from {!r}".format(self.webFileName) ) - basename, _ = os.path.splitext( self.webFileName ) +def weave(self, aWeaver: "Weaver") -> None: + self.logger.debug("Weaving file from %r", self.webFileName) + if not self.webFileName: + raise Error("No filename supplied for weaving.") + 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) + 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) @| weave weaveChunk @} @@ -3366,7 +3417,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. @@ -3376,7 +3427,7 @@ instance is created with code like the following: .. parsed-literal:: - c= WebReader( parent=parentWebReader ) + c = WebReader(parent=parentWebReader) @@ -3451,47 +3502,53 @@ 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='*'), + ) - def __init__( self, parent=None ): - self.logger= logging.getLogger( self.__class__.__qualname__ ) + # State of reading and parsing. + tokenizer: Tokenizer + aChunk: Chunk + + # Configuration + command: str + permitList: list[str] + + # State of the reader + _source: TextIO + fileName: str + theWeb: "Web" + + 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 @ - def __str__( self ): + + def __str__(self) -> str: return self.__class__.__name__ + @ @ @ @@ -3524,17 +3581,17 @@ A subclass can override ``handleCommand()`` to @d WebReader handle a command... @{ -def handleCommand( self, token ): - self.logger.debug( "Reading {!r}".format(token) ) +def handleCommand(self, token: str) -> bool: + self.logger.debug("Reading %r", token) @ @ 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.logger.error("Extra %r (possibly missing chunk name) near %r", token, self.location()) self.errors += 1 else: - return None # did not recogize the command - return True # did recognize the command + return False # did not recogize the command + return True # did recognize the command @| handleCommand @} @@ -3561,19 +3618,20 @@ 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)``. @d start an OutputChunk... @{ -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.fileName = self.fileName +self.aChunk.webAdd(self.theWeb) # capture an OutputChunk up to @@} @} @@ -3599,25 +3657,25 @@ If both are in the options, we can provide a warning, I guess. @d start a NamedChunk... @{ -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.fileName = self.fileName +self.aChunk.webAdd(self.theWeb) # capture a NamedChunk up to @@} or @@] @} @@ -3642,38 +3700,32 @@ 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. @d import another file @{ -incFile= next(self.tokenizer).strip() +incFile = next(self.tokenizer).strip() try: - self.logger.info( "Including {!r}".format(incFile) ) - include= WebReader( parent=self ) - include.load( self.theWeb, incFile ) + self.logger.info("Including %r", 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) ) + self.logger.error("Errors in included file %r, output is incomplete.", incFile) except Error as e: - self.logger.error( - "Problems with included file {!s}, output is incomplete.".format( - incFile) ) + self.logger.error("Problems with included file %r, output is incomplete.", incFile) 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 %r, output is incomplete.", 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 ) + else: raise # Seems heavy-handed, but, the file wasn't found! +self.aChunk = Chunk() +self.aChunk.webAdd(self.theWeb) @} When a ``@@}`` or ``@@]`` are found, this finishes a named chunk. The next @@ -3690,8 +3742,8 @@ For the base ``Chunk`` class, this would be false, but for all other subclasses @d finish a chunk... @{ -self.aChunk= Chunk() -self.aChunk.webAdd( self.theWeb ) +self.aChunk = Chunk() +self.aChunk.webAdd(self.theWeb) @} The following sequence of ``elif`` statements identifies @@ -3703,11 +3755,11 @@ the minor commands that add ``Command`` instances to the current open ``Chunk``. elif token[:2] == self.cmdpipe: @ elif token[:2] == self.cmdf: - self.aChunk.append( FileXrefCommand(self.tokenizer.lineNumber) ) + self.aChunk.append(FileXrefCommand(self.tokenizer.lineNumber)) elif token[:2] == self.cmdm: - self.aChunk.append( MacroXrefCommand(self.tokenizer.lineNumber) ) + self.aChunk.append(MacroXrefCommand(self.tokenizer.lineNumber)) elif token[:2] == self.cmdu: - self.aChunk.append( UserIdXrefCommand(self.tokenizer.lineNumber) ) + self.aChunk.append(UserIdXrefCommand(self.tokenizer.lineNumber)) elif token[:2] == self.cmdlangl: @ elif token[:2] == self.cmdlexpr: @@ -3731,10 +3783,10 @@ These are accumulated and expanded by ``@@u`` reference @d assign user identifiers... @{ 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 @} @@ -3745,12 +3797,12 @@ tokens from the input, the middle token is the referenced name. @d add a reference command... @{ # 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) @} An expression command has the form ``@@(``\ *Python Expression*\ ``@@)``. @@ -3762,7 +3814,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 @@ -3770,8 +3822,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('.')``. @d Imports... @{ @@ -3784,31 +3836,34 @@ import platform @d add an expression command... @{ # 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) + 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 ('breakpoint', 'compile', 'eval', 'exec', 'execfile', 'globals', 'help', 'input', 'memoryview', 'open', 'print', 'super', '__import__') + )) + globals = dict( + __builtins__=safe, + os=types.SimpleNamespace(path=os.path, getcwd=os.getcwd, name=os.name), + time=time, + 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) ) + 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) @} A double command sequence (``'@@@@'``, when the command is an ``'@@'``) has the @@ -3823,7 +3878,7 @@ largely seamless. @d double at-sign... @{ -self.aChunk.appendText( self.command, self.tokenizer.lineNumber ) +self.aChunk.appendText(self.command, self.tokenizer.lineNumber) @} The ``expect()`` method examines the @@ -3833,19 +3888,19 @@ This is used by ``handleCommand()``. @d WebReader handle a command... @{ -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 @| expect @} @@ -3856,7 +3911,7 @@ to correctly reference the original input files. @d WebReader location... @{ -def location( self ): +def location(self) -> tuple[str, int]: return (self.fileName, self.tokenizer.lineNumber+1) @| location @} @@ -3871,41 +3926,46 @@ 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. +@d Imports... +@{from typing import TextIO +@} + @d WebReader load... @{ -def load( self, web, filename, source=None ): - self.theWeb= web - self.fileName= filename +def load(self, web: "Web", filename: str, source: TextIO | None = None) -> "WebReader": + 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 + self.theWeb.webFileName = self.fileName if source: - self._source= source + self._source = source self.parse_source() else: - with open( self.fileName, "r" ) as self._source: + 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 + return self - self.aChunk= Chunk() # Initial anonymous chunk of text. - self.aChunk.webAdd( self.theWeb ) +def parse_source(self) -> None: + self.tokenizer = Tokenizer(self._source, self.command) + self.totalFiles += 1 - 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 ) + 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.warning('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) @| load parse @} @@ -3919,26 +3979,26 @@ command character. @d WebReader command literals @{ # 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' @} @@ -3980,29 +4040,30 @@ 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. @d Imports @{ import re +from collections.abc import Iterator, Iterable @| re @} @d Tokenizer class... @{ -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 @} @@ -4041,21 +4102,26 @@ 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. +@d Option Parser class... +@{ +class ParseError(Exception): pass +@} + @d Option Parser class... @{ 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) @} The parser breaks the text into words using ``shelex`` rules. @@ -4065,26 +4131,31 @@ final argument value. @d Option Parser class... @{ 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: @@ -4092,22 +4163,22 @@ class OptionParser: 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 @} In principle, we step through the trailers based on nargs counts. @@ -4120,11 +4191,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?? yield trailers[0], " ".join(final) - final= 0 + final = 0 trailers.pop(0) yield trailers[0], " ".join(final) @@ -4146,23 +4217,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" ) + 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" ) + 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 +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. @@ -4183,10 +4254,10 @@ that defines the application options, inputs and results. 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 @@ -4200,8 +4271,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() @@ -4227,14 +4298,16 @@ An ``Action`` has a number of common attributes. @{ 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 ) + 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 @@ -4246,9 +4319,9 @@ by a subclass. @d Action call... @{ -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() @| perform @} @@ -4257,11 +4330,12 @@ statistics for this action. @d Action final... @{ -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." @| duration summary @} @@ -4283,14 +4357,16 @@ an ``append()`` method that is used to construct the sequence of actions. @d ActionSequence subclass... @{ -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 ] ) + 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]) + @ @ @ @@ -4304,10 +4380,11 @@ sub-action. @d ActionSequence call... @{ -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() @| perform @} @@ -4316,8 +4393,8 @@ Since this class is essentially a wrapper around the built-in sequence type, we delegate sequence related actions directly to the underlying sequence. @d ActionSequence append... @{ -def append( self, anAction ): - self.opSequence.append( anAction ) +def append(self, anAction: Action) -> None: + self.opSequence.append(anAction) @| append @} @@ -4325,8 +4402,8 @@ The ``summary()`` method returns some basic processing statistics for each step of this action. @d ActionSequence summary... @{ -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]) @| summary @} @@ -4345,12 +4422,13 @@ 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. @d WeaveAction subclass... @{ -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}]" @ @ @@ -4365,20 +4443,18 @@ Weaving can only raise an exception when there is a reference to a chunk that is never defined. @d WeaveAction call... @{ -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 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.webFileName) #raise @| perform @} @@ -4388,11 +4464,12 @@ statistics for the weave action. @d WeaveAction summary... @{ -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}" @| summary @} @@ -4410,10 +4487,11 @@ This class overrides the ``__call__()`` method of the superclass. The options **must** include ``theTangler``, with the ``Tangler`` instance to be used. @d TangleAction subclass... @{ -class TangleAction( Action ): +class TangleAction(Action): """Tangle source files.""" - def __init__( self ): - super().__init__( "Tangle" ) + def __init__(self) -> None: + super().__init__("Tangle") + @ @ @| TangleAction @@ -4425,15 +4503,13 @@ with any of ``@@d`` or ``@@o`` and use ``@@{`` ``@@}`` brackets. @d TangleAction call... @{ -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 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.webFileName) #raise @| perform @} @@ -4442,11 +4518,12 @@ The ``summary()`` method returns some basic processing statistics for the tangle action. @d TangleAction summary... @{ -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}" @| summary @} @@ -4466,12 +4543,12 @@ The options **must** include ``webReader``, with the ``WebReader`` instance to b @d LoadAction subclass... @{ -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 ) + def __init__(self) -> None: + super().__init__("Load") + def __str__(self) -> str: + return f"Load [{self.webReader!s}, {self.web!s}]" @ @ @| LoadAction @@ -4497,23 +4574,22 @@ exceptions due to incorrect inputs. chunk reference cannot be resolved to a named chunk. @d LoadAction call... @{ -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.webFileName = self.options.webFileName + error = f"Problems with source file {self.options.webFileName!r}, no output produced." try: - self.webReader.load( self.web, self.options.webFileName ) + 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.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. @@ -4527,10 +4603,10 @@ The ``summary()`` method returns some basic processing statistics for the load action. @d LoadAction summary... @{ -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." + ) @| summary @} @@ -4625,7 +4701,7 @@ detailed usage information. @d Overheads -@{"""pyWeb Literate Programming - tangle and weave tool. +@{"""py-web-tool Literate Programming. Yet another simple literate programming tool derived from nuweb, implemented entirely in Python. @@ -4663,7 +4739,7 @@ We also sneak in a "DO NOT EDIT" warning that belongs in all generated applicati source files. @d Overheads -@{__version__ = """3.0""" +@{__version__ = """3.1""" ### DO NOT EDIT THIS FILE! ### It was created by @(thisApplication@), __version__='@(__version__@)'. @@ -4704,13 +4780,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 @@ -4729,9 +4805,10 @@ The configuration can be either a ``types.SimpleNamespace`` or an @d Application Class... @{ class Application: - def __init__( self ): - self.logger= logging.getLogger( self.__class__.__qualname__ ) + def __init__(self) -> None: + self.logger = logging.getLogger(self.__class__.__qualname__) @ + @ @ @| Application @@ -4827,26 +4904,26 @@ on these simple text values to create more useful objects. @d Application default options... @{ -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, ) -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]) @} The algorithm for parsing the command line parameters uses the built in @@ -4859,23 +4936,23 @@ instances. @d Application parse command line... @{ -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("files", nargs='+') + 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. """ @@ -4884,27 +4961,27 @@ def expand( self, config ): elif config.reference == 's': config.reference_style = SimpleReference() else: - raise Error( "Improper configuration" ) + raise Error("Improper configuration") 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() + 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 @@ -4935,33 +5012,32 @@ outermost main program. @d Application class process all... @{ -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 + self.theAction = self.doTangle elif config.skip.lower().startswith('t'): # not tangling == weaving - self.theAction= self.doWeave + 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.webFileName = f + self.theAction.web = w + self.theAction.options = config self.theAction() - self.logger.info( self.theAction.summary() ) + self.logger.info(self.theAction.summary()) @| process @} @@ -4989,16 +5065,16 @@ encoded in YAML and use that with ``logging.config.dictConfig``. @d Logging Setup @{ 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 @} @@ -5011,32 +5087,33 @@ used to gather additional information. @d Logging Setup @{ -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}, }, -) +} @} This seems a bit verbose; a separate configuration file might be better. @@ -5059,14 +5136,14 @@ as a weaver template configuration file. @d Interface Functions... @{ -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() @| main @} This can be extended by doing something like the following. @@ -5082,13 +5159,12 @@ 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``. - diff --git a/intro.w b/intro.w index 5fde886..831541d 100644 --- a/intro.w +++ b/intro.w @@ -15,11 +15,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 @@ -71,11 +72,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 @@ -83,7 +84,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. @@ -118,45 +119,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. @@ -164,7 +165,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 @@ -172,18 +173,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: @@ -234,17 +235,17 @@ 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. @@ -252,7 +253,7 @@ simple approach started by *nuweb* and adopted by **pyWeb**. Use Cases ----------- -**pyWeb** supports two use cases, `Tangle Source Files`_ and `Weave Documentation`_. +**py-web-tool** 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. @@ -271,7 +272,7 @@ 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``. +The sequence is ``./pyweb.py *theFile*.w``. Weave Documentation ~~~~~~~~~~~~~~~~~~~~ @@ -288,18 +289,18 @@ 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``. +The sequence is ``./pyweb.py *theFile*.w``. -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. @@ -308,7 +309,7 @@ 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: @@ -318,35 +319,42 @@ The sequence is as follows: python *theTest* >\ *aLog* ./pyweb.py -xt *theFile*\ .w +Another possibility includes the following: + .. parsed-literal:: + + ./pyweb.py -xw -pi *theFile*\ .w + python -m pytest *theTestFile* >\ *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. +runs the test, creating a log file. The third step weaves the final document, +including the test output. -Writing **pyWeb** ``.w`` Files -------------------------------- +Writing **py-web-tool** ``.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*. +The **py-web-tool** 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 +The **py-web-tool** 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. +**py-web-tool** 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** +When weaving, the tags are replaced with output markup. This means that **py-web-tool** is not **totally** independent of the output markup. The code chunks will have their indentation adjusted to match the context in which @@ -354,9 +362,9 @@ they were originally defined. This assures that Python (which relies on indentat 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. +explicitly a code chunk is output without modification. -All of the **pyWeb** tags begin with ``@@``. This can be changed. +All of the **py-web-tool** 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 @@ -509,7 +517,7 @@ 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. @@ -557,14 +565,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. @@ -593,7 +601,7 @@ 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. @@ -622,7 +630,7 @@ Here's how the context-sensitive indentation works. @@o myFile.py @@{ - def aFunction( a, b ): + def aFunction(a, b): @@ @@| aFunction @@} @@ -642,7 +650,7 @@ more obvious. .. parsed-literal:: ~ - ~def aFunction( a, b ): + ~def aFunction(a, b): ~ ~ """doc string""" ~ return a + b @@ -733,14 +741,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. @@ -760,14 +773,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. + The version string in the **py-web-tool** application. -Running **pyWeb** to Tangle and Weave +Running **py-web-tool** to Tangle and Weave -------------------------------------- Assuming that you have marked ``pyweb.py`` as executable, @@ -816,7 +829,7 @@ Currently, the following command line options are accepted. Bootstrapping -------------- -**pyWeb** is written using **pyWeb**. The distribution includes the original ``.w`` +**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 this. @@ -840,7 +853,7 @@ Similarly, the tests are bootstrapped from ``.w`` files. Dependencies ------------- -**pyWeb** requires Python 3.3 or newer. +**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. @@ -856,10 +869,9 @@ This application is very directly based on (derived from?) work that - 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. +for two large development efforts, I finally understood the feature set I really wanted. -Jason Fruit contributed to the previous version. +Jason Fruit and others contributed to the previous version. diff --git a/overview.w b/overview.w index f5607a8..bc1f443 100644 --- a/overview.w +++ b/overview.w @@ -30,7 +30,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. @@ -85,7 +86,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 @@ -98,11 +99,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. @@ -118,7 +119,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, @@ -131,9 +132,8 @@ 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. \ No newline at end of file +The idea is that the Weaver Action should be visible to tools like `PyInvoke `_. +We want ``Weave("someFile.w")`` to be a sensible task. diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..8992a95 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,21 @@ + +[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 = """ +[tox] +envlist = py310 + +[testenv] +deps = + pytest == 7.1.2 + mypy == 0.910 +commands_pre = + python3 pyweb-3.0.py pyweb.w + python3 pyweb.py -o test test/pyweb_test.w +commands = + python3 test/test.py + mypy --strict pyweb.py +""" diff --git a/pyweb.py b/pyweb.py index 4167527..fffa5ae 100644 --- a/pyweb.py +++ b/pyweb.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -"""pyWeb Literate Programming - tangle and weave tool. +"""py-web-tool Literate Programming. Yet another simple literate programming tool derived from nuweb, implemented entirely in Python. @@ -26,18 +26,19 @@ file.w The input file, with @o, @d, @i, @[, @{, @|, @<, @f, @m, @u commands. """ -__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__='2.3.2'. -### 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 pyweb-3.0.py, __version__='3.0'. +### From source pyweb.w modified Wed Jun 8 14:04:44 2022. +### In working directory '/Users/slott/Documents/Projects/py-web-tool'. import string import tempfile import filecmp +from typing import Pattern, Match, Optional, Any, Literal import weakref @@ -46,8 +47,10 @@ import sys import platform +from typing import TextIO import re +from collections.abc import Iterator, Iterable import shlex @@ -67,51 +70,54 @@ -class Error( Exception ): pass +class Error(Exception): pass 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) - - def startswith( self, prefix ): - return None - def searchForRE( self, rePat ): - return None - def indent( self ): + 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 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 ): + def ref(self, aWeb: "Web") -> str | None: return None - def weave( self, aWeb, aWeaver ): + def weave(self, aWeb: "Web", aWeaver: "Weaver") -> None: pass - def tangle( self, aWeb, aTangler ): + def tangle(self, aWeb: "Web", aTangler: "Tangler") -> None: pass -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: @@ -119,115 +125,119 @@ def indent( self ): 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 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 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.') -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 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 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() -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) - - def resolve( self, aWeb ): + 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 ) + self.fullName = aWeb.fullNameFor(self.refTo) + self.chunkList = aWeb.getchunk(self.refTo) - 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 - 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) - 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) @@ -237,56 +247,60 @@ def tangle( self, aWeb, aTangler ): 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 ) - - def append( self, command ): + web : weakref.ReferenceType["Web"] + previous_command : "Command" + initial: bool + 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.fileName = '' + 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 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 - def appendText( self, text, lineNumber=0 ): + def appendText(self, text: str, lineNumber: int = 0) -> None: """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) ) + 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) ) + self.commands.append(self.makeContent(text,lineNumber)) - def webAdd( self, web ): + def webAdd(self, web: "Web") -> None: """Add self to a Web as anonymous chunk.""" - web.add( self ) + web.add(self) - 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: @@ -294,195 +308,207 @@ def genReferences( self, aWeb ): - def makeContent( self, text, lineNumber=0 ): - return TextCommand( text, lineNumber ) + def makeContent(self, text: str, lineNumber: int = 0) -> Command: + return TextCommand(text, lineNumber) - 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 [] - 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) + ] - 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.""") - 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) - 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 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 ) - - def setUserIDRefs( self, text ): + 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 ): + self.user_id_list = text.split() + def getUserIDRefs(self) -> list[str]: return self.user_id_list - 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) - 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) - 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 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 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 + 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 ): + def webAdd(self, web: "Web") -> None: """Add self to a Web as output chunk, update xrefs.""" - web.addOutput( self ) + web.addOutput(self) - 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) - 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 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 ) + + def makeContent(self, text: str, lineNumber: int = 0) -> Command: + return TextCommand(text, lineNumber) - 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) - 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 @[.""") @@ -491,169 +517,173 @@ def tangle( self, aWeb, aTangler ): 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, filename: str | None = None) -> None: + self.webFileName = filename + 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.webFileName!r}" - 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 - 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) - 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) - 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 - 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] ) ] + 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)) ) ) + raise Error(f"Ambiguous abbreviation {name!r}, matches {list(sorted(best))!r}") elif len(best) == 1: return best[0] return name - 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}") - 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 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 - 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] - 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 - 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 ): + + 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, [] ) + ux[id] = (c.seq, []) - def _updateUserId( self, chunkMap, ux ): + + 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}".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) - 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('\\'): @@ -662,75 +692,85 @@ def language( self, preferredWeaverClass=None ): - def tangle( self, aTangler ): + def tangle(self, aTangler: "Tangler") -> None: for f, c in self.output.items(): with aTangler.open(f): for p in c: - p.tangle( self, aTangler ) + p.tangle(self, aTangler) - def weave( self, aWeaver ): - self.logger.debug( "Weaving file from {!r}".format(self.webFileName) ) - basename, _ = os.path.splitext( self.webFileName ) + def weave(self, aWeaver: "Weaver") -> None: + self.logger.debug("Weaving file from %r", self.webFileName) + if not self.webFileName: + raise Error("No filename supplied for weaving.") + 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) + 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 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 +class ParseError(Exception): pass + 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 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: @@ -738,288 +778,293 @@ def _group( self, word_iter ): 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 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__ ) + # State of reading and parsing. + tokenizer: Tokenizer + aChunk: Chunk + + # Configuration + command: str + permitList: list[str] + + # State of the reader + _source: TextIO + fileName: str + theWeb: "Web" + + 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 # 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' - def __str__( self ): + + def __str__(self) -> str: return self.__class__.__name__ - def location( self ): + + def location(self) -> tuple[str, int]: return (self.fileName, self.tokenizer.lineNumber+1) - def load( self, web, filename, source=None ): - self.theWeb= web - self.fileName= filename + def load(self, web: "Web", filename: str, source: TextIO | None = None) -> "WebReader": + 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 + self.theWeb.webFileName = self.fileName if source: - self._source= source + self._source = source self.parse_source() else: - with open( self.fileName, "r" ) as self._source: + 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 + 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 ) + 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.warning('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) - def handleCommand( self, token ): - self.logger.debug( "Reading {!r}".format(token) ) + def handleCommand(self, token: str) -> bool: + self.logger.debug("Reading %r", token) if token[:2] == self.cmdo: - 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.fileName = self.fileName + self.aChunk.webAdd(self.theWeb) # capture an OutputChunk up to @} elif token[:2] == self.cmdd: - 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.fileName = self.fileName + self.aChunk.webAdd(self.theWeb) # capture a NamedChunk up to @} or @] elif token[:2] == self.cmdi: - incFile= next(self.tokenizer).strip() + incFile = next(self.tokenizer).strip() try: - self.logger.info( "Including {!r}".format(incFile) ) - include= WebReader( parent=self ) - include.load( self.theWeb, incFile ) + self.logger.info("Including %r", 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) ) + self.logger.error("Errors in included file %r, output is incomplete.", incFile) except Error as e: - self.logger.error( - "Problems with included file {!s}, output is incomplete.".format( - incFile) ) + self.logger.error("Problems with included file %r, output is incomplete.", incFile) 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 %r, output is incomplete.", 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 ) + else: raise # Seems heavy-handed, but, the file wasn't found! + self.aChunk = Chunk() + self.aChunk.webAdd(self.theWeb) elif token[:2] in (self.cmdrcurl,self.cmdrbrak): - self.aChunk= Chunk() - self.aChunk.webAdd( self.theWeb ) + self.aChunk = Chunk() + self.aChunk.webAdd(self.theWeb) elif token[:2] == self.cmdpipe: 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 elif token[:2] == self.cmdf: - self.aChunk.append( FileXrefCommand(self.tokenizer.lineNumber) ) + self.aChunk.append(FileXrefCommand(self.tokenizer.lineNumber)) elif token[:2] == self.cmdm: - self.aChunk.append( MacroXrefCommand(self.tokenizer.lineNumber) ) + self.aChunk.append(MacroXrefCommand(self.tokenizer.lineNumber)) elif token[:2] == self.cmdu: - self.aChunk.append( UserIdXrefCommand(self.tokenizer.lineNumber) ) + self.aChunk.append(UserIdXrefCommand(self.tokenizer.lineNumber)) elif token[:2] == 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}".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) elif token[:2] == self.cmdlexpr: # 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) + 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 ('breakpoint', 'compile', 'eval', 'exec', 'execfile', 'globals', 'help', 'input', 'memoryview', 'open', 'print', 'super', '__import__') + )) + globals = dict( + __builtins__=safe, + os=types.SimpleNamespace(path=os.path, getcwd=os.getcwd, name=os.name), + time=time, + 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) ) + 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) elif token[:2] == self.cmdcmd: - self.aChunk.appendText( self.command, self.tokenizer.lineNumber ) + self.aChunk.appendText(self.command, self.tokenizer.lineNumber) 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.logger.error("Extra %r (possibly missing chunk name) near %r", token, self.location()) self.errors += 1 else: - return None # did not recogize the command - return True # did recognize the command + return False # did not recogize the command + return True # did recognize the command - 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 @@ -1028,151 +1073,173 @@ def expect( self, tokens ): 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 + + theFile: TextIO + def __init__(self) -> None: + self.fileName = "" + self.logger = logging.getLogger(self.__class__.__qualname__) + self.log_indent = logging.getLogger("indent." + self.__class__.__qualname__) + # Summary + self.linesWritten = 0 + self.totalFiles = 0 + self.totalLines = 0 + # Working State + self.lastIndent = 0 + self.fragment = False + self.context: list[int] = [] + self.readdIndent(self.code_indent) # Create context and initial lastIndent values + + def __str__(self) -> str: return self.__class__.__name__ - def open( self, aFile ): + + def open(self, aFile: str) -> "Emitter": """Open a file.""" - self.fileName= aFile - self.linesWritten= 0 - self.doOpen( aFile ) + self.fileName = aFile + self.linesWritten = 0 + self.doOpen(aFile) return self + + + def doOpen(self, aFile: str) -> None: + self.logger.debug("creating %r", self.fileName) - def doOpen( self, aFile ): - self.logger.debug( "creating {!r}".format(self.fileName) ) - def close( self ): + 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.fileName + ) - def doClose( self ): - self.logger.debug( "wrote {:d} lines to {!s}".format( - self.linesWritten, self.fileName) ) - def write( self, text ): + def write(self, text: str) -> None: if text is None: return self.linesWritten += text.count('\n') - self.theFile.write( text ) + 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 - def codeBlock( self, text ): + def codeBlock(self, text: str) -> None: """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 + 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: + # Last line was empty, a trailing newline. + self.logger.debug("Last (Empty) Line: indent is %d", len(rest[-1]) + indent) # Buffer a next indent - self.lastIndent= len(rest[-1]) + indent - self.fragment= False + self.lastIndent = len(rest[-1]) + indent + self.fragment = False - 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 - def codeFinish( self ): + def codeFinish(self) -> None: if self.fragment: self.write('\n') - 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 Weaver( Emitter ): +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 - header= """\n.. include:: \n.. include:: \n""" + extension = ".rst" + code_indent = 4 + header = """\n.. include:: \n.. include:: \n""" + + reference_style : "Reference" - def __init__( self ): + def __init__(self) -> None: super().__init__() - self.reference_style= None # Must be configured. - 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, basename: str) -> None: + self.fileName = basename + self.extension + self.logger.info("Weaving %r", self.fileName) + self.theFile = open(self.fileName, "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.fileName) + 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 @@ -1180,7 +1247,7 @@ def codeFinish( self ): # Template Expansions. - quoted_chars = [ + quoted_chars: list[tuple[str, str]] = [ # prevent some RST markup from being recognized ('\\',r'\\'), # Must be first. ('`',r'\`'), @@ -1190,127 +1257,129 @@ def codeFinish( self ): ] - def docBegin( self, aChunk ): + def docBegin(self, aChunk: Chunk) -> None: pass - def docEnd( self, aChunk ): + def docEnd(self, aChunk: Chunk) -> None: pass - 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 ) + 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 ) ) + return self.ref_template.substitute(refList=self.ref_separator.join(refList)) else: return "" - cb_template = string.Template( "\n.. _`${seq}`:\n.. rubric:: ${fullName} (${seq}) ${concat}\n.. parsed-literal::\n :class: code\n\n" ) + cb_template = string.Template("\n.. _`${seq}`:\n.. rubric:: ${fullName} (${seq}) ${concat}\n.. parsed-literal::\n :class: code\n\n") - 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 "+=", # RST Separator ) - self.write( txt ) + self.write(txt) - ce_template = string.Template( "\n..\n\n .. class:: small\n\n |loz| *${fullName} (${seq})*. Used by: ${references}\n" ) + 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) - fb_template = string.Template( "\n.. _`${seq}`:\n.. rubric:: ${fullName} (${seq}) ${concat}\n.. parsed-literal::\n :class: code\n\n" ) + 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 + 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 "+=", # RST Separator ) - self.write( txt ) + self.write(txt) - fe_template= string.Template( "\n..\n\n .. class:: small\n\n |loz| *${fullName} (${seq})*.\n" ) + 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 ) + 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("|srarr|\ (`${seq}`_)") - refto_seq_separator= ", " + refto_name_template = string.Template(r"|srarr|\ ${fullName} (`${seq}`_)") + refto_seq_template = string.Template("|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.""" 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 - 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("\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 ): + 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()) - 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) @@ -1320,13 +1389,13 @@ class RST(Weaver): pass -class LaTeX( Weaver ): +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""" + extension = ".tex" + code_indent = 0 + header = """\n\\usepackage{fancyvrb}\n""" cb_template = string.Template( """\\label{pyweb${seq}} @@ -1336,18 +1405,18 @@ class LaTeX( Weaver ): - ce_template= string.Template(""" + ce_template = string.Template(""" \\end{Verbatim} ${references} \\end{flushleft}\n""") # Prevent indentation - fb_template= cb_template + fb_template = cb_template - fe_template= ce_template + fe_template = ce_template @@ -1363,7 +1432,7 @@ class LaTeX( Weaver ): - quoted_chars = [ + quoted_chars: list[tuple[str, str]] = [ ("\\end{Verbatim}", "\\end\,{Verbatim}"), # Allow \end{Verbatim} ("\\{","\\\,{"), # Prevent unexpected commands in Verbatim ("$","\\$"), # Prevent unexpected math in Verbatim @@ -1371,20 +1440,20 @@ class LaTeX( Weaver ): - 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 HTML( Weaver ): +class HTML(Weaver): """HTML formatting for XRef's and code blocks when weaving.""" - extension= ".html" - code_indent= 0 - header= "" + extension = ".html" + code_indent = 0 + header = "" - cb_template= string.Template(""" + cb_template = string.Template("""

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

@@ -1392,7 +1461,7 @@ class HTML( Weaver ): - ce_template= string.Template(""" + ce_template = string.Template("""

${fullName} (${seq}). ${references} @@ -1400,28 +1469,26 @@ class HTML( Weaver ): - 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""") - ref_item_template = string.Template( - '${fullName} (${seq})' - ) - ref_template = string.Template( ' Used by ${refList}.' ) + ref_item_template = string.Template('${fullName} (${seq})') + ref_template = string.Template(' Used by ${refList}.' ) - quoted_chars = [ + quoted_chars: list[tuple[str, str]] = [ ("&", "&"), # Must be first ("<", "<"), (">", ">"), @@ -1430,119 +1497,111 @@ class HTML( Weaver ): - refto_name_template = string.Template( - '${fullName} (${seq})' - ) - refto_seq_template = string.Template( - '(${seq})' - ) + 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" ) + 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 HTMLShort( HTML ): +class HTMLShort(HTML): """HTML formatting for XRef's and code blocks when weaving with short references.""" - ref_item_template = string.Template( '(${seq})' ) + ref_item_template = string.Template('(${seq})') -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 + self.comment_start: str = "#" + self.comment_end: str = "" + self.include_line_numbers = False - def checkPath( self ): + def checkPath(self) -> None: if "/" in self.fileName: dirname, _, _ = self.fileName.rpartition("/") try: - os.makedirs( dirname ) - self.logger.info( "Creating {!r}".format(dirname) ) - except OSError as e: + os.makedirs(dirname) + self.logger.info("Creating %r", dirname) + except OSError as exc: # 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.logger.debug("Exception %r creating %r", exc, dirname) + def doOpen(self, aFile: str) -> None: + self.fileName = aFile self.checkPath() - self.theFile= open( aFile, "w" ) - self.logger.info( "Tangling {!r}".format(aFile) ) - def doClose( self ): + self.theFile = open(aFile, "w") + self.logger.info("Tangling %r", aFile) + 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.fileName) - 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 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 + tempname : str + def __init__(self, *args: Any) -> None: + super().__init__(*args) + - 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, aFile: str) -> None: + fd, self.tempname = tempfile.mkstemp(dir=os.curdir) + self.theFile = os.fdopen(fd, "w") + self.logger.info("Tangling %r", aFile) + - def doClose( self ): + def doClose(self) -> None: self.theFile.close() try: - same= filecmp.cmp( self.tempname, self.fileName ) + same = filecmp.cmp(self.tempname, self.fileName) 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("No change to %r", self.fileName) + os.remove(self.tempname) else: # Windows requires the original file name be removed first. self.checkPath() try: - os.remove( self.fileName ) + 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) ) + os.rename(self.tempname, self.fileName) + self.logger.info("Wrote %e lines to %r", self.linesWritten, self.fileName) @@ -1550,30 +1609,30 @@ def doClose( self ): class Reference: - def __init__( self ): - self.logger= logging.getLogger( self.__class__.__qualname__ ) - def chunkReferencedBy( self, aChunk ): + def __init__(self) -> None: + self.logger = logging.getLogger(self.__class__.__qualname__) + def chunkReferencedBy(self, aChunk: Chunk) -> list[Chunk]: """Return a list of Chunks.""" - pass + return [] -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 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 @@ -1581,148 +1640,153 @@ def allParentsOf( self, chunkList, depth=0 ): 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 ) + 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 ): - 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() - 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 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 ] ) - - def __call__( self ): + 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.web = self.web + o.options = self.options o() - def append( self, anAction ): - self.opSequence.append( anAction ) + def append(self, anAction: Action) -> None: + self.opSequence.append(anAction) - 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 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}]" - 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 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.webFileName) #raise - 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 TangleAction( Action ): +class TangleAction(Action): """Tangle source files.""" - def __init__( self ): - super().__init__( "Tangle" ) + def __init__(self) -> None: + super().__init__("Tangle") - 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 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.webFileName) #raise - 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 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 ) + def __init__(self) -> None: + super().__init__("Load") + def __str__(self) -> str: + return f"Load [{self.webReader!s}, {self.web!s}]" - 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.webFileName = self.options.webFileName + error = f"Problems with source file {self.options.webFileName!r}, no output produced." try: - self.webReader.load( self.web, self.options.webFileName ) + 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.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. @@ -1732,10 +1796,10 @@ def __call__( self ): - 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." + ) @@ -1744,48 +1808,49 @@ def summary( self ): class Application: - def __init__( self ): - self.logger= logging.getLogger( self.__class__.__qualname__ ) + 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, + 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 ) + 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]) - 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("files", nargs='+') + 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. """ @@ -1794,60 +1859,59 @@ def expand( self, config ): elif config.reference == 's': config.reference_style = SimpleReference() else: - raise Error( "Improper configuration" ) + raise Error("Improper configuration") 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() + 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 - 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 + self.theAction = self.doTangle elif config.skip.lower().startswith('t'): # not tangling == weaving - self.theAction= self.doWeave + 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.webFileName = f + self.theAction.web = w + self.theAction.options = config self.theAction() - self.logger.info( self.theAction.summary() ) + self.logger.info(self.theAction.summary()) @@ -1862,53 +1926,54 @@ def process( self, config ): 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 -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}, }, -) +} -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() diff --git a/pyweb.rst b/pyweb.rst index f018f82..d8a9650 100644 --- a/pyweb.rst +++ b/pyweb.rst @@ -1,5 +1,5 @@ ############################## -pyWeb Literate Programming 3.0 +pyWeb Literate Programming 3.1 ############################## ================================================= @@ -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,17 +249,17 @@ 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. @@ -266,7 +267,7 @@ simple approach started by *nuweb* and adopted by **pyWeb**. Use Cases ----------- -**pyWeb** supports two use cases, `Tangle Source Files`_ and `Weave Documentation`_. +**py-web-tool** 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. @@ -285,7 +286,7 @@ 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``. +The sequence is ``./pyweb.py *theFile*.w``. Weave Documentation ~~~~~~~~~~~~~~~~~~~~ @@ -302,18 +303,18 @@ 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``. +The sequence is ``./pyweb.py *theFile*.w``. -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,7 +323,7 @@ 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: @@ -332,35 +333,42 @@ The sequence is as follows: python *theTest* >\ *aLog* ./pyweb.py -xt *theFile*\ .w +Another possibility includes the following: + .. parsed-literal:: + + ./pyweb.py -xw -pi *theFile*\ .w + python -m pytest *theTestFile* >\ *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. +runs the test, creating a log file. The third step weaves the final document, +including the test output. -Writing **pyWeb** ``.w`` Files -------------------------------- +Writing **py-web-tool** ``.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*. +The **py-web-tool** 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 +The **py-web-tool** 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. +**py-web-tool** 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** +When weaving, the tags are replaced with output markup. This means that **py-web-tool** is not **totally** independent of the output markup. The code chunks will have their indentation adjusted to match the context in which @@ -368,9 +376,9 @@ they were originally defined. This assures that Python (which relies on indentat 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. +explicitly a code chunk is output without modification. -All of the **pyWeb** tags begin with ``@``. This can be changed. +All of the **py-web-tool** 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 @@ -522,8 +530,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 +579,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,7 +615,7 @@ 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. @@ -636,7 +644,7 @@ Here's how the context-sensitive indentation works. @o myFile.py @{ - def aFunction( a, b ): + def aFunction(a, b): @ @| aFunction @} @@ -656,7 +664,7 @@ more obvious. .. parsed-literal:: ~ - ~def aFunction( a, b ): + ~def aFunction(a, b): ~ ~ """doc string""" ~ return a + b @@ -747,14 +755,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,14 +787,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. + The version string in the **py-web-tool** application. -Running **pyWeb** to Tangle and Weave +Running **py-web-tool** to Tangle and Weave -------------------------------------- Assuming that you have marked ``pyweb.py`` as executable, @@ -830,7 +843,7 @@ Currently, the following command line options are accepted. Bootstrapping -------------- -**pyWeb** is written using **pyWeb**. The distribution includes the original ``.w`` +**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 this. @@ -854,7 +867,7 @@ Similarly, the tests are bootstrapped from ``.w`` files. Dependencies ------------- -**pyWeb** requires Python 3.3 or newer. +**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. @@ -870,13 +883,13 @@ This application is very directly based on (derived from?) work that - 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. +for two large development efforts, I finally understood the feature set I really wanted. + +Jason Fruit and others contributed to the previous version. -Jason Fruit contributed to the previous version. .. pyweb/overview.w @@ -910,7 +923,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 +979,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 +992,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 +1012,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,12 +1025,12 @@ 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 @@ -1110,23 +1124,23 @@ fit elsewhere :class: code - |srarr|\ Error class - defines the errors raised (`94`_) - |srarr|\ Command class hierarchy - used to describe individual commands (`76`_) + |srarr|\ Error class - defines the errors raised (`95`_) + |srarr|\ Command class hierarchy - used to describe individual commands (`77`_) |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|\ Web class - describes the overall "web" of chunks (`96`_) + |srarr|\ Tokenizer class - breaks input into tokens (`134`_) + |srarr|\ Option Parser class - locates optional values on commands (`136`_), |srarr|\ (`137`_), |srarr|\ (`138`_) + |srarr|\ WebReader class - parses the input file, building the Web structure (`115`_) |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|\ Reference class hierarchy - strategies for references to a chunk (`92`_), |srarr|\ (`93`_), |srarr|\ (`94`_) - |srarr|\ Action class hierarchy - used to describe basic actions of the application (`136`_) + |srarr|\ Action class hierarchy - used to describe basic actions of the application (`139`_) .. .. class:: small - |loz| *Base Class Definitions (1)*. Used by: pyweb.py (`153`_) + |loz| *Base Class Definitions (1)*. Used by: pyweb.py (`156`_) Emitters @@ -1176,7 +1190,7 @@ 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, +- Boilerplate text to replace various **py-web-tool** constructs, - Escape rules to make source code amenable to the markup language, @@ -1248,7 +1262,7 @@ directs us to factor the basic open(), close() and write() methods into two step .. parsed-literal:: - def open( self ): + def open(self) -> "Emitter": *common preparation* self.doOpen() *#overridden by subclasses* return self @@ -1302,19 +1316,26 @@ The ``codeBlock()`` method to indent each line written. 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 + + theFile: TextIO + def \_\_init\_\_(self) -> None: + self.fileName = "" + self.logger = logging.getLogger(self.\_\_class\_\_.\_\_qualname\_\_) + self.log\_indent = logging.getLogger("indent." + self.\_\_class\_\_.\_\_qualname\_\_) + # Summary + self.linesWritten = 0 + self.totalFiles = 0 + self.totalLines = 0 + # Working State + self.lastIndent = 0 + self.fragment = False + self.context: list[int] = [] + self.readdIndent(self.code\_indent) # Create context and initial lastIndent values + + 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`_) @@ -1348,28 +1369,32 @@ characters to the file. :class: code - def open( self, aFile ): + def open(self, aFile: str) -> "Emitter": """Open a file.""" - self.fileName= aFile - self.linesWritten= 0 - self.doOpen( aFile ) + self.fileName = aFile + self.linesWritten = 0 + self.doOpen(aFile) return self + |srarr|\ Emitter doOpen, to be overridden by subclasses (`5`_) - def close( self ): + + 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 ): + + def write(self, text: str) -> None: if text is None: return self.linesWritten += text.count('\\n') - self.theFile.write( text ) + 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 @@ -1393,8 +1418,8 @@ perform the unique operation for the subclass. :class: code - def doOpen( self, aFile ): - self.logger.debug( "creating {!r}".format(self.fileName) ) + def doOpen(self, aFile: str) -> None: + self.logger.debug("creating {!r}".format(self.fileName)) .. @@ -1411,9 +1436,8 @@ perform the unique operation for the subclass. :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 {!s}".format( self.linesWritten, self.fileName)) .. @@ -1477,29 +1501,38 @@ a NamedChunk. It's not really a general feature of emitters or even tanglers. :class: code - def codeBlock( self, text ): + def codeBlock(self, text: str) -> None: """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 + indent = self.context[-1] + lines = text.split('\\n') + if len(lines) == 1: + # Fragment with no newline. + self.logger.debug(f"Fragment: {self.lastIndent}, {lines[0]!r}") + 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]) ) + # Multiple lines with one or more newlines. + first, rest = lines[:1], lines[1:] + self.logger.debug(f"First Line: {self.lastIndent}, {first[0]!r}") + self.write('{!s}{!s}\\n'.format(self.lastIndent\*' ', first[0])) for l in rest[:-1]: - self.write( '{!s}{!s}\\n'.format(indent\*' ', l) ) + self.logger.debug(f"Next Line: {indent}, {l!r}") + 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 + # Last line is non-empty. + self.logger.debug(f"Last (Partial) Line: {indent}, {rest[-1]!r}") + self.write('{!s}{!s}'.format(indent\*' ', rest[-1])) + self.lastIndent = 0 + self.fragment = True else: + # Last line was empty, a trailing newline. + self.logger.debug(f"Last (Empty) Line: indent is {len(rest[-1]) + indent}") # Buffer a next indent - self.lastIndent= len(rest[-1]) + indent - self.fragment= False + self.lastIndent = len(rest[-1]) + indent + self.fragment = False .. @@ -1526,15 +1559,15 @@ HTML these will not be altered. :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 @@ -1554,7 +1587,7 @@ The ``codeFinish()`` method handles a trailing fragmentary line when tangling. :class: code - def codeFinish( self ): + def codeFinish(self) -> None: if self.fragment: self.write('\\n') @@ -1600,23 +1633,24 @@ requires this. ``readdIndent()`` uses this initial offset for weaving. :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 {!s}: {!r}".format(increment, self.context)) + def setIndent(self, indent: int) -> None: + self.context.append(indent) + self.lastIndent = self.context[-1] + self.log\_indent.debug("setIndent {!s}: {!r}".format(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}".format(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 {!s}: {!r}".format(indent, self.context)) .. @@ -1631,7 +1665,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 @@ -1685,7 +1719,7 @@ Instance-level configuration values: .. class:: small - |loz| *Imports (11)*. Used by: pyweb.py (`153`_) + |loz| *Imports (11)*. Used by: pyweb.py (`156`_) @@ -1695,19 +1729,20 @@ Instance-level configuration values: :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:: \`\` """ - extension= ".rst" - code\_indent= 4 - header= """\\n.. include:: \\n.. include:: \\n""" + extension = ".rst" + code\_indent = 4 + header = """\\n.. include:: \\n.. include:: \\n""" - 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`_) @@ -1746,20 +1781,19 @@ we're not always starting a fresh line with ``weaveReferenceTo()``. :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, basename: str) -> None: + 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) -> 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}".format( self.linesWritten, self.fileName)) + 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 {!s}: {!r}".format(self.lastIndent, self.context)) + def codeFinish(self) -> None: pass # Not needed when weaving @@ -1785,7 +1819,7 @@ to look for paired RST inline markup and quote just these special character occu :class: code - quoted\_chars = [ + quoted\_chars: list[tuple[str, str]] = [ # prevent some RST markup from being recognized ('\\\\',r'\\\\'), # Must be first. ('\`',r'\\\`'), @@ -1817,9 +1851,9 @@ of possible additional processing. :class: code - def docBegin( self, aChunk ): + def docBegin(self, aChunk: Chunk) -> None: pass - def docEnd( self, aChunk ): + def docEnd(self, aChunk: Chunk) -> None: pass @@ -1845,16 +1879,16 @@ Each code chunk includes the places where the chunk is referenced. :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 ) + 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 ) ) + return self.ref\_template.substitute(refList=self.ref\_separator.join(refList)) else: return "" @@ -1883,25 +1917,25 @@ refer to this chunk can be emitted. :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("\\n.. \_\`${seq}\`:\\n.. rubric:: ${fullName} (${seq}) ${concat}\\n.. parsed-literal::\\n :class: code\\n\\n") - 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 "+=", # RST Separator ) - self.write( txt ) + self.write(txt) - ce\_template = string.Template( "\\n..\\n\\n .. class:: small\\n\\n \|loz\| \*${fullName} (${seq})\*. Used by: ${references}\\n" ) + 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) @@ -1931,27 +1965,27 @@ list is always empty. :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("\\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 + 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 "+=", # RST Separator ) - self.write( txt ) + self.write(txt) - fe\_template= string.Template( "\\n..\\n\\n .. class:: small\\n\\n \|loz\| \*${fullName} (${seq})\*.\\n" ) + 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 ) + 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) .. @@ -1980,20 +2014,20 @@ a simple ``" "`` because it looks better. :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("\|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.""" 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 @@ -2035,26 +2069,26 @@ to change the look of the final woven document. :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("\\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 ): + 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()) .. @@ -2072,17 +2106,19 @@ Cross-reference definition line :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) .. @@ -2127,10 +2163,10 @@ given to the ``weave()`` method of the Web. .. parsed-literal:: - w= Web() + w = Web() WebReader().load(w,"somefile.w") - weave_latex= LaTeX() - w.weave( weave_latex ) + 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 @@ -2150,13 +2186,13 @@ function pretty well in most L\ !sub:`A`\ T\ !sub:`E`\ X documents. :class: code - class LaTeX( Weaver ): + 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""" + extension = ".tex" + code\_indent = 0 + header = """\\n\\\\usepackage{fancyvrb}\\n""" |srarr|\ LaTeX code chunk begin (`24`_) |srarr|\ LaTeX code chunk end (`25`_) @@ -2217,7 +2253,7 @@ indentation. :class: code - ce\_template= string.Template(""" + ce\_template = string.Template(""" \\\\end{Verbatim} ${references} \\\\end{flushleft}\\n""") # Prevent indentation @@ -2243,7 +2279,7 @@ start of a code chunk. :class: code - fb\_template= cb\_template + fb\_template = cb\_template .. @@ -2265,7 +2301,7 @@ invokes this chunk, and restores normal indentation. :class: code - fe\_template= ce\_template + fe\_template = ce\_template .. @@ -2319,7 +2355,7 @@ block. Our one compromise is a thin space if the phrase :class: code - quoted\_chars = [ + quoted\_chars: list[tuple[str, str]] = [ ("\\\\end{Verbatim}", "\\\\end\\,{Verbatim}"), # Allow \\end{Verbatim} ("\\\\{","\\\\\\,{"), # Prevent unexpected commands in Verbatim ("$","\\\\$"), # Prevent unexpected math in Verbatim @@ -2345,8 +2381,8 @@ the current line of code. :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})""") .. @@ -2368,10 +2404,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 @@ -2396,11 +2432,11 @@ with abbreviated (no name) cross references at the end of the chunk. :class: code - class HTML( Weaver ): + class HTML(Weaver): """HTML formatting for XRef's and code blocks when weaving.""" - extension= ".html" - code\_indent= 0 - header= "" + extension = ".html" + code\_indent = 0 + header = "" |srarr|\ HTML code chunk begin (`33`_) |srarr|\ HTML code chunk end (`34`_) |srarr|\ HTML output file begin (`35`_) @@ -2425,7 +2461,7 @@ with abbreviated (no name) cross references at the end of the chunk. :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`_) @@ -2448,7 +2484,7 @@ and HTML tags necessary to set the code off visually. :class: code - cb\_template= string.Template(""" + cb\_template = string.Template("""

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

@@ -2473,7 +2509,7 @@ write the list of chunks that reference this chunk. :class: code - ce\_template= string.Template(""" + ce\_template = string.Template("""

${fullName} (${seq}). ${references} @@ -2497,7 +2533,7 @@ and HTML tags necessary to set the code off visually. :class: code - fb\_template= string.Template(""" + fb\_template = string.Template("""

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

\\n""") # Prevent indent
@@ -2521,7 +2557,7 @@ write the list of chunks that reference this chunk.
     :class: code
 
     
-    fe\_template= string.Template( """
+ fe\_template = string.Template( """

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

\\n""") @@ -2545,10 +2581,8 @@ transitive references. :class: code - ref\_item\_template = string.Template( - '${fullName} (${seq})' - ) - ref\_template = string.Template( ' Used by ${refList}.' ) + ref\_item\_template = string.Template('${fullName} (${seq})') + ref\_template = string.Template(' Used by ${refList}.' ) .. @@ -2569,7 +2603,7 @@ as HTML. :class: code - quoted\_chars = [ + quoted\_chars: list[tuple[str, str]] = [ ("&", "&"), # Must be first ("<", "<"), (">", ">"), @@ -2595,12 +2629,8 @@ surrounding source code. :class: code - refto\_name\_template = string.Template( - '${fullName} (${seq})' - ) - refto\_seq\_template = string.Template( - '(${seq})' - ) + refto\_name\_template = string.Template('${fullName} (${seq})') + refto\_seq\_template = string.Template('(${seq})') .. @@ -2626,9 +2656,9 @@ The ``xrefLine()`` method writes a line for the file or macro cross reference bl :class: code - 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") |srarr|\ HTML write user id cross reference line (`41`_) @@ -2651,8 +2681,8 @@ is included in the correct order with the other instances, but is bold and marke :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}') .. @@ -2675,7 +2705,7 @@ transitive references. :class: code - ref\_item\_template = string.Template( '(${seq})' ) + ref\_item\_template = string.Template('(${seq})') .. @@ -2693,10 +2723,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 @@ -2729,13 +2759,13 @@ There are three configurable values: :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 + self.comment\_start: str = "#" + self.comment\_end: str = "" + self.include\_line\_numbers = False |srarr|\ Tangler doOpen, and doClose overrides (`44`_) |srarr|\ Tangler code chunk begin (`45`_) |srarr|\ Tangler code chunk end (`46`_) @@ -2764,24 +2794,23 @@ actual file created by open. :class: code - def checkPath( self ): + def checkPath(self) -> None: if "/" in self.fileName: dirname, \_, \_ = self.fileName.rpartition("/") try: - os.makedirs( dirname ) - self.logger.info( "Creating {!r}".format(dirname) ) + 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.logger.debug("Exception {!r} creating {!r}".format(e, dirname)) + def doOpen(self, aFile: str) -> None: + self.fileName = aFile self.checkPath() - self.theFile= open( aFile, "w" ) - self.logger.info( "Tangling {!r}".format(aFile) ) - def doClose( self ): + self.theFile = open(aFile, "w") + self.logger.info("Tangling {!r}".format(aFile)) + 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}".format( self.linesWritten, self.fileName)) .. @@ -2802,18 +2831,21 @@ prevailing indent at the start of the ``@<`` reference command. :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(">{!s}".format(aChunk.fullName)) .. @@ -2854,15 +2886,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. @@ -2884,7 +2916,7 @@ are opened and closed. .. class:: small - |loz| *Imports (47)*. Used by: pyweb.py (`153`_) + |loz| *Imports (47)*. Used by: pyweb.py (`156`_) @@ -2894,12 +2926,14 @@ are opened and closed. :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 + tempname : str + def \_\_init\_\_(self, \*args: Any) -> None: + super().\_\_init\_\_(\*args) + |srarr|\ TanglerMake doOpen override, using a temporary file (`49`_) + |srarr|\ TanglerMake doClose override, comparing temporary to original (`50`_) @@ -2923,10 +2957,10 @@ a "touch" if the new file is the same as the original. :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, aFile: str) -> None: + fd, self.tempname = tempfile.mkstemp(dir=os.curdir) + self.theFile = os.fdopen(fd, "w") + self.logger.info("Tangling {!r}".format(aFile)) .. @@ -2952,25 +2986,24 @@ and time) if nothing has changed. :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.fileName) 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("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 ) + 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) ) + os.rename(self.tempname, self.fileName) + self.logger.info("Wrote {:d} lines to {!r}".format(self.linesWritten, self.fileName)) .. @@ -3005,9 +3038,9 @@ This text can be program source, a reference command, or the documentation sourc |srarr|\ Chunk class (`52`_) - |srarr|\ NamedChunk class (`63`_), |srarr|\ (`68`_) - |srarr|\ OutputChunk class (`69`_) - |srarr|\ NamedDocumentChunk class (`73`_) + |srarr|\ NamedChunk class (`64`_), |srarr|\ (`69`_) + |srarr|\ OutputChunk class (`70`_) + |srarr|\ NamedDocumentChunk class (`74`_) .. @@ -3052,11 +3085,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 +3100,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('\W{!s}\W'.format(i) ) 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 +3114,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()``, @@ -3166,34 +3199,38 @@ The ``Chunk`` constructor initializes the following instance variables: 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 ) + web : weakref.ReferenceType["Web"] + previous\_command : "Command" + initial: bool + 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.fileName = '' + 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 "{!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 generate references from this Chunk (`59`_) |srarr|\ Chunk superclass make Content definition (`56`_) - |srarr|\ Chunk examination: starts with, matches pattern (`57`_) - |srarr|\ Chunk references to this Chunk (`59`_) + |srarr|\ Chunk examination: starts with, matches pattern (`58`_) + |srarr|\ Chunk references to this Chunk (`60`_) - |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 (`61`_) + |srarr|\ Chunk tangle this Chunk into a code file (`62`_) + |srarr|\ Chunk indent adjustments (`63`_) .. @@ -3212,10 +3249,10 @@ The ``append()`` method simply appends a ``Command`` instance to this chunk. :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 .. @@ -3242,17 +3279,17 @@ be a separate ``TextCommand`` because it will wind up indented. :class: code - def appendText( self, text, lineNumber=0 ): + def appendText(self, text: str, lineNumber: int = 0) -> None: """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) ) + 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) ) + self.commands.append(self.makeContent(text,lineNumber)) .. @@ -3275,9 +3312,9 @@ of the ``Web`` class to append an anonymous, unindexed chunk. :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) .. @@ -3301,8 +3338,8 @@ A Named Chunk using ``@[`` and ``@]`` creates text. :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) .. @@ -3332,28 +3369,48 @@ and accumulated as part of a cross reference for this ``Chunk``. .. _`57`: -.. rubric:: Chunk examination: starts with, matches pattern (57) = +.. rubric:: Imports (57) += +.. parsed-literal:: + :class: code + + from typing import Pattern, Match, Optional, Any, Literal + +.. + + .. class:: small + + |loz| *Imports (57)*. Used by: pyweb.py (`156`_) + + + +.. _`58`: +.. rubric:: Chunk examination: starts with, matches pattern (58) = .. parsed-literal:: :class: code - 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 [] @@ -3361,7 +3418,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 (58)*. Used by: Chunk class (`52`_) The chunk search in the ``searchForRE()`` method parallels weaving and tangling a ``Chunk``. @@ -3377,17 +3434,17 @@ context information. -.. _`58`: -.. rubric:: Chunk generate references from this Chunk (58) = +.. _`59`: +.. rubric:: Chunk generate references from this Chunk (59) = .. 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 +3455,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 (59)*. Used by: Chunk class (`52`_) The list of references to a Chunk uses a **Strategy** plug-in @@ -3409,22 +3466,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) = +.. _`60`: +.. rubric:: Chunk references to this Chunk (60) = .. 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 (60)*. Used by: Chunk class (`52`_) The ``weave()`` method weaves this chunk into the final document as follows: @@ -3443,22 +3502,22 @@ context information. -.. _`60`: -.. rubric:: Chunk weave this Chunk into the documentation (60) = +.. _`61`: +.. rubric:: Chunk weave this Chunk into the documentation (61) = .. 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 +3526,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 (61)*. Used by: Chunk class (`52`_) 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) = +.. _`62`: +.. rubric:: Chunk tangle this Chunk into a code file (62) = .. 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 (62)*. Used by: Chunk class (`52`_) Generally, a Chunk with a reference will adjust the indentation for @@ -3498,23 +3557,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) = +.. _`63`: +.. rubric:: Chunk indent adjustments (63) = .. 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 (63)*. Used by: Chunk class (`52`_) NamedChunk class @@ -3567,34 +3626,37 @@ This class introduces some additional attributes. -.. _`63`: -.. rubric:: NamedChunk class (63) = +.. _`64`: +.. rubric:: NamedChunk class (64) = .. 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 "{!r}: {!s}".format(self.name, Chunk.\_\_str\_\_(self)) + + def makeContent(self, text: str, lineNumber: int = 0) -> Command: + return CodeCommand(text, lineNumber) + + |srarr|\ NamedChunk user identifiers set and get (`65`_) + |srarr|\ NamedChunk add to the web (`66`_) + |srarr|\ NamedChunk weave into the documentation (`67`_) + |srarr|\ NamedChunk tangle into the source file (`68`_) .. .. class:: small - |loz| *NamedChunk class (63)*. Used by: Chunk class hierarchy... (`51`_) + |loz| *NamedChunk class (64)*. Used by: Chunk class hierarchy... (`51`_) The ``setUserIDRefs()`` method accepts a list of user identifiers that are @@ -3602,16 +3664,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) = +.. _`65`: +.. rubric:: NamedChunk user identifiers set and get (65) = .. 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 +3681,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 (65)*. Used by: NamedChunk class (`64`_) The ``webAdd()`` method adds this chunk to the given document ``Web`` instance. @@ -3628,22 +3690,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) = +.. _`66`: +.. rubric:: NamedChunk add to the web (66) = .. 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 (66)*. Used by: NamedChunk class (`64`_) The ``weave()`` method weaves this chunk into the final document as follows: @@ -3670,37 +3732,37 @@ The woven references simply follow whatever preceded them on the line; the inden -.. _`66`: -.. rubric:: NamedChunk weave into the documentation (66) = +.. _`67`: +.. rubric:: NamedChunk weave into the documentation (67) = .. 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 (67)*. Used by: NamedChunk class (`64`_) The ``tangle()`` method tangles this chunk into the final document as follows: @@ -3717,58 +3779,58 @@ context information. -.. _`67`: -.. rubric:: NamedChunk tangle into the source file (67) = +.. _`68`: +.. rubric:: NamedChunk tangle into the source file (68) = .. 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 (68)*. Used by: NamedChunk class (`64`_) 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) += +.. _`69`: +.. rubric:: NamedChunk class (69) += .. 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 (69)*. Used by: Chunk class hierarchy... (`51`_) OutputChunk class @@ -3790,28 +3852,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) = +.. _`70`: +.. rubric:: OutputChunk class (70) = .. 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 (`71`_) + |srarr|\ OutputChunk weave (`72`_) + |srarr|\ OutputChunk tangle (`73`_) .. .. class:: small - |loz| *OutputChunk class (69)*. Used by: Chunk class hierarchy... (`51`_) + |loz| *OutputChunk class (70)*. Used by: Chunk class hierarchy... (`51`_) The ``webAdd()`` method adds this chunk to the given document ``Web``. @@ -3820,22 +3882,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) = +.. _`71`: +.. rubric:: OutputChunk add to the web (71) = .. 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 (71)*. Used by: OutputChunk class (`70`_) The ``weave()`` method weaves this chunk into the final document as follows: @@ -3855,48 +3917,48 @@ context information. -.. _`71`: -.. rubric:: OutputChunk weave (71) = +.. _`72`: +.. rubric:: OutputChunk weave (72) = .. 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 (72)*. Used by: OutputChunk class (`70`_) 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) = +.. _`73`: +.. rubric:: OutputChunk tangle (73) = .. 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 (73)*. Used by: OutputChunk class (`70`_) NamedDocumentChunk class @@ -3920,25 +3982,27 @@ All other methods, including the tangle method are identical to ``NamedChunk``. -.. _`73`: -.. rubric:: NamedDocumentChunk class (73) = +.. _`74`: +.. rubric:: NamedDocumentChunk class (74) = .. 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 (`75`_) + |srarr|\ NamedDocumentChunk tangle (`76`_) .. .. class:: small - |loz| *NamedDocumentChunk class (73)*. Used by: Chunk class hierarchy... (`51`_) + |loz| *NamedDocumentChunk class (74)*. Used by: Chunk class hierarchy... (`51`_) The ``weave()`` method quietly ignores this chunk in the document. @@ -3954,48 +4018,48 @@ to insert the entire chunk. -.. _`74`: -.. rubric:: NamedDocumentChunk weave (74) = +.. _`75`: +.. rubric:: NamedDocumentChunk weave (75) = .. 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 (75)*. Used by: NamedDocumentChunk class (`74`_) -.. _`75`: -.. rubric:: NamedDocumentChunk tangle (75) = +.. _`76`: +.. rubric:: NamedDocumentChunk tangle (76) = .. 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 (76)*. Used by: NamedDocumentChunk class (`74`_) Commands @@ -4017,26 +4081,26 @@ cross reference information and tangle a file or weave the final document. -.. _`76`: -.. rubric:: Command class hierarchy - used to describe individual commands (76) = +.. _`77`: +.. rubric:: Command class hierarchy - used to describe individual commands (77) = .. 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 (`78`_) + |srarr|\ TextCommand class to contain a document text block (`81`_) + |srarr|\ CodeCommand class to contain a program source code block (`82`_) + |srarr|\ XrefCommand superclass for all cross-reference commands (`83`_) + |srarr|\ FileXrefCommand class for an output file cross-reference (`84`_) + |srarr|\ MacroXrefCommand class for a named chunk cross-reference (`85`_) + |srarr|\ UserIdXrefCommand class for a user identifier cross-reference (`86`_) + |srarr|\ ReferenceCommand class for chunk references (`87`_) .. .. 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 (77)*. Used by: Base Class Definitions (`1`_) Command Superclass @@ -4051,7 +4115,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,65 +4164,68 @@ The attributes of a ``Command`` instance includes the line number on which the command began, in ``lineNumber``. -.. _`77`: -.. rubric:: Command superclass (77) = +.. _`78`: +.. rubric:: Command superclass (78) = .. 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 ): + 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 "at {!r}".format(self.lineNumber) - |srarr|\ Command analysis features: starts-with and Regular Expression search (`78`_) - |srarr|\ Command tangle and weave functions (`79`_) + + |srarr|\ Command analysis features: starts-with and Regular Expression search (`79`_) + |srarr|\ Command tangle and weave functions (`80`_) .. .. class:: small - |loz| *Command superclass (77)*. Used by: Command class hierarchy... (`76`_) + |loz| *Command superclass (78)*. Used by: Command class hierarchy... (`77`_) -.. _`78`: -.. rubric:: Command analysis features: starts-with and Regular Expression search (78) = +.. _`79`: +.. rubric:: Command analysis features: starts-with and Regular Expression search (79) = .. 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 (79)*. Used by: Command superclass (`78`_) -.. _`79`: -.. rubric:: Command tangle and weave functions (79) = +.. _`80`: +.. rubric:: Command tangle and weave functions (80) = .. parsed-literal:: :class: code - def ref( self, aWeb ): + def ref(self, aWeb: "Web") -> str \| None: return None - def weave( self, aWeb, aWeaver ): + def weave(self, aWeb: "Web", aWeaver: "Weaver") -> None: pass - def tangle( self, aWeb, aTangler ): + def tangle(self, aWeb: "Web", aTangler: "Tangler") -> None: pass @@ -4166,7 +4233,7 @@ the command began, in ``lineNumber``. .. class:: small - |loz| *Command tangle and weave functions (79)*. Used by: Command superclass (`77`_) + |loz| *Command tangle and weave functions (80)*. Used by: Command superclass (`78`_) TextCommand class @@ -4188,24 +4255,24 @@ or tangler. -.. _`80`: -.. rubric:: TextCommand class to contain a document text block (80) = +.. _`81`: +.. rubric:: TextCommand class to contain a document text block (81) = .. 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 ): + def \_\_init\_\_(self, text: str, fromLine: int = 0) -> None: + super().\_\_init\_\_(fromLine) + self.text = text + def \_\_str\_\_(self) -> str: 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 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 +4280,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 (81)*. Used by: Command class hierarchy... (`77`_) CodeCommand class @@ -4247,25 +4314,25 @@ indentation is maintained. -.. _`81`: -.. rubric:: CodeCommand class to contain a program source code block (81) = +.. _`82`: +.. rubric:: CodeCommand class to contain a program source code block (82) = .. 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 (82)*. Used by: Command class hierarchy... (`77`_) XrefCommand superclass @@ -4294,22 +4361,24 @@ is illegal. An exception is raised and processing stops. -.. _`82`: -.. rubric:: XrefCommand superclass for all cross-reference commands (82) = +.. _`83`: +.. rubric:: XrefCommand superclass for all cross-reference commands (83) = .. parsed-literal:: :class: code - class XrefCommand( Command ): + class XrefCommand(Command): """Any of the Xref-goes-here commands in the input.""" - def \_\_str\_\_( self ): + def \_\_str\_\_(self) -> str: return "at {!r}: cross reference".format(self.lineNumber) - def formatXref( self, xref, aWeaver ): + + 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 +4386,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 (83)*. Used by: Command class hierarchy... (`77`_) FileXrefCommand class @@ -4333,24 +4402,24 @@ the ``formatXref()`` method of the ``XrefCommand`` superclass for format this r -.. _`83`: -.. rubric:: FileXrefCommand class for an output file cross-reference (83) = +.. _`84`: +.. rubric:: FileXrefCommand class for an output file cross-reference (84) = .. 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 (84)*. Used by: Command class hierarchy... (`77`_) MacroXrefCommand class @@ -4366,24 +4435,24 @@ the ``formatXref()`` method of the ``XrefCommand`` superclass method for format -.. _`84`: -.. rubric:: MacroXrefCommand class for a named chunk cross-reference (84) = +.. _`85`: +.. rubric:: MacroXrefCommand class for a named chunk cross-reference (85) = .. 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 (85)*. Used by: Command class hierarchy... (`77`_) UserIdXrefCommand class @@ -4408,22 +4477,22 @@ algorithm, which is similar to the algorithm in the ``XrefCommand`` superclass. -.. _`85`: -.. rubric:: UserIdXrefCommand class for a user identifier cross-reference (85) = +.. _`86`: +.. rubric:: UserIdXrefCommand class for a user identifier cross-reference (86) = .. 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 +4502,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 (86)*. Used by: Command class hierarchy... (`77`_) ReferenceCommand class @@ -4465,33 +4534,35 @@ of a ``ReferenceCommand``. -.. _`86`: -.. rubric:: ReferenceCommand class for chunk references (86) = +.. _`87`: +.. rubric:: ReferenceCommand class for chunk references (87) = .. 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 ): + 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 {!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`_) + + |srarr|\ ReferenceCommand resolve a referenced chunk name (`88`_) + |srarr|\ ReferenceCommand refers to a chunk (`89`_) + |srarr|\ ReferenceCommand weave a reference to a chunk (`90`_) + |srarr|\ ReferenceCommand tangle a referenced chunk (`91`_) .. .. class:: small - |loz| *ReferenceCommand class for chunk references (86)*. Used by: Command class hierarchy... (`76`_) + |loz| *ReferenceCommand class for chunk references (87)*. Used by: Command class hierarchy... (`77`_) The ``resolve()`` method queries the overall ``Web`` instance for the full @@ -4501,23 +4572,23 @@ to the chunk. -.. _`87`: -.. rubric:: ReferenceCommand resolve a referenced chunk name (87) = +.. _`88`: +.. rubric:: ReferenceCommand resolve a referenced chunk name (88) = .. 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 (88)*. Used by: ReferenceCommand class... (`87`_) The ``ref()`` method is a request that is delegated by a ``Chunk``; @@ -4527,15 +4598,15 @@ Chinks to which it refers. -.. _`88`: -.. rubric:: ReferenceCommand refers to a chunk (88) = +.. _`89`: +.. rubric:: ReferenceCommand refers to a chunk (89) = .. 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 +4614,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 (89)*. Used by: ReferenceCommand class... (`87`_) The ``weave()`` method inserts a markup reference to a named @@ -4552,23 +4623,23 @@ this appropriately for the document type being woven. -.. _`89`: -.. rubric:: ReferenceCommand weave a reference to a chunk (89) = +.. _`90`: +.. rubric:: ReferenceCommand weave a reference to a chunk (90) = .. 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 (90)*. Used by: ReferenceCommand class... (`87`_) The ``tangle()`` method inserts the resolved chunk in this @@ -4580,34 +4651,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) = +.. _`91`: +.. rubric:: ReferenceCommand tangle a referenced chunk (91) = .. 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}".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) ) + self.logger.debug(f"Tangling {self.fullName!r} with chunks {self.chunkList!r}") 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("Attempt to tangle an undefined Chunk, {!s}.".format(self.fullName,)) - 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 (91)*. Used by: ReferenceCommand class... (`87`_) Reference Strategy @@ -4630,24 +4701,24 @@ this object. -.. _`91`: -.. rubric:: Reference class hierarchy - strategies for references to a chunk (91) = +.. _`92`: +.. rubric:: Reference class hierarchy - strategies for references to a chunk (92) = .. parsed-literal:: :class: code class Reference: - def \_\_init\_\_( self ): - self.logger= logging.getLogger( self.\_\_class\_\_.\_\_qualname\_\_ ) - def chunkReferencedBy( self, aChunk ): + def \_\_init\_\_(self) -> None: + self.logger = logging.getLogger(self.\_\_class\_\_.\_\_qualname\_\_) + def chunkReferencedBy(self, aChunk: Chunk) -> list[Chunk]: """Return a list of Chunks.""" - pass + return [] .. .. 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 (92)*. Used by: Base Class Definitions (`1`_) SimpleReference Class @@ -4657,22 +4728,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) += +.. _`93`: +.. rubric:: Reference class hierarchy - strategies for references to a chunk (93) += .. 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 (93)*. Used by: Base Class Definitions (`1`_) TransitiveReference Class @@ -4685,32 +4756,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) += +.. _`94`: +.. rubric:: Reference class hierarchy - strategies for references to a chunk (94) += .. 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: {!s}({:d}) {!r}".format(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("References: {0:>{indent}s} {1!s}".format('--', final, indent=2\*depth)) 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 (94)*. Used by: Base Class Definitions (`1`_) @@ -4741,9 +4812,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 +4823,19 @@ but merely creates a distinct class to facilitate writing ``except`` statements. -.. _`94`: -.. rubric:: Error class - defines the errors raised (94) = +.. _`95`: +.. rubric:: Error class - defines the errors raised (95) = .. 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 (95)*. Used by: Base Class Definitions (`1`_) The Web and WebReader Classes @@ -4828,37 +4899,39 @@ A web instance has a number of attributes. named chunk. -.. _`95`: -.. rubric:: Web class - describes the overall "web" of chunks (95) = +.. _`96`: +.. rubric:: Web class - describes the overall "web" of chunks (96) = .. 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, filename: str \| None = None) -> None: + self.webFileName = filename + 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 "Web {!r}".format(self.webFileName,) - |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 (`98`_) + |srarr|\ Web Chunk name resolution methods (`103`_), |srarr|\ (`104`_) + |srarr|\ Web Chunk cross reference methods (`105`_), |srarr|\ (`107`_), |srarr|\ (`108`_), |srarr|\ (`109`_) + |srarr|\ Web determination of the language from the first chunk (`112`_) + |srarr|\ Web tangle the output files (`113`_) + |srarr|\ Web weave the output document (`114`_) .. .. 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 (96)*. Used by: Base Class Definitions (`1`_) Web Construction @@ -4880,8 +4953,8 @@ to contain a more complete description of the chunk. We include a weakref to the ``Web`` to each ``Chunk``. -.. _`96`: -.. rubric:: Imports (96) += +.. _`97`: +.. rubric:: Imports (97) += .. parsed-literal:: :class: code @@ -4893,26 +4966,26 @@ We include a weakref to the ``Web`` to each ``Chunk``. .. class:: small - |loz| *Imports (96)*. Used by: pyweb.py (`153`_) + |loz| *Imports (97)*. Used by: pyweb.py (`156`_) -.. _`97`: -.. rubric:: Web construction methods used by Chunks and WebReader (97) = +.. _`98`: +.. rubric:: Web construction methods used by Chunks and WebReader (98) = .. 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 (`99`_) + |srarr|\ Web add an anonymous chunk (`100`_) + |srarr|\ Web add a named macro chunk (`101`_) + |srarr|\ Web add an output file definition chunk (`102`_) .. .. 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 (98)*. Used by: Web class... (`96`_) A name is only added to the known names when it is @@ -4955,22 +5028,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) = +.. _`99`: +.. rubric:: Web add full chunk names, ignoring abbreviated names (99) = .. 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}".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) ) + self.named[nm] = [] + self.logger.debug("Adding empty chunk {!r}".format(name)) return nm @@ -4978,7 +5051,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 (99)*. Used by: Web construction... (`98`_) An anonymous ``Chunk`` is kept in a sequence of Chunks, used for @@ -4986,23 +5059,23 @@ tangling. -.. _`99`: -.. rubric:: Web add an anonymous chunk (99) = +.. _`100`: +.. rubric:: Web add an anonymous chunk (100) = .. 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 (100)*. Used by: Web construction... (`98`_) A named ``Chunk`` is defined with a ``@d`` command. @@ -5036,25 +5109,25 @@ 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) = +.. _`101`: +.. rubric:: Web add a named macro chunk (101) = .. 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}".format(nm, chunk.name)) else: raise Error("No full name for {!r}".format(chunk.name), chunk) @@ -5063,7 +5136,7 @@ in the list. Otherwise, it's False. .. class:: small - |loz| *Web add a named macro chunk (100)*. Used by: Web construction... (`97`_) + |loz| *Web add a named macro chunk (101)*. Used by: Web construction... (`98`_) An output file definition ``Chunk`` is defined with an ``@o`` @@ -5094,23 +5167,23 @@ If the chunk list was empty, this is the first chunk, the -.. _`101`: -.. rubric:: Web add an output file definition chunk (101) = +.. _`102`: +.. rubric:: Web add an output file definition chunk (102) = .. 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}".format(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 +5191,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 (102)*. Used by: Web construction... (`98`_) Web Chunk Name Resolution @@ -5149,20 +5222,20 @@ The ``fullNameFor()`` method resolves full name for a chunk as follows: -.. _`102`: -.. rubric:: Web Chunk name resolution methods (102) = +.. _`103`: +.. rubric:: Web Chunk name resolution methods (103) = .. 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] ) ] + 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)) ) ) + raise Error("Ambiguous abbreviation {!r}, matches {!r}".format(name, list(sorted(best))) ) elif len(best) == 1: return best[0] return name @@ -5172,7 +5245,7 @@ The ``fullNameFor()`` method resolves full name for a chunk as follows: .. class:: small - |loz| *Web Chunk name resolution methods (102)*. Used by: Web class... (`95`_) + |loz| *Web Chunk name resolution methods (103)*. Used by: Web class... (`96`_) The ``getchunk()`` method locates a named sequence of chunks by first determining the full name @@ -5183,28 +5256,28 @@ 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) += +.. _`104`: +.. rubric:: Web Chunk name resolution methods (104) += .. 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("Cannot resolve {!r} in {!r}".format(name,self.named.keys())) .. .. class:: small - |loz| *Web Chunk name resolution methods (103)*. Used by: Web class... (`95`_) + |loz| *Web Chunk name resolution methods (104)*. Used by: Web class... (`96`_) Web Cross-Reference Support @@ -5239,30 +5312,30 @@ 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) = +.. _`105`: +.. rubric:: Web Chunk cross reference methods (105) = .. 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 (`106`_) .. .. class:: small - |loz| *Web Chunk cross reference methods (104)*. Used by: Web class... (`95`_) + |loz| *Web Chunk cross reference methods (105)*. Used by: Web class... (`96`_) We verify that the reference count for a @@ -5270,39 +5343,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) = +.. _`106`: +.. rubric:: Web Chunk check reference counts are all one (106) = .. 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}".format(nm)) for nm in self.multi\_reference(): - self.logger.warn( "Multiple references to {!r}".format(nm) ) + self.logger.warning("Multiple references to {!r}".format(nm)) for nm in self.no\_definition(): - self.logger.error( "No definition for {!r}".format(nm) ) + self.logger.error("No definition for {!r}".format(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 (106)*. Used by: Web Chunk cross reference methods... (`105`_) -The one-pass version +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) ) + self.logger.warning("No reference to {!r}".format(nm)) elif cl[0].refCount > 1: - self.logger.warn( "Multiple references to {!r}".format(nm) ) + self.logger.warning("Multiple references to {!r}".format(nm)) else: - self.logger.error( "No definition for {!r}".format(nm) ) + self.logger.error("No definition for {!r}".format(nm)) We use three methods to filter chunk names into @@ -5315,25 +5388,25 @@ is a list of chunks referenced but not defined. -.. _`106`: -.. rubric:: Web Chunk cross reference methods (106) += +.. _`107`: +.. rubric:: Web Chunk cross reference methods (107) += .. 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 (107)*. Used by: Web class... (`96`_) The ``fileXref()`` method visits all named file output chunks in ``output`` and @@ -5345,21 +5418,21 @@ but applies it to the ``named`` mapping. -.. _`107`: -.. rubric:: Web Chunk cross reference methods (107) += +.. _`108`: +.. rubric:: Web Chunk cross reference methods (108) += .. 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 +5440,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 (108)*. Used by: Web class... (`96`_) The ``userNamesXref()`` method creates a mapping for each @@ -5377,7 +5450,7 @@ 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 +5466,32 @@ This works in two passes: -.. _`108`: -.. rubric:: Web Chunk cross reference methods (108) += +.. _`109`: +.. rubric:: Web Chunk cross reference methods (109) += .. 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 (`110`_) + + 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 (`111`_) .. .. class:: small - |loz| *Web Chunk cross reference methods (108)*. Used by: Web class... (`95`_) + |loz| *Web Chunk cross reference methods (109)*. Used by: Web class... (`96`_) User identifiers are collected by visiting each of the sequence of @@ -5428,8 +5503,8 @@ list as a default action. -.. _`109`: -.. rubric:: collect all user identifiers from a given map into ux (109) = +.. _`110`: +.. rubric:: collect all user identifiers from a given map into ux (110) = .. parsed-literal:: :class: code @@ -5437,13 +5512,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 (110)*. Used by: Web Chunk cross reference methods... (`109`_) User identifiers are cross-referenced by visiting @@ -5454,26 +5529,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) = +.. _`111`: +.. rubric:: find user identifier usage and update ux from the given map (111) = .. 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}".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 ) + 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 (111)*. Used by: Web Chunk cross reference methods... (`109`_) Loop Detection @@ -5526,17 +5601,17 @@ LaTeX files typically begin with '%' or '\'. Everything else is probably RST. -.. _`111`: -.. rubric:: Web determination of the language from the first chunk (111) = +.. _`112`: +.. rubric:: Web determination of the language from the first chunk (112) = .. 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}".format(str(self.chunkSeq[0])[:4])) if self.chunkSeq[0].startswith('<'): return HTML() if self.chunkSeq[0].startswith('%') or self.chunkSeq[0].startswith('\\\\'): @@ -5548,7 +5623,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 (112)*. Used by: Web class... (`96`_) The ``tangle()`` method of the ``Web`` class performs @@ -5557,24 +5632,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) = +.. _`113`: +.. rubric:: Web tangle the output files (113) = .. 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): 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 (113)*. Used by: Web class... (`96`_) The ``weave()`` method of the ``Web`` class creates the final documentation. @@ -5592,34 +5667,37 @@ The decision is delegated to the referenced chunk. Should it go in ``ReferenceCommand weave...``? -.. _`113`: -.. rubric:: Web weave the output document (113) = +.. _`114`: +.. rubric:: Web weave the output document (114) = .. parsed-literal:: :class: code - def weave( self, aWeaver ): - self.logger.debug( "Weaving file from {!r}".format(self.webFileName) ) - basename, \_ = os.path.splitext( self.webFileName ) + def weave(self, aWeaver: "Weaver") -> None: + self.logger.debug("Weaving file from {!r}".format(self.webFileName)) + if not self.webFileName: + raise Error("No filename supplied for weaving.") + 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) + c.weave(self, aWeaver) + + def weaveChunk(self, name: str, aWeaver: "Weaver") -> None: + 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 ) + 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 ) + 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 (114)*. Used by: Web class... (`96`_) The WebReader Class @@ -5631,7 +5709,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 +5719,7 @@ instance is created with code like the following: .. parsed-literal:: - c= WebReader( parent=parentWebReader ) + c = WebReader(parent=parentWebReader) @@ -5712,8 +5790,8 @@ The class has the following attributes: Summaries -.. _`114`: -.. rubric:: WebReader class - parses the input file, building the Web structure (114) = +.. _`115`: +.. rubric:: WebReader class - parses the input file, building the Web structure (115) = .. parsed-literal:: :class: code @@ -5721,56 +5799,62 @@ 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='\*'), + ) + + # State of reading and parsing. + tokenizer: Tokenizer + aChunk: Chunk + + # Configuration + command: str + permitList: list[str] + + # State of the reader + \_source: TextIO + fileName: str + theWeb: "Web" - 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 (`130`_) - def \_\_str\_\_( self ): + |srarr|\ WebReader command literals (`132`_) + + 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 location in the input stream (`129`_) + |srarr|\ WebReader load the web (`131`_) + |srarr|\ WebReader handle a command string (`116`_), |srarr|\ (`128`_) .. .. 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 (115)*. Used by: Base Class Definitions (`1`_) Command recognition is done via a **Chain of Command**-like design. @@ -5799,56 +5883,56 @@ 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) = +.. _`116`: +.. rubric:: WebReader handle a command string (116) = .. 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`_) + def handleCommand(self, token: str) -> bool: + self.logger.debug("Reading {!r}".format(token)) + |srarr|\ major commands segment the input into separate Chunks (`117`_) + |srarr|\ minor commands add Commands to the current Chunk (`122`_) 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.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 + return False # 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`_) + |loz| *WebReader handle a command string (116)*. Used by: WebReader class... (`115`_) 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:: major commands segment the input into separate Chunks (117) = .. parsed-literal:: :class: code if token[:2] == self.cmdo: - |srarr|\ start an OutputChunk, adding it to the web (`117`_) + |srarr|\ start an OutputChunk, adding it to the web (`118`_) elif token[:2] == self.cmdd: - |srarr|\ start a NamedChunk or NamedDocumentChunk, adding it to the web (`118`_) + |srarr|\ start a NamedChunk or NamedDocumentChunk, adding it to the web (`119`_) elif token[:2] == self.cmdi: - |srarr|\ import another file (`119`_) + |srarr|\ import another file (`120`_) elif token[:2] in (self.cmdrcurl,self.cmdrbrak): - |srarr|\ finish a chunk, start a new Chunk adding it to the web (`120`_) + |srarr|\ finish a chunk, start a new Chunk adding it to the web (`121`_) .. .. class:: small - |loz| *major commands segment the input into separate Chunks (116)*. Used by: WebReader handle a command... (`115`_) + |loz| *major commands segment the input into separate Chunks (117)*. Used by: WebReader handle a command... (`116`_) An output chunk has the form ``@o`` *name* ``@{`` *content* ``@}``. @@ -5859,31 +5943,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.fileName = self.fileName + 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: major commands... (`117`_) A named chunk has the form ``@d`` *name* ``@{`` *content* ``@}`` for @@ -5907,38 +5992,38 @@ If both are in the options, we can provide a warning, I guess. **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.fileName = self.fileName + 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: major commands... (`117`_) An import command has the unusual form of ``@i`` *name*, with no trailing @@ -5962,49 +6047,49 @@ 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:: import another file (120) = .. parsed-literal:: :class: code - incFile= next(self.tokenizer).strip() + incFile = next(self.tokenizer).strip() try: - self.logger.info( "Including {!r}".format(incFile) ) - include= WebReader( parent=self ) - include.load( self.theWeb, incFile ) + 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) ) + "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) ) + "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) ) + "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 ) + 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| *import another file (120)*. Used by: major commands... (`117`_) When a ``@}`` or ``@]`` are found, this finishes a named chunk. The next @@ -6020,20 +6105,20 @@ 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: major commands... (`117`_) The following sequence of ``elif`` statements identifies @@ -6041,32 +6126,32 @@ the minor commands that add ``Command`` instances to the current open ``Chunk``. -.. _`121`: -.. rubric:: minor commands add Commands to the current Chunk (121) = +.. _`122`: +.. rubric:: minor commands add Commands to the current Chunk (122) = .. parsed-literal:: :class: code elif token[:2] == self.cmdpipe: - |srarr|\ assign user identifiers to the current chunk (`122`_) + |srarr|\ assign user identifiers to the current chunk (`123`_) elif token[:2] == self.cmdf: - self.aChunk.append( FileXrefCommand(self.tokenizer.lineNumber) ) + self.aChunk.append(FileXrefCommand(self.tokenizer.lineNumber)) elif token[:2] == self.cmdm: - self.aChunk.append( MacroXrefCommand(self.tokenizer.lineNumber) ) + self.aChunk.append(MacroXrefCommand(self.tokenizer.lineNumber)) elif token[:2] == self.cmdu: - self.aChunk.append( UserIdXrefCommand(self.tokenizer.lineNumber) ) + self.aChunk.append(UserIdXrefCommand(self.tokenizer.lineNumber)) elif token[:2] == self.cmdlangl: - |srarr|\ add a reference command to the current chunk (`123`_) + |srarr|\ add a reference command to the current chunk (`124`_) elif token[:2] == self.cmdlexpr: - |srarr|\ add an expression command to the current chunk (`125`_) + |srarr|\ add an expression command to the current chunk (`126`_) elif token[:2] == self.cmdcmd: - |srarr|\ double at-sign replacement, append this character to previous TextCommand (`126`_) + |srarr|\ double at-sign replacement, append this character to previous TextCommand (`127`_) .. .. class:: small - |loz| *minor commands add Commands to the current Chunk (121)*. Used by: WebReader handle a command... (`115`_) + |loz| *minor commands add Commands to the current Chunk (122)*. Used by: WebReader handle a command... (`116`_) User identifiers occur after a ``@|`` in a ``NamedChunk``. @@ -6082,24 +6167,24 @@ User identifiers are name references at the end of a NamedChunk These are accumulated and expanded by ``@u`` reference -.. _`122`: -.. rubric:: assign user identifiers to the current chunk (122) = +.. _`123`: +.. rubric:: assign user identifiers to the current chunk (123) = .. parsed-literal:: :class: code 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 {!s}: {!s}".format(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 (123)*. Used by: minor commands... (`122`_) A reference command has the form ``@<``\ *name*\ ``@>``. We accept three @@ -6107,25 +6192,25 @@ tokens from the input, the middle token is the referenced name. -.. _`123`: -.. rubric:: add a reference command to the current chunk (123) = +.. _`124`: +.. rubric:: add a reference command to the current chunk (124) = .. 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) ) + 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 (123)*. Used by: minor commands... (`121`_) + |loz| *add a reference command to the current chunk (124)*. Used by: minor commands... (`122`_) An expression command has the form ``@(``\ *Python Expression*\ ``@)``. @@ -6137,7 +6222,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,12 +6230,12 @@ 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`: -.. rubric:: Imports (124) += +.. _`125`: +.. rubric:: Imports (125) += .. parsed-literal:: :class: code @@ -6164,48 +6249,51 @@ An ``os.getcwd()`` must be changed to ``os.path.realpath('.')``. .. class:: small - |loz| *Imports (124)*. Used by: pyweb.py (`153`_) + |loz| *Imports (125)*. Used by: pyweb.py (`156`_) -.. _`125`: -.. rubric:: add an expression command to the current chunk (125) = +.. _`126`: +.. rubric:: add an expression command to the current chunk (126) = .. parsed-literal:: :class: code # 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) + 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 ('breakpoint', 'compile', 'eval', 'exec', 'execfile', 'globals', 'help', 'input', 'memoryview', 'open', 'print', 'super', '\_\_import\_\_') + )) + globals = dict( + \_\_builtins\_\_=safe, + os=types.SimpleNamespace(path=os.path, getcwd=os.getcwd, name=os.name), + time=time, + 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)) + result = str(eval(expression, globals)) except Exception as e: - self.logger.error( 'Failure to process {!r}: result is {!r}'.format(expression, 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 ) + 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 (125)*. Used by: minor commands... (`121`_) + |loz| *add an expression command to the current chunk (126)*. Used by: minor commands... (`122`_) A double command sequence (``'@@'``, when the command is an ``'@'``) has the @@ -6219,19 +6307,19 @@ And we make sure the next chunk will be appended to this so that it's largely seamless. -.. _`126`: -.. rubric:: double at-sign replacement, append this character to previous TextCommand (126) = +.. _`127`: +.. rubric:: double at-sign replacement, append this character to previous TextCommand (127) = .. parsed-literal:: :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 (127)*. Used by: minor commands... (`122`_) The ``expect()`` method examines the @@ -6240,25 +6328,25 @@ If this is not found, a standard type of error message is raised. This is used by ``handleCommand()``. -.. _`127`: -.. rubric:: WebReader handle a command string (127) += +.. _`128`: +.. rubric:: WebReader handle a command string (128) += .. parsed-literal:: :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".format(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}".format(self.location(),tokens,t)) self.errors += 1 - return + return None return t @@ -6266,7 +6354,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 (128)*. Used by: WebReader class... (`115`_) The ``location()`` provides the file name and line number. @@ -6274,13 +6362,13 @@ This allows error messages as well as tangled or woven output to correctly reference the original input files. -.. _`128`: -.. rubric:: WebReader location in the input stream (128) = +.. _`129`: +.. rubric:: WebReader location in the input stream (129) = .. parsed-literal:: :class: code - def location( self ): + def location(self) -> tuple[str, int]: return (self.fileName, self.tokenizer.lineNumber+1) @@ -6288,7 +6376,7 @@ to correctly reference the original input files. .. class:: small - |loz| *WebReader location in the input stream (128)*. Used by: WebReader class... (`114`_) + |loz| *WebReader location in the input stream (129)*. Used by: WebReader class... (`115`_) The ``load()`` method reads the entire input file as a sequence @@ -6302,52 +6390,68 @@ The ``load()`` method is used recursively to handle the ``@i`` command. The issu is that it's always loading a single top-level web. -.. _`129`: -.. rubric:: WebReader load the web (129) = +.. _`130`: +.. rubric:: Imports (130) += +.. parsed-literal:: + :class: code + + from typing import TextIO + +.. + + .. class:: small + + |loz| *Imports (130)*. Used by: pyweb.py (`156`_) + + + +.. _`131`: +.. rubric:: WebReader load the web (131) = .. parsed-literal:: :class: code - def load( self, web, filename, source=None ): - self.theWeb= web - self.fileName= filename + def load(self, web: "Web", filename: str, source: TextIO \| None = None) -> "WebReader": + 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 + self.theWeb.webFileName = self.fileName if source: - self.\_source= source + self.\_source = source self.parse\_source() else: - with open( self.fileName, "r" ) as self.\_source: + 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 + return self - self.aChunk= Chunk() # Initial anonymous chunk of text. - self.aChunk.webAdd( self.theWeb ) + def parse\_source(self) -> None: + self.tokenizer = Tokenizer(self.\_source, self.command) + self.totalFiles += 1 - 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 ) + 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.warning('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... (`114`_) + |loz| *WebReader load the web (131)*. Used by: WebReader class... (`115`_) The command character can be changed to permit @@ -6358,39 +6462,39 @@ command character. -.. _`130`: -.. rubric:: WebReader command literals (130) = +.. _`132`: +.. rubric:: WebReader command literals (132) = .. 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 (132)*. Used by: WebReader class... (`115`_) @@ -6432,45 +6536,46 @@ 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) += +.. _`133`: +.. rubric:: Imports (133) += .. parsed-literal:: :class: code import re + from collections.abc import Iterator, Iterable .. .. class:: small - |loz| *Imports (131)*. Used by: pyweb.py (`153`_) + |loz| *Imports (133)*. Used by: pyweb.py (`156`_) -.. _`132`: -.. rubric:: Tokenizer class - breaks input into tokens (132) = +.. _`134`: +.. rubric:: Tokenizer class - breaks input into tokens (134) = .. 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(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) -> 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 +6583,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 (134)*. Used by: Base Class Definitions (`1`_) The Option Parser Class @@ -6505,8 +6610,8 @@ To handle this, we have a separate lexical scanner and parser for these two commands. -.. _`133`: -.. rubric:: Imports (133) += +.. _`135`: +.. rubric:: Imports (135) += .. parsed-literal:: :class: code @@ -6518,7 +6623,7 @@ two commands. .. class:: small - |loz| *Imports (133)*. Used by: pyweb.py (`153`_) + |loz| *Imports (135)*. Used by: pyweb.py (`156`_) Here's how we can define an option. @@ -6526,32 +6631,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) = +.. _`136`: +.. rubric:: Option Parser class - locates optional values on commands (136) = +.. parsed-literal:: + :class: code + + + class ParseError(Exception): pass + +.. + + .. class:: small + + |loz| *Option Parser class - locates optional values on commands (136)*. Used by: Base Class Definitions (`1`_) + + + +.. _`137`: +.. rubric:: Option Parser class - locates optional values on commands (137) += .. 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 (137)*. Used by: Base Class Definitions (`1`_) The parser breaks the text into words using ``shelex`` rules. @@ -6559,33 +6680,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) += +.. _`138`: +.. rubric:: Option Parser class - locates optional values on commands (138) += .. 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("Error parsing options in {!r}".format(text)) + 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 +6719,28 @@ final argument value. yield option, value option, value = word, [] else: - raise ParseError( "Unknown option {0}".format(word) ) + raise ParseError("Unknown option {0}".format(word)) 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 (138)*. Used by: Base Class Definitions (`1`_) In principle, we step through the trailers based on nargs counts. @@ -6627,11 +6753,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?? yield trailers[0], " ".join(final) - final= 0 + final = 0 trailers.pop(0) yield trailers[0], " ".join(final) @@ -6653,23 +6779,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" ) + 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" ) + 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 +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 +6805,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) = +.. _`139`: +.. rubric:: Action class hierarchy - used to describe basic actions of the application (139) = .. 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 (`140`_) + |srarr|\ ActionSequence subclass that holds a sequence of other actions (`143`_) + |srarr|\ WeaveAction subclass initiates the weave action (`147`_) + |srarr|\ TangleAction subclass initiates the tangle action (`150`_) + |srarr|\ LoadAction subclass loads the document web (`153`_) .. .. 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 basic actions of the application (139)*. 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 +6844,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 +6868,33 @@ An ``Action`` has a number of common attributes. -.. _`137`: -.. rubric:: Action superclass has common features of all actions (137) = +.. _`140`: +.. rubric:: Action superclass has common features of all actions (140) = .. 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 "{!s} [{!s}]".format(self.name, self.web) + + |srarr|\ Action call method actually does the real work (`141`_) + |srarr|\ Action final summary of what was done (`142`_) .. .. 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 (140)*. Used by: Action class hierarchy... (`139`_) The ``__call__()`` method does the real work of the action. @@ -6774,22 +6902,22 @@ 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) = +.. _`141`: +.. rubric:: Action call method actually does the real work (141) = .. 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}".format(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 (141)*. Used by: Action superclass... (`140`_) The ``summary()`` method returns some basic processing @@ -6797,24 +6925,25 @@ statistics for this action. -.. _`139`: -.. rubric:: Action final summary of what was done (139) = +.. _`142`: +.. rubric:: Action final summary of what was done (142) = .. 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 "{!s} in {:0.3f} sec.".format(self.name, self.duration()) .. .. class:: small - |loz| *Action final summary of what was done (139)*. Used by: Action superclass... (`137`_) + |loz| *Action final summary of what was done (142)*. Used by: Action superclass... (`140`_) ActionSequence Class @@ -6834,30 +6963,32 @@ an ``append()`` method that is used to construct the sequence of actions. -.. _`140`: -.. rubric:: ActionSequence subclass that holds a sequence of other actions (140) = +.. _`143`: +.. rubric:: ActionSequence subclass that holds a sequence of other actions (143) = .. 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 (`144`_) + |srarr|\ ActionSequence append adds a new action to the sequence (`145`_) + |srarr|\ ActionSequence summary summarizes each step (`146`_) .. .. 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 (143)*. Used by: Action class hierarchy... (`139`_) Since the macro ``__call__()`` method delegates to other Actions, @@ -6866,16 +6997,16 @@ 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) = +.. _`144`: +.. rubric:: ActionSequence call method delegates the sequence of ations (144) = .. parsed-literal:: :class: code - def \_\_call\_\_( self ): + def \_\_call\_\_(self) -> None: for o in self.opSequence: - o.web= self.web - o.options= self.options + o.web = self.web + o.options = self.options o() @@ -6883,49 +7014,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 (144)*. Used by: ActionSequence subclass... (`143`_) 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) = +.. _`145`: +.. rubric:: ActionSequence append adds a new action to the sequence (145) = .. 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 (145)*. Used by: ActionSequence subclass... (`143`_) The ``summary()`` method returns some basic processing statistics for each step of this action. -.. _`143`: -.. rubric:: ActionSequence summary summarizes each step (143) = +.. _`146`: +.. rubric:: ActionSequence summary summarizes each step (146) = .. 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 (146)*. Used by: ActionSequence subclass... (`143`_) WeaveAction Class @@ -6943,28 +7074,29 @@ 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) = +.. _`147`: +.. rubric:: WeaveAction subclass initiates the weave action (147) = .. 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 "{!s} [{!s}, {!s}]".format(self.name, self.web, self.options.theWeaver) - |srarr|\ WeaveAction call method to pick the language (`145`_) - |srarr|\ WeaveAction summary of language choice (`146`_) + |srarr|\ WeaveAction call method to pick the language (`148`_) + |srarr|\ WeaveAction summary of language choice (`149`_) .. .. class:: small - |loz| *WeaveAction subclass initiates the weave action (144)*. Used by: Action class hierarchy... (`136`_) + |loz| *WeaveAction subclass initiates the weave action (147)*. Used by: Action class hierarchy... (`139`_) The language is picked just prior to weaving. It is either (1) the language @@ -6975,26 +7107,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) = +.. _`148`: +.. rubric:: WeaveAction call method to pick the language (148) = .. 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 {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" ) + 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) ) + "Problems weaving document from {!s} (weave file is faulty).".format( self.web.webFileName)) #raise @@ -7002,7 +7133,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 (148)*. Used by: WeaveAction subclass... (`147`_) The ``summary()`` method returns some basic processing @@ -7010,24 +7141,24 @@ statistics for the weave action. -.. _`146`: -.. rubric:: WeaveAction summary of language choice (146) = +.. _`149`: +.. rubric:: WeaveAction summary of language choice (149) = .. 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, + return "{!s} {:d} lines in {:0.3f} sec.".format( self.name, self.options.theWeaver.linesWritten, self.duration() ) - return "did not {!s}".format( self.name, ) + return "did not {!s}".format(self.name,) .. .. class:: small - |loz| *WeaveAction summary of language choice (146)*. Used by: WeaveAction subclass... (`144`_) + |loz| *WeaveAction summary of language choice (149)*. Used by: WeaveAction subclass... (`147`_) TangleAction Class @@ -7044,25 +7175,26 @@ 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) = +.. _`150`: +.. rubric:: TangleAction subclass initiates the tangle action (150) = .. 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 (`151`_) + |srarr|\ TangleAction summary method provides total lines tangled (`152`_) .. .. class:: small - |loz| *TangleAction subclass initiates the tangle action (147)*. Used by: Action class hierarchy... (`136`_) + |loz| *TangleAction subclass initiates the tangle action (150)*. Used by: Action class hierarchy... (`139`_) Tangling can only raise an exception when a cross reference request (``@f``, ``@m`` or ``@u``) @@ -7071,21 +7203,20 @@ with any of ``@d`` or ``@o`` and use ``@{`` ``@}`` brackets. -.. _`148`: -.. rubric:: TangleAction call method does tangling of the output files (148) = +.. _`151`: +.. rubric:: TangleAction call method does tangling of the output files (151) = .. 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 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) ) + "Problems tangling outputs from {!r} (tangle files are faulty).".format( self.web.webFileName)) #raise @@ -7093,31 +7224,31 @@ 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 (151)*. Used by: TangleAction subclass... (`150`_) The ``summary()`` method returns some basic processing statistics for the tangle action. -.. _`149`: -.. rubric:: TangleAction summary method provides total lines tangled (149) = +.. _`152`: +.. rubric:: TangleAction summary method provides total lines tangled (152) = .. 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, + return "{!s} {:d} lines in {:0.3f} sec.".format( self.name, self.options.theTangler.totalLines, self.duration() ) - return "did not {!r}".format( self.name, ) + return "did not {!r}".format(self.name,) .. .. class:: small - |loz| *TangleAction summary method provides total lines tangled (149)*. Used by: TangleAction subclass... (`147`_) + |loz| *TangleAction summary method provides total lines tangled (152)*. Used by: TangleAction subclass... (`150`_) @@ -7136,27 +7267,27 @@ The options **must** include ``webReader``, with the ``WebReader`` instance to b -.. _`150`: -.. rubric:: LoadAction subclass loads the document web (150) = +.. _`153`: +.. rubric:: LoadAction subclass loads the document web (153) = .. 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 "Load [{!s}, {!s}]".format(self.webReader, self.web) + |srarr|\ LoadAction call method loads the input files (`154`_) + |srarr|\ LoadAction summary provides lines read (`155`_) .. .. class:: small - |loz| *LoadAction subclass loads the document web (150)*. Used by: Action class hierarchy... (`136`_) + |loz| *LoadAction subclass loads the document web (153)*. Used by: Action class hierarchy... (`139`_) Trying to load the web involves two steps, either of which can raise @@ -7179,29 +7310,29 @@ exceptions due to incorrect inputs. chunk reference cannot be resolved to a named chunk. -.. _`151`: -.. rubric:: LoadAction call method loads the input files (151) = +.. _`154`: +.. rubric:: LoadAction call method loads the input files (154) = .. 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.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 ) + 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.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,21 +7345,21 @@ 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 (154)*. Used by: LoadAction subclass... (`153`_) The ``summary()`` method returns some basic processing statistics for the load action. -.. _`152`: -.. rubric:: LoadAction summary provides lines read (152) = +.. _`155`: +.. rubric:: LoadAction summary provides lines read (155) = .. parsed-literal:: :class: code - def summary( self ): - return "{!s} {:d} lines from {:d} files in {:0.2f} sec.".format( + def summary(self) -> str: + return "{!s} {:d} lines from {:d} files in {:0.3f} sec.".format( self.name, self.webReader.totalLines, self.webReader.totalFiles, self.duration() ) @@ -7237,7 +7368,7 @@ statistics for the load action. .. class:: small - |loz| *LoadAction summary provides lines read (152)*. Used by: LoadAction subclass... (`150`_) + |loz| *LoadAction summary provides lines read (155)*. Used by: LoadAction subclass... (`153`_) @@ -7247,23 +7378,23 @@ statistics for the load action. The **pyWeb** application file is shown below: -.. _`153`: -.. rubric:: pyweb.py (153) = +.. _`156`: +.. rubric:: pyweb.py (156) = .. 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 (`158`_), |srarr|\ (`159`_), |srarr|\ (`160`_) + |srarr|\ Imports (`11`_), |srarr|\ (`47`_), |srarr|\ (`57`_), |srarr|\ (`97`_), |srarr|\ (`125`_), |srarr|\ (`130`_), |srarr|\ (`133`_), |srarr|\ (`135`_), |srarr|\ (`157`_), |srarr|\ (`161`_), |srarr|\ (`167`_) |srarr|\ Base Class Definitions (`1`_) - |srarr|\ Application Class (`159`_), |srarr|\ (`160`_) - |srarr|\ Logging Setup (`165`_), |srarr|\ (`166`_) - |srarr|\ Interface Functions (`167`_) + |srarr|\ Application Class (`162`_), |srarr|\ (`163`_) + |srarr|\ Logging Setup (`168`_), |srarr|\ (`169`_) + |srarr|\ Interface Functions (`170`_) .. .. class:: small - |loz| *pyweb.py (153)*. + |loz| *pyweb.py (156)*. The `Overheads`_ are described below, they include things like: @@ -7309,8 +7440,8 @@ closer to where they're referenced. -.. _`154`: -.. rubric:: Imports (154) += +.. _`157`: +.. rubric:: Imports (157) += .. parsed-literal:: :class: code @@ -7325,7 +7456,7 @@ closer to where they're referenced. .. class:: small - |loz| *Imports (154)*. Used by: pyweb.py (`153`_) + |loz| *Imports (157)*. Used by: pyweb.py (`156`_) Note that ``os.path``, ``time``, ``datetime`` and ``platform``` @@ -7343,8 +7474,8 @@ file as standard input. -.. _`155`: -.. rubric:: Overheads (155) = +.. _`158`: +.. rubric:: Overheads (158) = .. parsed-literal:: :class: code @@ -7354,7 +7485,7 @@ file as standard input. .. class:: small - |loz| *Overheads (155)*. Used by: pyweb.py (`153`_) + |loz| *Overheads (158)*. Used by: pyweb.py (`156`_) A Python ``__doc__`` string provides a standard vehicle for documenting @@ -7364,12 +7495,12 @@ detailed usage information. -.. _`156`: -.. rubric:: Overheads (156) += +.. _`159`: +.. rubric:: Overheads (159) += .. parsed-literal:: :class: code - """pyWeb Literate Programming - tangle and weave tool. + """py-web-tool Literate Programming. Yet another simple literate programming tool derived from nuweb, implemented entirely in Python. @@ -7401,7 +7532,7 @@ detailed usage information. .. class:: small - |loz| *Overheads (156)*. Used by: pyweb.py (`153`_) + |loz| *Overheads (159)*. Used by: pyweb.py (`156`_) The keyword cruft is a standard way of placing version control information into @@ -7413,23 +7544,23 @@ We also sneak in a "DO NOT EDIT" warning that belongs in all generated applicati source files. -.. _`157`: -.. rubric:: Overheads (157) += +.. _`160`: +.. rubric:: Overheads (160) += .. 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 pyweb-3.0.py, \_\_version\_\_='3.0'. + ### From source pyweb.w modified Wed Jun 8 14:04:44 2022. + ### In working directory '/Users/slott/Documents/Projects/py-web-tool'. .. .. class:: small - |loz| *Overheads (157)*. Used by: pyweb.py (`153`_) + |loz| *Overheads (160)*. Used by: pyweb.py (`156`_) @@ -7465,13 +7596,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 +7614,8 @@ The configuration can be either a ``types.SimpleNamespace`` or an -.. _`158`: -.. rubric:: Imports (158) += +.. _`161`: +.. rubric:: Imports (161) += .. parsed-literal:: :class: code @@ -7495,29 +7626,30 @@ The configuration can be either a ``types.SimpleNamespace`` or an .. class:: small - |loz| *Imports (158)*. Used by: pyweb.py (`153`_) + |loz| *Imports (161)*. Used by: pyweb.py (`156`_) -.. _`159`: -.. rubric:: Application Class (159) = +.. _`162`: +.. rubric:: Application Class (162) = .. 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 (`164`_) + + |srarr|\ Application parse command line (`165`_) + |srarr|\ Application class process all files (`166`_) .. .. class:: small - |loz| *Application Class (159)*. Used by: pyweb.py (`153`_) + |loz| *Application Class (162)*. Used by: pyweb.py (`156`_) The first part of parsing the command line is @@ -7595,8 +7727,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) += +.. _`163`: +.. rubric:: Application Class (163) += .. parsed-literal:: :class: code @@ -7613,45 +7745,45 @@ 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 (163)*. Used by: pyweb.py (`156`_) 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) = +.. _`164`: +.. rubric:: Application default options (164) = .. 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, ) - 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 (164)*. Used by: Application Class... (`162`_) The algorithm for parsing the command line parameters uses the built in @@ -7663,29 +7795,29 @@ instances. -.. _`162`: -.. rubric:: Application parse command line (162) = +.. _`165`: +.. rubric:: Application parse command line (165) = .. 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("files", nargs='+') + 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. """ @@ -7694,27 +7826,27 @@ instances. elif config.reference == 's': config.reference\_style = SimpleReference() else: - raise Error( "Improper configuration" ) + raise Error("Improper configuration") 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("{0!r} not a subclass of Weaver".format(weaver\_class)) + config.theWeaver = weaver\_class() - config.theTangler= TanglerMake() + 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 = ['{!s}{!s}'.format(config.command, c) for c in config.permit] else: - config.permitList= [] + config.permitList = [] - config.webReader= WebReader() + config.webReader = WebReader() return config @@ -7724,7 +7856,7 @@ instances. .. class:: small - |loz| *Application parse command line (162)*. Used by: Application Class... (`159`_) + |loz| *Application parse command line (165)*. Used by: Application Class... (`162`_) The ``process()`` function uses the current ``Application`` settings @@ -7750,46 +7882,46 @@ The re-raising is done so that all exceptions are handled by the outermost main program. -.. _`163`: -.. rubric:: Application class process all files (163) = +.. _`166`: +.. rubric:: Application class process all files (166) = .. parsed-literal:: :class: code - def process( self, config ): - root= logging.getLogger() - root.setLevel( config.verbosity ) + def process(self, config: argparse.Namespace) -> None: + 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) ) + 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 + self.theAction = self.doTangle elif config.skip.lower().startswith('t'): # not tangling == weaving - self.theAction= self.doWeave + self.theAction = self.doWeave else: - raise Exception( "Unknown -x option {!r}".format(config.skip) ) + raise Exception("Unknown -x option {!r}".format(config.skip)) - self.logger.info( "Weaver {!s}".format(config.theWeaver) ) + 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 + 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() ) + 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 (166)*. Used by: Application Class... (`162`_) Logging Setup @@ -7800,8 +7932,8 @@ function in an explicit ``with`` statement that assures that logging is configured and cleaned up politely. -.. _`164`: -.. rubric:: Imports (164) += +.. _`167`: +.. rubric:: Imports (167) += .. parsed-literal:: :class: code @@ -7814,7 +7946,7 @@ configured and cleaned up politely. .. class:: small - |loz| *Imports (164)*. Used by: pyweb.py (`153`_) + |loz| *Imports (167)*. Used by: pyweb.py (`156`_) This has two configuration approaches. If a positional argument is given, @@ -7825,23 +7957,23 @@ A subclass might properly load a dictionary encoded in YAML and use that with ``logging.config.dictConfig``. -.. _`165`: -.. rubric:: Logging Setup (165) = +.. _`168`: +.. rubric:: Logging Setup (168) = .. 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 +7981,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 (168)*. Used by: pyweb.py (`156`_) Here's a sample logging setup. This creates a simple console handler and @@ -7859,44 +7991,44 @@ It defines the root logger plus two overrides for class loggers that might be used to gather additional information. -.. _`166`: -.. rubric:: Logging Setup (166) += +.. _`169`: +.. rubric:: Logging Setup (169) += .. 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. + '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 (169)*. Used by: pyweb.py (`156`_) This seems a bit verbose; a separate configuration file might be better. @@ -7918,26 +8050,26 @@ We might also want to parse a logging configuration file, as well as a weaver template configuration file. -.. _`167`: -.. rubric:: Interface Functions (167) = +.. _`170`: +.. rubric:: Interface Functions (170) = .. 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 (170)*. Used by: pyweb.py (`156`_) This can be extended by doing something like the following. @@ -7953,18 +8085,17 @@ 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 Unit Tests @@ -8027,8 +8158,8 @@ Note the general flow of this top-level script. a summary. -.. _`168`: -.. rubric:: tangle.py (168) = +.. _`171`: +.. rubric:: tangle.py (171) = .. parsed-literal:: :class: code @@ -8038,34 +8169,34 @@ Note the general flow of this top-level script. import logging import argparse - with pyweb.Logger( pyweb.log\_config ): - logger= logging.getLogger(\_\_file\_\_) + 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(), + 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() + w = pyweb.Web() for action in LoadAction(), TangleAction(): - action.web= w - action.options= options + action.web = w + action.options = options action() - logger.info( action.summary() ) + logger.info(action.summary()) .. .. class:: small - |loz| *tangle.py (168)*. + |loz| *tangle.py (171)*. ``weave.py`` Script @@ -8078,25 +8209,25 @@ to define a customized set of templates for a different markup language. A customized weaver generally has three parts. -.. _`169`: -.. rubric:: weave.py (169) = +.. _`172`: +.. rubric:: weave.py (172) = .. 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 (`173`_) + |srarr|\ weave.py custom weaver definition to customize the Weaver being used (`174`_) + |srarr|\ weaver.py processing: load and weave the document (`175`_) .. .. class:: small - |loz| *weave.py (169)*. + |loz| *weave.py (172)*. -.. _`170`: -.. rubric:: weave.py overheads for correct operation of a script (170) = +.. _`173`: +.. rubric:: weave.py overheads for correct operation of a script (173) = .. parsed-literal:: :class: code @@ -8111,37 +8242,37 @@ A customized weaver generally has three parts. .. 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 (173)*. Used by: weave.py (`172`_) -.. _`171`: -.. rubric:: weave.py custom weaver definition to customize the Weaver being used (171) = +.. _`174`: +.. rubric:: weave.py custom weaver definition to customize the Weaver being used (174) = .. 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,72 +8281,72 @@ 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 (174)*. Used by: weave.py (`172`_) -.. _`172`: -.. rubric:: weaver.py processing: load and weave the document (172) = +.. _`175`: +.. rubric:: weaver.py processing: load and weave the document (175) = .. parsed-literal:: :class: code - with pyweb.Logger( pyweb.log\_config ): - logger= logging.getLogger(\_\_file\_\_) + 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(), + 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() + w = pyweb.Web() for action in LoadAction(), WeaveAction(): - action.web= w - action.options= options + action.web = w + action.options = options action() - logger.info( action.summary() ) + logger.info(action.summary()) .. .. 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 (175)*. Used by: weave.py (`172`_) -The ``setup.py`` and ``MANIFEST.in`` files --------------------------------------------- +The ``setup.py``, ``requirements-dev.txt`` and ``MANIFEST.in`` files +--------------------------------------------------------------------- In order to support a pleasant installation, the ``setup.py`` file is helpful. -.. _`173`: -.. rubric:: setup.py (173) = +.. _`176`: +.. rubric:: setup.py (176) = .. parsed-literal:: :class: code @@ -8224,9 +8355,9 @@ In order to support a pleasant installation, the ``setup.py`` file is helpful. 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='pyWeb 3.1: Yet Another Literate Programming Tool', author='S. Lott', author\_email='s\_lott@yahoo.com', url='http://slott-softwarearchitect.blogspot.com/', @@ -8243,7 +8374,7 @@ In order to support a pleasant installation, the ``setup.py`` file is helpful. .. class:: small - |loz| *setup.py (173)*. + |loz| *setup.py (176)*. In order build a source distribution kit the ``python3 setup.py sdist`` requires a @@ -8252,8 +8383,8 @@ that specifies additional rules. We use a simple inclusion to augment the default manifest rules. -.. _`174`: -.. rubric:: MANIFEST.in (174) = +.. _`177`: +.. rubric:: MANIFEST.in (177) = .. parsed-literal:: :class: code @@ -8265,7 +8396,27 @@ We use a simple inclusion to augment the default manifest rules. .. class:: small - |loz| *MANIFEST.in (174)*. + |loz| *MANIFEST.in (177)*. + + +In order to install dependencies, the following file is also used. + + +.. _`178`: +.. rubric:: requirements-dev.txt (178) = +.. parsed-literal:: + :class: code + + + docutils==0.18.1 + tox==3.25.0 + mypy==0.910 + +.. + + .. class:: small + + |loz| *requirements-dev.txt (178)*. The ``README`` file @@ -8274,12 +8425,12 @@ The ``README`` file Here's the README file. -.. _`175`: -.. rubric:: README (175) = +.. _`179`: +.. rubric:: README (179) = .. parsed-literal:: :class: code - 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 @@ -8295,7 +8446,7 @@ Here's the README file. 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. @@ -8303,16 +8454,24 @@ Here's the README file. Installation ------------- + This requires Python 3.10. + + First, downnload the distribution kit from PyPI. + :: python3 setup.py install - This will install the pyweb module. + This will install the \`\`pyweb\`\` module, and the \`\`weave\`\` and \`\`tangle\`\` applications. - Document production - -------------------- + Produce Documentation + --------------------- - The supplied documentation uses RST markup and requires docutils. + The supplied documentation uses RST markup; it requires docutils. + + :: + + python3 -m pip install docutils :: @@ -8322,16 +8481,16 @@ Here's the README file. 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. 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 :: @@ -8369,7 +8528,7 @@ Here's the README file. .. class:: small - |loz| *README (175)*. + |loz| *README (179)*. The HTML Support Files @@ -8382,15 +8541,15 @@ The default CSS file (stylesheet-path) may need to be customized for your installation of docutils. -.. _`176`: -.. rubric:: docutils.conf (176) = +.. _`180`: +.. rubric:: docutils.conf (180) = .. 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, + stylesheet-path: /Users/slott/miniconda3/envs/pywebtool/lib/python3.10/site-packages/docutils/writers/html4css1/html4css1.css, page-layout.css syntax-highlight: long @@ -8398,15 +8557,15 @@ installation of docutils. .. class:: small - |loz| *docutils.conf (176)*. + |loz| *docutils.conf (180)*. ``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) = +.. _`181`: +.. rubric:: page-layout.css (181) = .. parsed-literal:: :class: code @@ -8432,15 +8591,15 @@ the resulting HTML pages are easier to read. .. class:: small - |loz| *page-layout.css (177)*. + |loz| *page-layout.css (181)*. 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) = +.. _`182`: +.. rubric:: .nojekyll (182) = .. parsed-literal:: :class: code @@ -8450,14 +8609,14 @@ bug in ``NamedChunk.tangle()`` that prevents handling zero-length text. .. class:: small - |loz| *.nojekyll (178)*. + |loz| *.nojekyll (182)*. -Finally, an ``index.html`` to redirect GitHub to the ``pyweb.html`` file. +Here's an ``index.html`` to redirect GitHub to the ``pyweb.html`` file. -.. _`179`: -.. rubric:: index.html (179) = +.. _`183`: +.. rubric:: index.html (183) = .. parsed-literal:: :class: code @@ -8474,7 +8633,89 @@ Finally, an ``index.html`` to redirect GitHub to the ``pyweb.html`` file. .. class:: small - |loz| *index.html (179)*. + |loz| *index.html (183)*. + + + +Tox and Makefile +---------------- + +It's simpler to have a ``Makefile`` to automate testing, particularly when making changes +to **py-web-tool**. + +Note that there are tabs in this file. We bootstrap the next version from the 3.0 version. + + +.. _`184`: +.. rubric:: Makefile (184) = +.. parsed-literal:: + :class: code + + # Makefile for py-web-tool. + # Requires a pyweb-3.0.py (untouched) to bootstrap the current version. + + SOURCE = pyweb.w intro.w overview.w impl.w tests.w additional.w todo.w done.w \\ + test/pyweb\_test.w test/intro.w test/unit.w test/func.w test/combined.w + + .PHONY : test build + + # Note the bootstrapping new version from version 3.0 as baseline. + + test : $(SOURCE) + python3 pyweb-3.0.py -xw pyweb.w + cd test && python3 ../pyweb.py pyweb\_test.w + cd test && PYTHONPATH=.. python3 test.py + cd test && rst2html.py pyweb\_test.rst pyweb\_test.html + mypy --strict pyweb.py + + build : pyweb.py pyweb.html + + pyweb.py pyweb.html : $(SOURCE) + python3 pyweb-3.0.py pyweb.w + + +.. + + .. class:: small + + |loz| *Makefile (184)*. + + +**TODO:** Finish ``tox.ini`` or ``pyproject.toml``. + + +.. _`185`: +.. rubric:: pyproject.toml (185) = +.. parsed-literal:: + :class: code + + + [build-system] + requires = ["setuptools >= 61.2.0", "wheel >= 0.37.1"] + build-backend = "setuptools.build\_meta" + + [tool.tox] + legacy\_tox\_ini = """ + [tox] + envlist = py310 + + [testenv] + deps = + pytest >= 3.0.0, <4 + commands\_pre = + python3 pyweb-3.0.py pyweb.w + python3 pyweb.py -o test test/pyweb\_test.w + commands = + python3 test/test.py + mypy --strict pyweb.py + """ + +.. + + .. class:: small + + |loz| *pyproject.toml (185)*. + .. pyweb/jedit.w @@ -8487,8 +8728,8 @@ 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) = +.. _`186`: +.. rubric:: jedit/pyweb.xml (186) = .. parsed-literal:: :class: code @@ -8496,22 +8737,22 @@ We'll define the overall properties plus two sets of rules. - |srarr|\ props for JEdit mode (`181`_) - |srarr|\ rules for JEdit PyWeb and RST (`182`_) - |srarr|\ rules for JEdit PyWeb XML-Like Constructs (`183`_) + |srarr|\ props for JEdit mode (`187`_) + |srarr|\ rules for JEdit PyWeb and RST (`188`_) + |srarr|\ rules for JEdit PyWeb XML-Like Constructs (`189`_) .. .. class:: small - |loz| *jedit/pyweb.xml (180)*. + |loz| *jedit/pyweb.xml (186)*. Here are some properties to define RST constructs to JEdit -.. _`181`: -.. rubric:: props for JEdit mode (181) = +.. _`187`: +.. rubric:: props for JEdit mode (187) = .. parsed-literal:: :class: code @@ -8530,14 +8771,14 @@ Here are some properties to define RST constructs to JEdit .. class:: small - |loz| *props for JEdit mode (181)*. Used by: jedit/pyweb.xml (`180`_) + |loz| *props for JEdit mode (187)*. Used by: jedit/pyweb.xml (`186`_) Here are some rules to define PyWeb and RST constructs to JEdit. -.. _`182`: -.. rubric:: rules for JEdit PyWeb and RST (182) = +.. _`188`: +.. rubric:: rules for JEdit PyWeb and RST (188) = .. parsed-literal:: :class: code @@ -8678,15 +8919,15 @@ Here are some rules to define PyWeb and RST constructs to JEdit. .. class:: small - |loz| *rules for JEdit PyWeb and RST (182)*. Used by: jedit/pyweb.xml (`180`_) + |loz| *rules for JEdit PyWeb and RST (188)*. Used by: jedit/pyweb.xml (`186`_) 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) = +.. _`189`: +.. rubric:: rules for JEdit PyWeb XML-Like Constructs (189) = .. parsed-literal:: :class: code @@ -8702,7 +8943,7 @@ that look like XML. .. class:: small - |loz| *rules for JEdit PyWeb XML-Like Constructs (183)*. Used by: jedit/pyweb.xml (`180`_) + |loz| *rules for JEdit PyWeb XML-Like Constructs (189)*. Used by: jedit/pyweb.xml (`186`_) Additionally, you'll want to update the JEdit catalog. @@ -8723,21 +8964,35 @@ Additionally, you'll want to update the JEdit catalog. .. pyweb/todo.w +Python 3.10 Migration +===================== + + +1. [x] Add type hints. + +#. [ ] Replace all ``.format()`` with f-strings. + +#. [ ] Replace filename strings (and ``os.path``) with ``pathlib.Path``. + +#. [ ] ``pyproject.toml``. This requires -o dir to write output to a directory of choice; which requires Pathlib + +#. [ ] Introduce ``match`` statements for some of the ``elif`` blocks + +#. [ ] Replace mock class with ``unittest.mock.Mock`` objects. + + 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. +1. Silence the logging during testing. -#. Add a JSON-based configuration file to configure templates. +#. Add a JSON-based (or TOML) 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. + - An alternative is to get markup templates from some kind of "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 @@ -8746,12 +9001,15 @@ To Do #. 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 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. +#. Fix name definition order. There's no **good** reason why a full name should + be first and elided names defined later. + #. 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 @@ -8796,6 +9054,16 @@ The advantage of adding these two projects might be some simplification. Change Log =========== +Changes for 3.1 + +- Change to Python 3.10. + +- Add type hints, match statements, f-strings, pathlib. + +- Remove the Jedit configuration file as an output. + +- Add a makefile and tox.ini + Changes for 3.0 - Move to GitHub @@ -8934,27 +9202,33 @@ Files :.nojekyll: - |srarr|\ (`178`_) + |srarr|\ (`182`_) :MANIFEST.in: - |srarr|\ (`174`_) + |srarr|\ (`177`_) +:Makefile: + |srarr|\ (`184`_) :README: - |srarr|\ (`175`_) + |srarr|\ (`179`_) :docutils.conf: - |srarr|\ (`176`_) + |srarr|\ (`180`_) :index.html: - |srarr|\ (`179`_) + |srarr|\ (`183`_) :jedit/pyweb.xml: - |srarr|\ (`180`_) + |srarr|\ (`186`_) :page-layout.css: - |srarr|\ (`177`_) + |srarr|\ (`181`_) +:pyproject.toml: + |srarr|\ (`185`_) :pyweb.py: - |srarr|\ (`153`_) + |srarr|\ (`156`_) +:requirements-dev.txt: + |srarr|\ (`178`_) :setup.py: - |srarr|\ (`173`_) + |srarr|\ (`176`_) :tangle.py: - |srarr|\ (`168`_) + |srarr|\ (`171`_) :weave.py: - |srarr|\ (`169`_) + |srarr|\ (`172`_) @@ -8963,29 +9237,29 @@ Macros :Action call method actually does the real work: - |srarr|\ (`138`_) + |srarr|\ (`141`_) :Action class hierarchy - used to describe basic actions of the application: - |srarr|\ (`136`_) -:Action final summary of what was done: |srarr|\ (`139`_) +:Action final summary of what was done: + |srarr|\ (`142`_) :Action superclass has common features of all actions: - |srarr|\ (`137`_) + |srarr|\ (`140`_) :ActionSequence append adds a new action to the sequence: - |srarr|\ (`142`_) + |srarr|\ (`145`_) :ActionSequence call method delegates the sequence of ations: - |srarr|\ (`141`_) + |srarr|\ (`144`_) :ActionSequence subclass that holds a sequence of other actions: - |srarr|\ (`140`_) -:ActionSequence summary summarizes each step: |srarr|\ (`143`_) +:ActionSequence summary summarizes each step: + |srarr|\ (`146`_) :Application Class: - |srarr|\ (`159`_) |srarr|\ (`160`_) + |srarr|\ (`162`_) |srarr|\ (`163`_) :Application class process all files: - |srarr|\ (`163`_) + |srarr|\ (`166`_) :Application default options: - |srarr|\ (`161`_) + |srarr|\ (`164`_) :Application parse command line: - |srarr|\ (`162`_) + |srarr|\ (`165`_) :Base Class Definitions: |srarr|\ (`1`_) :Chunk add to the web: @@ -8999,29 +9273,29 @@ Macros :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 generate references from this Chunk: + |srarr|\ (`59`_) :Chunk indent adjustments: - |srarr|\ (`62`_) + |srarr|\ (`63`_) :Chunk references to this Chunk: - |srarr|\ (`59`_) + |srarr|\ (`60`_) :Chunk superclass make Content definition: |srarr|\ (`56`_) :Chunk tangle this Chunk into a code file: - |srarr|\ (`61`_) + |srarr|\ (`62`_) :Chunk weave this Chunk into the documentation: - |srarr|\ (`60`_) + |srarr|\ (`61`_) :CodeCommand class to contain a program source code block: - |srarr|\ (`81`_) + |srarr|\ (`82`_) :Command analysis features: starts-with and Regular Expression search: - |srarr|\ (`78`_) + |srarr|\ (`79`_) :Command class hierarchy - used to describe individual commands: - |srarr|\ (`76`_) -:Command superclass: |srarr|\ (`77`_) +:Command superclass: + |srarr|\ (`78`_) :Command tangle and weave functions: - |srarr|\ (`79`_) + |srarr|\ (`80`_) :Emitter class hierarchy - used to control output files: |srarr|\ (`2`_) :Emitter core open, close and write: @@ -9037,9 +9311,9 @@ Macros :Emitter write a block of code: |srarr|\ (`7`_) |srarr|\ (`8`_) |srarr|\ (`9`_) :Error class - defines the errors raised: - |srarr|\ (`94`_) + |srarr|\ (`95`_) :FileXrefCommand class for an output file cross-reference: - |srarr|\ (`83`_) + |srarr|\ (`84`_) :HTML code chunk begin: |srarr|\ (`33`_) :HTML code chunk end: @@ -9063,9 +9337,9 @@ Macros :HTML write user id cross reference line: |srarr|\ (`41`_) :Imports: - |srarr|\ (`11`_) |srarr|\ (`47`_) |srarr|\ (`96`_) |srarr|\ (`124`_) |srarr|\ (`131`_) |srarr|\ (`133`_) |srarr|\ (`154`_) |srarr|\ (`158`_) |srarr|\ (`164`_) + |srarr|\ (`11`_) |srarr|\ (`47`_) |srarr|\ (`57`_) |srarr|\ (`97`_) |srarr|\ (`125`_) |srarr|\ (`130`_) |srarr|\ (`133`_) |srarr|\ (`135`_) |srarr|\ (`157`_) |srarr|\ (`161`_) |srarr|\ (`167`_) :Interface Functions: - |srarr|\ (`167`_) + |srarr|\ (`170`_) :LaTeX code chunk begin: |srarr|\ (`24`_) :LaTeX code chunk end: @@ -9083,63 +9357,63 @@ Macros :LaTeX write a line of code: |srarr|\ (`29`_) :LoadAction call method loads the input files: - |srarr|\ (`151`_) + |srarr|\ (`154`_) :LoadAction subclass loads the document web: - |srarr|\ (`150`_) + |srarr|\ (`153`_) :LoadAction summary provides lines read: - |srarr|\ (`152`_) + |srarr|\ (`155`_) :Logging Setup: - |srarr|\ (`165`_) |srarr|\ (`166`_) + |srarr|\ (`168`_) |srarr|\ (`169`_) :MacroXrefCommand class for a named chunk cross-reference: - |srarr|\ (`84`_) + |srarr|\ (`85`_) :NamedChunk add to the web: - |srarr|\ (`65`_) + |srarr|\ (`66`_) :NamedChunk class: - |srarr|\ (`63`_) |srarr|\ (`68`_) + |srarr|\ (`64`_) |srarr|\ (`69`_) :NamedChunk tangle into the source file: - |srarr|\ (`67`_) + |srarr|\ (`68`_) :NamedChunk user identifiers set and get: - |srarr|\ (`64`_) + |srarr|\ (`65`_) :NamedChunk weave into the documentation: - |srarr|\ (`66`_) + |srarr|\ (`67`_) :NamedDocumentChunk class: - |srarr|\ (`73`_) + |srarr|\ (`74`_) :NamedDocumentChunk tangle: - |srarr|\ (`75`_) + |srarr|\ (`76`_) :NamedDocumentChunk weave: - |srarr|\ (`74`_) + |srarr|\ (`75`_) :Option Parser class - locates optional values on commands: - |srarr|\ (`134`_) |srarr|\ (`135`_) + |srarr|\ (`136`_) |srarr|\ (`137`_) |srarr|\ (`138`_) :OutputChunk add to the web: - |srarr|\ (`70`_) + |srarr|\ (`71`_) :OutputChunk class: - |srarr|\ (`69`_) + |srarr|\ (`70`_) :OutputChunk tangle: - |srarr|\ (`72`_) + |srarr|\ (`73`_) :OutputChunk weave: - |srarr|\ (`71`_) + |srarr|\ (`72`_) :Overheads: - |srarr|\ (`155`_) |srarr|\ (`156`_) |srarr|\ (`157`_) + |srarr|\ (`158`_) |srarr|\ (`159`_) |srarr|\ (`160`_) :RST subclass of Weaver: |srarr|\ (`22`_) :Reference class hierarchy - strategies for references to a chunk: - |srarr|\ (`91`_) |srarr|\ (`92`_) |srarr|\ (`93`_) + |srarr|\ (`92`_) |srarr|\ (`93`_) |srarr|\ (`94`_) :ReferenceCommand class for chunk references: - |srarr|\ (`86`_) + |srarr|\ (`87`_) :ReferenceCommand refers to a chunk: - |srarr|\ (`88`_) + |srarr|\ (`89`_) :ReferenceCommand resolve a referenced chunk name: - |srarr|\ (`87`_) + |srarr|\ (`88`_) :ReferenceCommand tangle a referenced chunk: - |srarr|\ (`90`_) + |srarr|\ (`91`_) :ReferenceCommand weave a reference to a chunk: - |srarr|\ (`89`_) + |srarr|\ (`90`_) :TangleAction call method does tangling of the output files: - |srarr|\ (`148`_) + |srarr|\ (`151`_) :TangleAction subclass initiates the tangle action: - |srarr|\ (`147`_) + |srarr|\ (`150`_) :TangleAction summary method provides total lines tangled: - |srarr|\ (`149`_) + |srarr|\ (`152`_) :Tangler code chunk begin: |srarr|\ (`45`_) :Tangler code chunk end: @@ -9155,17 +9429,17 @@ Macros :TanglerMake subclass which is make-sensitive: |srarr|\ (`48`_) :TextCommand class to contain a document text block: - |srarr|\ (`80`_) + |srarr|\ (`81`_) :Tokenizer class - breaks input into tokens: - |srarr|\ (`132`_) + |srarr|\ (`134`_) :UserIdXrefCommand class for a user identifier cross-reference: - |srarr|\ (`85`_) + |srarr|\ (`86`_) :WeaveAction call method to pick the language: - |srarr|\ (`145`_) + |srarr|\ (`148`_) :WeaveAction subclass initiates the weave action: - |srarr|\ (`144`_) + |srarr|\ (`147`_) :WeaveAction summary of language choice: - |srarr|\ (`146`_) + |srarr|\ (`149`_) :Weaver code chunk begin-end: |srarr|\ (`17`_) :Weaver cross reference output methods: @@ -9185,77 +9459,77 @@ Macros :Weaver subclass of Emitter to create documentation: |srarr|\ (`12`_) :Web Chunk check reference counts are all one: - |srarr|\ (`105`_) + |srarr|\ (`106`_) :Web Chunk cross reference methods: - |srarr|\ (`104`_) |srarr|\ (`106`_) |srarr|\ (`107`_) |srarr|\ (`108`_) + |srarr|\ (`105`_) |srarr|\ (`107`_) |srarr|\ (`108`_) |srarr|\ (`109`_) :Web Chunk name resolution methods: - |srarr|\ (`102`_) |srarr|\ (`103`_) + |srarr|\ (`103`_) |srarr|\ (`104`_) :Web add a named macro chunk: - |srarr|\ (`100`_) + |srarr|\ (`101`_) :Web add an anonymous chunk: - |srarr|\ (`99`_) + |srarr|\ (`100`_) :Web add an output file definition chunk: - |srarr|\ (`101`_) + |srarr|\ (`102`_) :Web add full chunk names, ignoring abbreviated names: - |srarr|\ (`98`_) + |srarr|\ (`99`_) :Web class - describes the overall "web" of chunks: - |srarr|\ (`95`_) + |srarr|\ (`96`_) :Web construction methods used by Chunks and WebReader: - |srarr|\ (`97`_) + |srarr|\ (`98`_) :Web determination of the language from the first chunk: - |srarr|\ (`111`_) -:Web tangle the output files: |srarr|\ (`112`_) -:Web weave the output document: +:Web tangle the output files: |srarr|\ (`113`_) -:WebReader class - parses the input file, building the Web structure: +:Web weave the output document: |srarr|\ (`114`_) +:WebReader class - parses the input file, building the Web structure: + |srarr|\ (`115`_) :WebReader command literals: - |srarr|\ (`130`_) + |srarr|\ (`132`_) :WebReader handle a command string: - |srarr|\ (`115`_) |srarr|\ (`127`_) + |srarr|\ (`116`_) |srarr|\ (`128`_) :WebReader load the web: - |srarr|\ (`129`_) + |srarr|\ (`131`_) :WebReader location in the input stream: - |srarr|\ (`128`_) + |srarr|\ (`129`_) :XrefCommand superclass for all cross-reference commands: - |srarr|\ (`82`_) + |srarr|\ (`83`_) :add a reference command to the current chunk: - |srarr|\ (`123`_) + |srarr|\ (`124`_) :add an expression command to the current chunk: - |srarr|\ (`125`_) + |srarr|\ (`126`_) :assign user identifiers to the current chunk: - |srarr|\ (`122`_) + |srarr|\ (`123`_) :collect all user identifiers from a given map into ux: - |srarr|\ (`109`_) + |srarr|\ (`110`_) :double at-sign replacement, append this character to previous TextCommand: - |srarr|\ (`126`_) + |srarr|\ (`127`_) :find user identifier usage and update ux from the given map: - |srarr|\ (`110`_) + |srarr|\ (`111`_) :finish a chunk, start a new Chunk adding it to the web: - |srarr|\ (`120`_) + |srarr|\ (`121`_) :import another file: - |srarr|\ (`119`_) + |srarr|\ (`120`_) :major commands segment the input into separate Chunks: - |srarr|\ (`116`_) + |srarr|\ (`117`_) :minor commands add Commands to the current Chunk: - |srarr|\ (`121`_) + |srarr|\ (`122`_) :props for JEdit mode: - |srarr|\ (`181`_) + |srarr|\ (`187`_) :rules for JEdit PyWeb XML-Like Constructs: - |srarr|\ (`183`_) + |srarr|\ (`189`_) :rules for JEdit PyWeb and RST: - |srarr|\ (`182`_) + |srarr|\ (`188`_) :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|\ (`174`_) :weave.py overheads for correct operation of a script: - |srarr|\ (`170`_) + |srarr|\ (`173`_) :weaver.py processing: load and weave the document: - |srarr|\ (`172`_) + |srarr|\ (`175`_) @@ -9264,239 +9538,239 @@ User Identifiers :Action: - [`137`_] `140`_ `144`_ `147`_ `150`_ + [`140`_] `143`_ `145`_ `147`_ `150`_ `153`_ :ActionSequence: - [`140`_] `161`_ + [`143`_] `164`_ :Application: - [`159`_] `167`_ + [`162`_] `170`_ :Chunk: - [`52`_] `58`_ `63`_ `90`_ `95`_ `104`_ `119`_ `120`_ `123`_ `129`_ + `15`_ `16`_ `17`_ `18`_ `45`_ `46`_ [`52`_] `58`_ `59`_ `64`_ `78`_ `87`_ `91`_ `92`_ `93`_ `94`_ `96`_ `100`_ `101`_ `102`_ `104`_ `105`_ `109`_ `115`_ `120`_ `121`_ `124`_ `131`_ :CodeCommand: - `63`_ [`81`_] + `64`_ [`82`_] :Command: - `53`_ [`77`_] `80`_ `82`_ `86`_ `163`_ + `52`_ `53`_ `56`_ `64`_ `74`_ [`78`_] `81`_ `83`_ `87`_ `166`_ :Emitter: - [`3`_] `12`_ `43`_ + [`3`_] `4`_ `12`_ `43`_ :Error: - `58`_ `61`_ `67`_ `75`_ `82`_ `90`_ [`94`_] `100`_ `102`_ `103`_ `113`_ `118`_ `119`_ `125`_ `135`_ `145`_ `148`_ `151`_ `162`_ + `59`_ `62`_ `68`_ `76`_ `83`_ `91`_ [`95`_] `101`_ `103`_ `104`_ `114`_ `119`_ `120`_ `126`_ `138`_ `148`_ `151`_ `154`_ `165`_ :FileXrefCommand: - [`83`_] `121`_ + [`84`_] `122`_ :HTML: - `31`_ [`32`_] `111`_ `160`_ `171`_ + `31`_ [`32`_] `112`_ `163`_ `174`_ :LaTeX: - [`23`_] `111`_ `160`_ + [`23`_] `112`_ `163`_ :LoadAction: - [`150`_] `161`_ `168`_ `172`_ + [`153`_] `164`_ `171`_ `175`_ :MacroXrefCommand: - [`84`_] `121`_ + [`85`_] `122`_ :NamedChunk: - [`63`_] `68`_ `69`_ `73`_ `118`_ + `58`_ [`64`_] `69`_ `70`_ `74`_ `119`_ :NamedDocumentChunk: - [`73`_] `118`_ + [`74`_] `119`_ :OutputChunk: - [`69`_] `117`_ + [`70`_] `118`_ :ReferenceCommand: - [`86`_] `123`_ + [`87`_] `124`_ :TangleAction: - [`147`_] `161`_ `168`_ + [`150`_] `164`_ `171`_ :Tangler: - `3`_ [`43`_] `48`_ `162`_ + `3`_ [`43`_] `48`_ `62`_ `63`_ `68`_ `69`_ `73`_ `76`_ `80`_ `81`_ `82`_ `83`_ `91`_ `113`_ `165`_ :TanglerMake: - [`48`_] `162`_ `166`_ `168`_ `172`_ + [`48`_] `165`_ `169`_ `171`_ `175`_ :TextCommand: - `54`_ `56`_ `67`_ `73`_ [`80`_] `81`_ + `54`_ `56`_ `68`_ `74`_ [`81`_] `82`_ :Tokenizer: - `129`_ [`132`_] + `115`_ `131`_ [`134`_] :UserIdXrefCommand: - [`85`_] `121`_ + [`86`_] `122`_ :WeaveAction: - [`144`_] `161`_ `172`_ + [`147`_] `164`_ `175`_ :Weaver: - [`12`_] `22`_ `23`_ `31`_ `162`_ `163`_ + [`12`_] `22`_ `23`_ `31`_ `60`_ `61`_ `67`_ `72`_ `75`_ `80`_ `81`_ `82`_ `83`_ `84`_ `85`_ `86`_ `90`_ `112`_ `114`_ `165`_ `166`_ :Web: - `45`_ `55`_ `65`_ `70`_ [`95`_] `151`_ `163`_ `168`_ `172`_ `175`_ + `45`_ `52`_ `55`_ `59`_ `61`_ `62`_ `63`_ `66`_ `67`_ `68`_ `69`_ `71`_ `72`_ `73`_ `75`_ `76`_ `80`_ `81`_ `82`_ `83`_ `84`_ `85`_ `86`_ `88`_ `89`_ `90`_ `91`_ [`96`_] `115`_ `131`_ `140`_ `154`_ `166`_ `171`_ `175`_ `179`_ :WebReader: - [`114`_] `119`_ `162`_ `166`_ `168`_ `172`_ + [`115`_] `120`_ `131`_ `165`_ `169`_ `171`_ `175`_ :XrefCommand: - [`82`_] `83`_ `84`_ `85`_ + [`83`_] `84`_ `85`_ `86`_ :__version__: - `125`_ [`157`_] + `126`_ [`160`_] :_gatherUserId: - [`108`_] + [`109`_] :_updateUserId: - [`108`_] + [`109`_] :add: - `55`_ [`99`_] + `55`_ [`100`_] :addDefName: - [`98`_] `100`_ `123`_ + [`99`_] `101`_ `124`_ :addIndent: - `10`_ [`13`_] `62`_ `66`_ + `10`_ [`13`_] `63`_ `67`_ :addNamed: - `65`_ [`100`_] + `66`_ [`101`_] :addOutput: - `70`_ [`101`_] + `71`_ [`102`_] :append: - `10`_ `13`_ `53`_ `54`_ `93`_ `99`_ `100`_ `101`_ `104`_ `110`_ `121`_ `123`_ `135`_ [`142`_] + `10`_ `13`_ `53`_ `54`_ `94`_ `100`_ `101`_ `102`_ `105`_ `111`_ `122`_ `124`_ `138`_ [`145`_] :appendText: - [`54`_] `123`_ `125`_ `126`_ `129`_ + [`54`_] `124`_ `126`_ `127`_ `131`_ :argparse: - [`158`_] `161`_ `162`_ `168`_ `170`_ `172`_ + `140`_ [`161`_] `164`_ `165`_ `166`_ `171`_ `173`_ `175`_ :builtins: - [`124`_] `125`_ + [`125`_] `126`_ :chunkXref: - `84`_ [`107`_] + `85`_ [`108`_] :close: [`4`_] `13`_ `44`_ `50`_ :clrIndent: - [`10`_] `62`_ `66`_ `68`_ + [`10`_] `63`_ `67`_ `69`_ :codeBegin: - `17`_ [`45`_] `66`_ `67`_ + `17`_ [`45`_] `67`_ `68`_ :codeBlock: - [`7`_] `66`_ `81`_ + [`7`_] `67`_ `82`_ :codeEnd: - `17`_ [`46`_] `66`_ `67`_ + `17`_ [`46`_] `67`_ `68`_ :codeFinish: `4`_ `9`_ [`13`_] :createUsedBy: - [`104`_] `151`_ + [`105`_] `154`_ :datetime: - `125`_ [`154`_] + `126`_ [`157`_] :doClose: `4`_ `6`_ `13`_ `44`_ [`50`_] :doOpen: `4`_ `5`_ `13`_ `44`_ [`49`_] :docBegin: - [`15`_] `60`_ + [`15`_] `61`_ :docEnd: - [`15`_] `60`_ + [`15`_] `61`_ :duration: - [`139`_] `146`_ `149`_ `152`_ + [`142`_] `149`_ `152`_ `155`_ :expand: - `74`_ `123`_ `161`_ [`162`_] + `75`_ `124`_ `164`_ [`165`_] :expect: - `117`_ `118`_ `123`_ `125`_ [`127`_] + `118`_ `119`_ `124`_ `126`_ [`128`_] :fileBegin: - `18`_ [`35`_] `71`_ + `18`_ [`35`_] `72`_ :fileEnd: - `18`_ [`36`_] `71`_ + `18`_ [`36`_] `72`_ :fileXref: - `83`_ [`107`_] + `84`_ [`108`_] :filecmp: [`47`_] `50`_ :formatXref: - [`82`_] `83`_ `84`_ + [`83`_] `84`_ `85`_ :fullNameFor: - `66`_ `71`_ `87`_ `98`_ [`102`_] `103`_ `104`_ + `67`_ `72`_ `88`_ `99`_ [`103`_] `104`_ `105`_ :genReferences: - [`58`_] `104`_ + [`59`_] `105`_ :getUserIDRefs: - `57`_ [`64`_] `109`_ + `58`_ [`65`_] `110`_ :getchunk: - `87`_ [`103`_] `104`_ `113`_ + `88`_ [`104`_] `105`_ `114`_ :handleCommand: - [`115`_] `129`_ + [`116`_] `131`_ :language: - [`111`_] `145`_ `156`_ `175`_ + [`112`_] `148`_ `159`_ `179`_ :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`_ + `17`_ `18`_ `33`_ `35`_ `45`_ `54`_ `56`_ [`58`_] `64`_ `68`_ `74`_ `78`_ `81`_ `83`_ `87`_ `120`_ `122`_ `124`_ `126`_ `127`_ `129`_ `131`_ `134`_ `174`_ :load: - `119`_ [`129`_] `151`_ `161`_ `163`_ + `120`_ [`131`_] `154`_ `164`_ `166`_ :location: - `115`_ `122`_ `125`_ `127`_ [`128`_] + `116`_ `123`_ `126`_ `128`_ [`129`_] :logging: - `3`_ `77`_ `91`_ `95`_ `114`_ `137`_ `159`_ `161`_ `162`_ `163`_ [`164`_] `165`_ `166`_ `168`_ `170`_ `172`_ + `3`_ `52`_ `78`_ `92`_ `96`_ `115`_ `140`_ `162`_ `164`_ `165`_ `166`_ [`167`_] `168`_ `169`_ `171`_ `173`_ `175`_ :logging.config: - [`164`_] `165`_ + [`167`_] `168`_ :main: - [`167`_] + [`170`_] :makeContent: - `54`_ [`56`_] `63`_ `73`_ + `54`_ [`56`_] `64`_ `74`_ :multi_reference: - `105`_ [`106`_] + `106`_ [`107`_] :no_definition: - `105`_ [`106`_] + `106`_ [`107`_] :no_reference: - `105`_ [`106`_] + `106`_ [`107`_] :open: - [`4`_] `13`_ `44`_ `112`_ `113`_ `125`_ `129`_ + [`4`_] `13`_ `44`_ `113`_ `114`_ `126`_ `131`_ :os: - `44`_ `49`_ `50`_ `113`_ `125`_ [`154`_] + `44`_ `49`_ `50`_ `114`_ `126`_ [`157`_] :parse: - `117`_ `118`_ [`129`_] `135`_ + `118`_ `119`_ [`131`_] `138`_ :parseArgs: - [`162`_] `167`_ + [`165`_] `170`_ :perform: - [`151`_] + [`154`_] :platform: - [`124`_] `125`_ + [`125`_] `126`_ :process: - `125`_ [`163`_] `167`_ + `126`_ [`166`_] `170`_ :quote: - [`8`_] `81`_ + [`8`_] `82`_ :quoted_chars: `8`_ `14`_ `29`_ [`38`_] :re: - `110`_ [`131`_] `132`_ `175`_ + `111`_ [`133`_] `134`_ `179`_ :readdIndent: `3`_ [`10`_] `13`_ :ref: - `28`_ `58`_ [`79`_] `88`_ `99`_ `100`_ `101`_ + `28`_ `59`_ [`80`_] `89`_ `100`_ `101`_ `102`_ :referenceSep: - [`19`_] `113`_ + [`19`_] `114`_ :referenceTo: - `19`_ `20`_ [`39`_] `66`_ + `19`_ `20`_ [`39`_] `67`_ :references: - `16`_ `17`_ `18`_ `19`_ `25`_ `32`_ `34`_ `36`_ [`42`_] `52`_ `58`_ `105`_ `122`_ `156`_ `161`_ `171`_ + `16`_ `17`_ `18`_ `19`_ `25`_ `32`_ `34`_ `36`_ [`42`_] `52`_ `59`_ `60`_ `106`_ `123`_ `159`_ `164`_ `174`_ :resolve: - `67`_ [`87`_] `88`_ `89`_ `90`_ `103`_ + `68`_ [`88`_] `89`_ `90`_ `91`_ `104`_ :searchForRE: - `57`_ [`78`_] `80`_ `110`_ + `58`_ [`79`_] `81`_ `111`_ :setUserIDRefs: - [`64`_] `122`_ + `58`_ [`65`_] `123`_ :shlex: - [`133`_] `135`_ + [`135`_] `138`_ :startswith: - `57`_ [`78`_] `80`_ `102`_ `111`_ `129`_ `135`_ `163`_ + `58`_ [`79`_] `81`_ `103`_ `112`_ `131`_ `138`_ `166`_ :string: - [`11`_] `16`_ `17`_ `18`_ `19`_ `20`_ `21`_ `24`_ `25`_ `28`_ `30`_ `33`_ `34`_ `35`_ `36`_ `37`_ `39`_ `40`_ `41`_ `42`_ `170`_ `171`_ + [`11`_] `16`_ `17`_ `18`_ `19`_ `20`_ `21`_ `24`_ `25`_ `28`_ `30`_ `33`_ `34`_ `35`_ `36`_ `37`_ `39`_ `40`_ `41`_ `42`_ `173`_ `174`_ :summary: - `139`_ `143`_ `146`_ `149`_ [`152`_] `163`_ `168`_ `172`_ + `142`_ `146`_ `149`_ `152`_ [`155`_] `166`_ `171`_ `175`_ :sys: - [`124`_] `125`_ `166`_ + [`125`_] `126`_ `169`_ `170`_ :tangle: - `45`_ `61`_ `67`_ `69`_ `72`_ `73`_ `75`_ `79`_ `80`_ `81`_ `82`_ `90`_ [`112`_] `148`_ `156`_ `161`_ `168`_ `175`_ + `45`_ `62`_ `68`_ `70`_ `73`_ `74`_ `76`_ `80`_ `81`_ `82`_ `83`_ `91`_ [`113`_] `151`_ `164`_ `171`_ `179`_ :tempfile: [`47`_] `49`_ :time: - `138`_ `139`_ [`154`_] + `126`_ `141`_ `142`_ [`157`_] :types: - `12`_ `125`_ [`154`_] + `12`_ `126`_ [`157`_] :usedBy: - [`88`_] + [`89`_] :userNamesXref: - `85`_ [`108`_] + `86`_ [`109`_] :weakref: - [`96`_] `99`_ `100`_ `101`_ + `52`_ [`97`_] `100`_ `101`_ `102`_ :weave: - `60`_ `66`_ `71`_ `74`_ `79`_ `80`_ `81`_ `83`_ `84`_ `85`_ `89`_ [`113`_] `145`_ `156`_ `161`_ `170`_ `175`_ + `61`_ `67`_ `72`_ `75`_ `80`_ `81`_ `82`_ `84`_ `85`_ `86`_ `90`_ [`114`_] `148`_ `164`_ `173`_ `179`_ :weaveChunk: - `89`_ [`113`_] + `90`_ [`114`_] :weaveReferenceTo: - `60`_ `66`_ [`74`_] `113`_ + `61`_ `67`_ [`75`_] `114`_ :weaveShortReferenceTo: - `60`_ `66`_ [`74`_] `113`_ + `61`_ `67`_ [`75`_] `114`_ :webAdd: - `55`_ `65`_ [`70`_] `117`_ `118`_ `119`_ `120`_ `129`_ + `55`_ `66`_ [`71`_] `118`_ `119`_ `120`_ `121`_ `131`_ :write: - [`4`_] `7`_ `9`_ `17`_ `18`_ `20`_ `21`_ `45`_ `80`_ `113`_ + [`4`_] `7`_ `9`_ `17`_ `18`_ `20`_ `21`_ `45`_ `81`_ `114`_ :xrefDefLine: - `21`_ [`41`_] `85`_ + `21`_ [`41`_] `86`_ :xrefFoot: - `20`_ [`40`_] `82`_ `85`_ + `20`_ [`40`_] `83`_ `86`_ :xrefHead: - `20`_ [`40`_] `82`_ `85`_ + `20`_ [`40`_] `83`_ `86`_ :xrefLine: - `20`_ [`40`_] `82`_ + `20`_ [`40`_] `83`_ @@ -9505,11 +9779,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 pyweb-3.0.py at Fri Jun 10 08:31:18 2022. - Source pyweb.w modified Sat Jun 16 08:10:37 2018. + Source pyweb.w modified Wed Jun 8 14:04:44 2022. pyweb.__version__ '3.0'. - Working directory '/Users/slott/Documents/Projects/PyWebTool-3/pyweb'. - + Working directory '/Users/slott/Documents/Projects/py-web-tool'. diff --git a/pyweb.w b/pyweb.w index 31686b5..5fb4640 100755 --- a/pyweb.w +++ b/pyweb.w @@ -1,5 +1,5 @@ ############################## -pyWeb Literate Programming 3.0 +pyWeb Literate Programming 3.1 ############################## ================================================= @@ -59,4 +59,3 @@ User Identifiers pyweb.__version__ '@(__version__@)'. Working directory '@(os.path.realpath('.')@)'. - diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..88f7bc9 --- /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..2fb0505 100644 --- a/setup.py +++ b/setup.py @@ -3,9 +3,9 @@ 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='pyWeb 3.1: Yet Another Literate Programming Tool', author='S. Lott', author_email='s_lott@yahoo.com', url='http://slott-softwarearchitect.blogspot.com/', diff --git a/tangle.py b/tangle.py index eccebc8..3841c54 100644 --- a/tangle.py +++ b/tangle.py @@ -4,25 +4,25 @@ import logging import argparse -with pyweb.Logger( pyweb.log_config ): - logger= logging.getLogger(__file__) +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(), + 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() + w = pyweb.Web() for action in LoadAction(), TangleAction(): - action.web= w - action.options= options + action.web = w + action.options = options action() - logger.info( action.summary() ) + logger.info(action.summary()) diff --git a/test/combined.rst b/test/combined.rst new file mode 100644 index 0000000..5037227 --- /dev/null +++ b/test/combined.rst @@ -0,0 +1,3081 @@ +############################################ +pyWeb Literate Programming 2.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:pyweb slott$ cd test + MacBookPro-SLott:test slott$ python ../pyweb.py 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. + 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$ + + +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( 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 ): + +- 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. + + +.. _`1`: +.. rubric:: test_unit.py (1) = +.. parsed-literal:: + :class: code + + |srarr|\ Unit Test overheads: imports, etc. (`45`_) + |srarr|\ Unit Test of Emitter class hierarchy (`2`_) + |srarr|\ Unit Test of Chunk class hierarchy (`11`_) + |srarr|\ Unit Test of Command class hierarchy (`22`_) + |srarr|\ Unit Test of Reference class hierarchy (`31`_) + |srarr|\ Unit Test of Web class (`32`_) + |srarr|\ Unit Test of WebReader class (`38`_) + |srarr|\ Unit Test of Action class hierarchy (`39`_) + |srarr|\ Unit Test of Application class (`44`_) + |srarr|\ Unit Test main (`46`_) + +.. + + .. 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, fileName ): + self.theFile= io.StringIO() + def doClose( self ): + self.theFile.flush() + + 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.theFile.getvalue() ) + def test\_emitter\_should\_codeBlock( self ): + self.emitter.open( "test.tmp" ) + self.emitter.codeBlock( "Some" ) + self.emitter.codeBlock( " Code" ) + self.emitter.close() + self.assertEquals( "Some Code\\n", self.emitter.theFile.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.theFile.getvalue() ) + +.. + + .. class:: small + + |loz| *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. + + +.. _`4`: +.. rubric:: Unit Test Mock Chunk class (4) = +.. parsed-literal:: + :class: code + + + 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:: small + + |loz| *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. + + +.. _`5`: +.. rubric:: Unit Test of Weaver subclass of Emitter (5) = +.. parsed-literal:: + :class: code + + + 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: + pass #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( "Container (\`123\`\_)", result ) + result= self.weaver.referenceTo( "Chunk", 314 ) + self.assertEquals( r"\|srarr\|\\ Chunk (\`314\`\_)", result ) + + def test\_weaver\_should\_codeBegin( self ): + self.weaver.open( self.filename ) + self.weaver.setIndent() + 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() + with open( "testweaver.rst", "r" ) as result: + txt= result.read() + self.assertEquals( "\\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: Container (\`123\`\_)\\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 :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 ): + self.weaver.open( self.filename ) + self.weaver.xrefHead( ) + self.weaver.xrefLine( "Chunk", [ ("Container", 123) ] ) + self.weaver.xrefFoot( ) + #self.weaver.fileEnd( self.aFileChunk ) # Why? + self.weaver.close() + with open( "testweaver.rst", "r" ) as result: + txt= result.read() + self.assertEquals( "\\n:Chunk:\\n \|srarr\|\\\\ (\`('Container', 123)\`\_)\\n\\n", txt ) + + def test\_weaver\_should\_xref\_def( self ): + self.weaver.open( self.filename ) + 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() + with open( "testweaver.rst", "r" ) as result: + txt= result.read() + self.assertEquals( "\\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`_); test_unit.py (`1`_) + + +Note that the XREF data structure seems to have changed without appropriate +unit test support. During version 2.3 (6 Mar 2014) development, this +unit test seemed to have failed. + +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 ): + 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:: small + + |loz| *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. + + +.. _`7`: +.. rubric:: Unit Test of HTML subclass of Emitter (7) = +.. parsed-literal:: + :class: code + + + 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:: small + + |loz| *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 ``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`_); test_unit.py (`1`_) + + +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 ): + 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:: small + + |loz| *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. + + + + +.. _`10`: +.. rubric:: Unit Test of TanglerMake subclass of Emitter (10) = +.. parsed-literal:: + :class: code + + + 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:: small + + |loz| *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. + + + +.. _`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 OutputChunk subclass (`20`_) + |srarr|\ Unit Test of NamedDocumentChunk subclass (`21`_) + +.. + + .. 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 + + + class MockCommand( object ): + def \_\_init\_\_( self ): + self.lineNumber= 314 + def startswith( self, text ): + return False + +.. + + .. class:: small + + |loz| *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. + + +.. _`13`: +.. rubric:: Unit Test of Chunk superclass (13) += +.. parsed-literal:: + :class: code + + + 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:: small + + |loz| *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. + + +.. _`14`: +.. rubric:: Unit Test of Chunk superclass (14) += +.. parsed-literal:: + :class: code + + + 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:: small + + |loz| *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. + + +.. _`15`: +.. rubric:: Unit Test of Chunk superclass (15) += +.. parsed-literal:: + :class: code + + + class TestChunk( unittest.TestCase ): + def setUp( self ): + 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`_); test_unit.py (`1`_) + + +Can we build a Chunk? + + +.. _`16`: +.. rubric:: Unit Test of Chunk construction (16) = +.. parsed-literal:: + :class: code + + + 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) ) + +.. + + .. class:: small + + |loz| *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? + + +.. _`17`: +.. rubric:: Unit Test of Chunk interrogation (17) = +.. parsed-literal:: + :class: code + + + 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 ) + +.. + + .. class:: small + + |loz| *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? + + +.. _`18`: +.. rubric:: Unit Test of Chunk emission (18) = +.. parsed-literal:: + :class: code + + + 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 as e: + self.assertEquals( "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`_); 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. + + +.. _`19`: +.. rubric:: Unit Test of NamedChunk subclass (19) = +.. parsed-literal:: + :class: code + + + 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:: small + + |loz| *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. + + +.. _`20`: +.. rubric:: Unit Test of OutputChunk subclass (20) = +.. parsed-literal:: + :class: code + + + 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:: small + + |loz| *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. + + **TODO** Test ``NamedDocumentChunk``. + + +.. _`21`: +.. rubric:: Unit Test of NamedDocumentChunk subclass (21) = +.. parsed-literal:: + :class: code + + # TODO Test This +.. + + .. class:: small + + |loz| *Unit Test of NamedDocumentChunk subclass (21)*. Used by: Unit Test of Chunk class hierarchy (`11`_); test_unit.py (`1`_) + + +Command Tests +--------------- + + +.. _`22`: +.. rubric:: Unit Test of Command class hierarchy (22) = +.. parsed-literal:: + :class: code + + + |srarr|\ Unit Test of Command superclass (`23`_) + |srarr|\ Unit Test of TextCommand class to contain a document text block (`24`_) + |srarr|\ Unit Test of CodeCommand class to contain a program source code block (`25`_) + |srarr|\ Unit Test of XrefCommand superclass for all cross-reference commands (`26`_) + |srarr|\ Unit Test of FileXrefCommand class for an output file cross-reference (`27`_) + |srarr|\ Unit Test of MacroXrefCommand class for a named chunk cross-reference (`28`_) + |srarr|\ Unit Test of UserIdXrefCommand class for a user identifier cross-reference (`29`_) + |srarr|\ Unit Test of ReferenceCommand class for chunk references (`30`_) + +.. + + .. class:: small + + |loz| *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. + + +.. _`23`: +.. rubric:: Unit Test of Command superclass (23) = +.. parsed-literal:: + :class: code + + # No Tests +.. + + .. class:: small + + |loz| *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. + + +.. _`24`: +.. rubric:: Unit Test of TextCommand class to contain a document text block (24) = +.. parsed-literal:: + :class: code + + + 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:: small + + |loz| *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. + + +.. _`25`: +.. rubric:: Unit Test of CodeCommand class to contain a program source code block (25) = +.. parsed-literal:: + :class: code + + + 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:: small + + |loz| *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. + + +.. _`26`: +.. rubric:: Unit Test of XrefCommand superclass for all cross-reference commands (26) = +.. parsed-literal:: + :class: code + + # No Tests +.. + + .. class:: small + + |loz| *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 ``@o`` +locations. + + +.. _`27`: +.. rubric:: Unit Test of FileXrefCommand class for an output file cross-reference (27) = +.. parsed-literal:: + :class: code + + + 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:: small + + |loz| *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. + + +.. _`28`: +.. rubric:: Unit Test of MacroXrefCommand class for a named chunk cross-reference (28) = +.. parsed-literal:: + :class: code + + + 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:: small + + |loz| *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. + + +.. _`29`: +.. rubric:: Unit Test of UserIdXrefCommand class for a user identifier cross-reference (29) = +.. parsed-literal:: + :class: code + + + 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:: small + + |loz| *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. + + +.. _`30`: +.. rubric:: Unit Test of ReferenceCommand class for chunk references (30) = +.. parsed-literal:: + :class: code + + + 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:: small + + |loz| *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. + + + +.. _`31`: +.. rubric:: Unit Test of Reference class hierarchy (31) = +.. parsed-literal:: + :class: code + + + 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:: small + + |loz| *Unit Test of Reference class hierarchy (31)*. Used by: test_unit.py (`1`_) + + +Web Tests +----------- + +This is more difficult to create mocks for. + + +.. _`32`: +.. rubric:: Unit Test of Web class (32) = +.. parsed-literal:: + :class: code + + + class TestWebConstruction( unittest.TestCase ): + def setUp( self ): + self.web= pyweb.Web() + |srarr|\ Unit Test Web class construction methods (`33`_) + + class TestWebProcessing( unittest.TestCase ): + def setUp( self ): + self.web= pyweb.Web() + 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 (`34`_) + |srarr|\ Unit Test Web class chunk cross-reference (`35`_) + |srarr|\ Unit Test Web class tangle (`36`_) + |srarr|\ Unit Test Web class weave (`37`_) + +.. + + .. class:: small + + |loz| *Unit Test of Web class (32)*. Used by: test_unit.py (`1`_) + + + +.. _`33`: +.. rubric:: Unit Test Web class construction methods (33) = +.. parsed-literal:: + :class: code + + + 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:: small + + |loz| *Unit Test Web class construction methods (33)*. Used by: Unit Test of Web class (`32`_); test_unit.py (`1`_) + + + +.. _`34`: +.. rubric:: Unit Test Web class name resolution methods (34) = +.. parsed-literal:: + :class: code + + + 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 as e: + self.assertTrue( e.args[0].startswith("Cannot resolve 'A File'") ) + +.. + + .. class:: small + + |loz| *Unit Test Web class name resolution methods (34)*. Used by: Unit Test of Web class (`32`_); test_unit.py (`1`_) + + + +.. _`35`: +.. rubric:: Unit Test Web class chunk cross-reference (35) = +.. parsed-literal:: + :class: code + + + 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 ) + +.. + + .. class:: small + + |loz| *Unit Test Web class chunk cross-reference (35)*. Used by: Unit Test of Web class (`32`_); test_unit.py (`1`_) + + + +.. _`36`: +.. rubric:: Unit Test Web class tangle (36) = +.. parsed-literal:: + :class: code + + + 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 ) + +.. + + .. class:: small + + |loz| *Unit Test Web class tangle (36)*. Used by: Unit Test of Web class (`32`_); test_unit.py (`1`_) + + + +.. _`37`: +.. rubric:: Unit Test Web class weave (37) = +.. parsed-literal:: + :class: code + + + 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:: small + + |loz| *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. + + +.. _`38`: +.. rubric:: Unit Test of WebReader class (38) = +.. parsed-literal:: + :class: code + + # Tested via functional tests +.. + + .. class:: small + + |loz| *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. + + +.. _`39`: +.. rubric:: Unit Test of Action class hierarchy (39) = +.. parsed-literal:: + :class: code + + + |srarr|\ Unit test of Action Sequence class (`40`_) + |srarr|\ Unit test of LoadAction class (`43`_) + |srarr|\ Unit test of TangleAction class (`42`_) + |srarr|\ Unit test of WeaverAction class (`41`_) + +.. + + .. class:: small + + |loz| *Unit Test of Action class hierarchy (39)*. Used by: test_unit.py (`1`_) + + + +.. _`40`: +.. rubric:: Unit test of Action Sequence class (40) = +.. parsed-literal:: + :class: code + + + 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 source( self, filename, file ): + self.webFileName= filename + 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 + self.action.options= argparse.Namespace() + 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:: small + + |loz| *Unit test of Action Sequence class (40)*. Used by: Unit Test of Action class hierarchy (`39`_); test_unit.py (`1`_) + + + +.. _`41`: +.. rubric:: Unit test of WeaverAction class (41) = +.. parsed-literal:: + :class: code + + + class TestWeaveAction( unittest.TestCase ): + def setUp( self ): + self.web= MockWeb() + self.action= pyweb.WeaveAction( ) + self.weaver= MockWeaver() + self.action.web= self.web + self.action.options= argparse.Namespace( theWeaver=self.weaver ) + def test\_should\_execute\_weaving( self ): + self.action() + self.assertTrue( self.web.wove is self.weaver ) + +.. + + .. class:: small + + |loz| *Unit test of WeaverAction class (41)*. Used by: Unit Test of Action class hierarchy (`39`_); test_unit.py (`1`_) + + + +.. _`42`: +.. rubric:: Unit test of TangleAction class (42) = +.. parsed-literal:: + :class: code + + + class TestTangleAction( unittest.TestCase ): + def setUp( self ): + self.web= MockWeb() + self.action= pyweb.TangleAction( ) + self.tangler= MockTangler() + self.action.web= self.web + self.action.options= argparse.Namespace( + theTangler= self.tangler ) + def test\_should\_execute\_tangling( self ): + self.action() + self.assertTrue( self.web.tangled is self.tangler ) + +.. + + .. class:: small + + |loz| *Unit test of TangleAction class (42)*. Used by: Unit Test of Action class hierarchy (`39`_); test_unit.py (`1`_) + + + +.. _`43`: +.. rubric:: Unit test of LoadAction class (43) = +.. parsed-literal:: + :class: code + + + class TestLoadAction( unittest.TestCase ): + def setUp( self ): + self.web= MockWeb() + self.action= pyweb.LoadAction( ) + self.webReader= MockWebReader() + self.action.webReader= self.webReader + self.action.web= self.web + self.action.options= argparse.Namespace( webReader= self.webReader, webFileName="TestLoadAction.w" ) + with open("TestLoadAction.w","w") as web: + pass + def tearDown( self ): + try: + os.remove("TestLoadAction.w") + except IOError: + pass + def test\_should\_execute\_loading( self ): + self.action() + self.assertEquals( 1, self.webReader.count ) + +.. + + .. class:: small + + |loz| *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. + + +.. _`44`: +.. rubric:: Unit Test of Application class (44) = +.. parsed-literal:: + :class: code + + # TODO Test Application class +.. + + .. class:: small + + |loz| *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. + + +.. _`45`: +.. rubric:: Unit Test overheads: imports, etc. (45) = +.. parsed-literal:: + :class: code + + from \_\_future\_\_ import print\_function + """Unit tests.""" + import pyweb + import unittest + import logging + import io + import string + import os + import time + import re + import argparse + +.. + + .. class:: small + + |loz| *Unit Test overheads: imports, etc. (45)*. Used by: test_unit.py (`1`_) + + + +.. _`46`: +.. rubric:: Unit Test main (46) = +.. parsed-literal:: + :class: code + + + if \_\_name\_\_ == "\_\_main\_\_": + import sys + logging.basicConfig( stream=sys.stdout, level= logging.WARN ) + unittest.main() + +.. + + .. class:: small + + |loz| *Unit Test main (46)*. 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. + + +.. _`47`: +.. rubric:: test_loader.py (47) = +.. parsed-literal:: + :class: code + + |srarr|\ Load Test overheads: imports, etc. (`53`_) + |srarr|\ Load Test superclass to refactor common setup (`48`_) + |srarr|\ Load Test error handling with a few common syntax errors (`49`_) + |srarr|\ Load Test include processing with syntax errors (`51`_) + |srarr|\ Load Test main program (`54`_) + +.. + + .. class:: small + + |loz| *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. + + +.. _`48`: +.. rubric:: Load Test superclass to refactor common setup (48) = +.. parsed-literal:: + :class: code + + + class ParseTestcase( unittest.TestCase ): + text= "" + file\_name= "" + def setUp( self ): + source= io.StringIO( self.text ) + self.web= pyweb.Web() + self.rdr= pyweb.WebReader() + self.rdr.source( self.file\_name, source ).web( self.web ) + +.. + + .. class:: small + + |loz| *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. + + +.. _`49`: +.. rubric:: Load Test error handling with a few common syntax errors (49) = +.. parsed-literal:: + :class: code + + + |srarr|\ 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 as e: + self.assertEquals( "At ('test1.w', 8): expected ('@{',), found '@o'", e.args[0] ) + +.. + + .. class:: small + + |loz| *Load Test error handling with a few common syntax errors (49)*. Used by: test_loader.py (`47`_) + + + +.. _`50`: +.. rubric:: Sample Document 1 with correct and incorrect syntax (50) = +.. 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 (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. + + +.. _`51`: +.. rubric:: Load Test include processing with syntax errors (51) = +.. parsed-literal:: + :class: code + + + |srarr|\ 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 as e: + self.assertEquals( "At ('test8\_inc.tmp', 4): end of input, ('@{', '@[') not found", e.args[0] ) + def tearDown( self ): + os.remove( 'test8\_inc.tmp' ) + super( Test\_IncludeParseErrors, self ).tearDown() + +.. + + .. class:: small + + |loz| *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``. + + +.. _`52`: +.. rubric:: Sample Document 8 and the file it includes (52) = +.. 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 (52)*. Used by: Load Test include processing with syntax errors (`51`_); test_loader.py (`47`_) + + +

The overheads for a Python unittest.

+ + +.. _`53`: +.. rubric:: Load Test overheads: imports, etc. (53) = +.. parsed-literal:: + :class: code + + from \_\_future\_\_ import print\_function + """Loader and parsing tests.""" + import pyweb + import unittest + import logging + import os + import io + +.. + + .. class:: small + + |loz| *Load Test overheads: imports, etc. (53)*. Used by: test_loader.py (`47`_) + + +A main program that configures logging and then runs the test. + + +.. _`54`: +.. rubric:: Load Test main program (54) = +.. parsed-literal:: + :class: code + + + if \_\_name\_\_ == "\_\_main\_\_": + import sys + logging.basicConfig( stream=sys.stdout, level= logging.WARN ) + unittest.main() + +.. + + .. class:: small + + |loz| *Load Test main program (54)*. Used by: test_loader.py (`47`_) + + +Tests for Tangling +------------------ + +We need to be able to tangle a web. + + +.. _`55`: +.. rubric:: test_tangler.py (55) = +.. parsed-literal:: + :class: code + + |srarr|\ Tangle Test overheads: imports, etc. (`69`_) + |srarr|\ Tangle Test superclass to refactor common setup (`56`_) + |srarr|\ Tangle Test semantic error 2 (`57`_) + |srarr|\ Tangle Test semantic error 3 (`59`_) + |srarr|\ Tangle Test semantic error 4 (`61`_) + |srarr|\ Tangle Test semantic error 5 (`63`_) + |srarr|\ Tangle Test semantic error 6 (`65`_) + |srarr|\ Tangle Test include error 7 (`67`_) + |srarr|\ Tangle Test main program (`70`_) + +.. + + .. class:: small + + |loz| *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. + + + +.. _`56`: +.. rubric:: Tangle Test superclass to refactor common setup (56) = +.. parsed-literal:: + :class: code + + + class TangleTestcase( unittest.TestCase ): + text= "" + file\_name= "" + error= "" + def setUp( self ): + source= io.StringIO( self.text ) + self.web= pyweb.Web() + 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 as 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 + +.. + + .. class:: small + + |loz| *Tangle Test superclass to refactor common setup (56)*. Used by: test_tangler.py (`55`_) + + + +.. _`57`: +.. rubric:: Tangle Test semantic error 2 (57) = +.. parsed-literal:: + :class: code + + + |srarr|\ 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." ) + +.. + + .. class:: small + + |loz| *Tangle Test semantic error 2 (57)*. Used by: test_tangler.py (`55`_) + + + +.. _`58`: +.. rubric:: Sample Document 2 (58) = +.. 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 (58)*. Used by: Tangle Test semantic error 2 (`57`_); test_tangler.py (`55`_) + + + +.. _`59`: +.. rubric:: Tangle Test semantic error 3 (59) = +.. parsed-literal:: + :class: code + + + |srarr|\ 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." ) + +.. + + .. class:: small + + |loz| *Tangle Test semantic error 3 (59)*. Used by: test_tangler.py (`55`_) + + + +.. _`60`: +.. rubric:: Sample Document 3 (60) = +.. 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 (60)*. Used by: Tangle Test semantic error 3 (`59`_); test_tangler.py (`55`_) + + + + +.. _`61`: +.. rubric:: Tangle Test semantic error 4 (61) = +.. parsed-literal:: + :class: code + + + |srarr|\ 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...'" ) + +.. + + .. class:: small + + |loz| *Tangle Test semantic error 4 (61)*. Used by: test_tangler.py (`55`_) + + + +.. _`62`: +.. rubric:: Sample Document 4 (62) = +.. 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 (62)*. Used by: Tangle Test semantic error 4 (`61`_); test_tangler.py (`55`_) + + + +.. _`63`: +.. rubric:: Tangle Test semantic error 5 (63) = +.. parsed-literal:: + :class: code + + + |srarr|\ 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 ['part1a', 'part1b']" ) + +.. + + .. class:: small + + |loz| *Tangle Test semantic error 5 (63)*. Used by: test_tangler.py (`55`_) + + + +.. _`64`: +.. rubric:: Sample Document 5 (64) = +.. 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 (64)*. Used by: Tangle Test semantic error 5 (`63`_); test_tangler.py (`55`_) + + + +.. _`65`: +.. rubric:: Tangle Test semantic error 6 (65) = +.. parsed-literal:: + :class: code + + + |srarr|\ 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() ) ) + +.. + + .. class:: small + + |loz| *Tangle Test semantic error 6 (65)*. Used by: test_tangler.py (`55`_) + + + +.. _`66`: +.. rubric:: Sample Document 6 (66) = +.. 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 (66)*. Used by: Tangle Test semantic error 6 (`65`_); test_tangler.py (`55`_) + + + +.. _`67`: +.. rubric:: Tangle Test include error 7 (67) = +.. parsed-literal:: + :class: code + + + |srarr|\ 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() + +.. + + .. class:: small + + |loz| *Tangle Test include error 7 (67)*. Used by: test_tangler.py (`55`_) + + + +.. _`68`: +.. rubric:: Sample Document 7 and it's included file (68) = +.. 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 (68)*. Used by: Tangle Test include error 7 (`67`_); test_tangler.py (`55`_) + + + +.. _`69`: +.. rubric:: Tangle Test overheads: imports, etc. (69) = +.. parsed-literal:: + :class: code + + from \_\_future\_\_ import print\_function + """Tangler tests exercise various semantic features.""" + import pyweb + import unittest + import logging + import os + import io + +.. + + .. class:: small + + |loz| *Tangle Test overheads: imports, etc. (69)*. Used by: test_tangler.py (`55`_) + + + +.. _`70`: +.. rubric:: Tangle Test main program (70) = +.. 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 (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. + + +.. _`71`: +.. rubric:: test_weaver.py (71) = +.. parsed-literal:: + :class: code + + |srarr|\ Weave Test overheads: imports, etc. (`78`_) + |srarr|\ Weave Test superclass to refactor common setup (`72`_) + |srarr|\ Weave Test references and definitions (`73`_) + |srarr|\ Weave Test evaluation of expressions (`76`_) + |srarr|\ Weave Test main program (`79`_) + +.. + + .. class:: small + + |loz| *test_weaver.py (71)*. + + +Weaving test cases have a common setup shown in this superclass. + + +.. _`72`: +.. rubric:: Weave Test superclass to refactor common setup (72) = +.. parsed-literal:: + :class: code + + + class WeaveTestcase( unittest.TestCase ): + text= "" + file\_name= "" + error= "" + def setUp( self ): + source= io.StringIO( self.text ) + self.web= pyweb.Web() + 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 as 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 + +.. + + .. class:: small + + |loz| *Weave Test superclass to refactor common setup (72)*. Used by: test_weaver.py (`71`_) + + + +.. _`73`: +.. rubric:: Weave Test references and definitions (73) = +.. parsed-literal:: + :class: code + + + |srarr|\ Sample Document 0 (`74`_) + |srarr|\ 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() + self.maxDiff= None + self.assertEqual( test0\_expected, actual ) + + +.. + + .. class:: small + + |loz| *Weave Test references and definitions (73)*. Used by: test_weaver.py (`71`_) + + + +.. _`74`: +.. rubric:: Sample Document 0 (74) = +.. 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 (74)*. Used by: Weave Test references and definitions (`73`_); test_weaver.py (`71`_) + + + +.. _`75`: +.. rubric:: Expected Output 0 (75) = +.. 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 (75)*. Used by: Weave Test references and definitions (`73`_); test_weaver.py (`71`_) + + +Note that this really requires a mocked ``time`` module in order +to properly provide a consistent output from ``time.asctime()``. + + +.. _`76`: +.. rubric:: Weave Test evaluation of expressions (76) = +.. parsed-literal:: + :class: code + + + |srarr|\ 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)\\n", actual[2] ) + self.assertEquals( 'Version = 2.3\\n', actual[3] ) + self.assertEquals( 'CWD = %s\\n' % os.getcwd(), actual[4] ) + +.. + + .. class:: small + + |loz| *Weave Test evaluation of expressions (76)*. Used by: test_weaver.py (`71`_) + + + +.. _`77`: +.. rubric:: Sample Document 9 (77) = +.. 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 (77)*. Used by: Weave Test evaluation of expressions (`76`_); test_weaver.py (`71`_) + + + +.. _`78`: +.. rubric:: Weave Test overheads: imports, etc. (78) = +.. parsed-literal:: + :class: code + + from \_\_future\_\_ import print\_function + """Weaver tests exercise various weaving features.""" + import pyweb + import unittest + import logging + import os + import string + import io + +.. + + .. class:: small + + |loz| *Weave Test overheads: imports, etc. (78)*. Used by: test_weaver.py (`71`_) + + + +.. _`79`: +.. rubric:: Weave Test main program (79) = +.. parsed-literal:: + :class: code + + + if \_\_name\_\_ == "\_\_main\_\_": + import sys + logging.basicConfig( stream=sys.stdout, level= logging.WARN ) + unittest.main() + +.. + + .. class:: small + + |loz| *Weave Test main program (79)*. Used by: test_weaver.py (`71`_) + + + +Combined Test Script +===================== + +.. test/combined.w + +The combined test script runs all tests in all test modules. + + +.. _`80`: +.. rubric:: test.py (80) = +.. parsed-literal:: + :class: code + + |srarr|\ Combined Test overheads, imports, etc. (`81`_) + |srarr|\ Combined Test suite which imports all other test modules (`82`_) + |srarr|\ Combined Test main script (`83`_) + +.. + + .. class:: small + + |loz| *test.py (80)*. + + +The overheads import unittest and logging, because those are essential +infrastructure. Additionally, each of the test modules is also imported. + + +.. _`81`: +.. rubric:: Combined Test overheads, imports, etc. (81) = +.. parsed-literal:: + :class: code + + """Combined tests.""" + import unittest + import test\_loader + import test\_tangler + import test\_weaver + import test\_unit + import logging + +.. + + .. class:: small + + |loz| *Combined Test overheads, imports, etc. (81)*. Used by: test.py (`80`_) + + +The test suite is built from each of the individual test modules. + + +.. _`82`: +.. rubric:: Combined Test suite which imports all other test modules (82) = +.. parsed-literal:: + :class: code + + + def suite(): + s= unittest.TestSuite() + for m in ( test\_loader, test\_tangler, test\_weaver, test\_unit ): + s.addTests( unittest.defaultTestLoader.loadTestsFromModule( m ) ) + return s + +.. + + .. class:: small + + |loz| *Combined Test suite which imports all other test modules (82)*. Used by: test.py (`80`_) + + +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. + + + +.. _`83`: +.. rubric:: Combined Test main script (83) = +.. parsed-literal:: + :class: code + + + 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) ) + +.. + + .. class:: small + + |loz| *Combined Test main script (83)*. Used by: test.py (`80`_) + + +Additional 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. + + +.. _`84`: +.. rubric:: docutils.conf (84) = +.. 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 (84)*. + + +``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. + + +.. _`85`: +.. rubric:: page-layout.css (85) = +.. 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 (85)*. + + +Indices +======= + +Files +----- + + +:docutils.conf: + |srarr|\ (`84`_) +:page-layout.css: + |srarr|\ (`85`_) +:test.py: + |srarr|\ (`80`_) +:test_loader.py: + |srarr|\ (`47`_) +:test_tangler.py: + |srarr|\ (`55`_) +:test_unit.py: + |srarr|\ (`1`_) +:test_weaver.py: + |srarr|\ (`71`_) + + + +Macros +------ + + +:Combined Test main script: + |srarr|\ (`83`_) +:Combined Test overheads, imports, etc.: + |srarr|\ (`81`_) +:Combined Test suite which imports all other test modules: + |srarr|\ (`82`_) +:Expected Output 0: + |srarr|\ (`75`_) +:Load Test error handling with a few common syntax errors: + |srarr|\ (`49`_) +:Load Test include processing with syntax errors: + |srarr|\ (`51`_) +:Load Test main program: + |srarr|\ (`54`_) +:Load Test overheads: imports, etc.: + |srarr|\ (`53`_) +:Load Test superclass to refactor common setup: + |srarr|\ (`48`_) +:Sample Document 0: + |srarr|\ (`74`_) +:Sample Document 1 with correct and incorrect syntax: + |srarr|\ (`50`_) +:Sample Document 2: + |srarr|\ (`58`_) +:Sample Document 3: + |srarr|\ (`60`_) +:Sample Document 4: + |srarr|\ (`62`_) +:Sample Document 5: + |srarr|\ (`64`_) +:Sample Document 6: + |srarr|\ (`66`_) +:Sample Document 7 and it's included file: + |srarr|\ (`68`_) +:Sample Document 8 and the file it includes: + |srarr|\ (`52`_) +:Sample Document 9: + |srarr|\ (`77`_) +:Tangle Test include error 7: + |srarr|\ (`67`_) +:Tangle Test main program: + |srarr|\ (`70`_) +:Tangle Test overheads: imports, etc.: + |srarr|\ (`69`_) +:Tangle Test semantic error 2: + |srarr|\ (`57`_) +:Tangle Test semantic error 3: + |srarr|\ (`59`_) +:Tangle Test semantic error 4: + |srarr|\ (`61`_) +:Tangle Test semantic error 5: + |srarr|\ (`63`_) +:Tangle Test semantic error 6: + |srarr|\ (`65`_) +:Tangle Test superclass to refactor common setup: + |srarr|\ (`56`_) +:Unit Test Mock Chunk class: + |srarr|\ (`4`_) +:Unit Test Web class chunk cross-reference: + |srarr|\ (`35`_) +:Unit Test Web class construction methods: + |srarr|\ (`33`_) +:Unit Test Web class name resolution methods: + |srarr|\ (`34`_) +:Unit Test Web class tangle: + |srarr|\ (`36`_) +:Unit Test Web class weave: + |srarr|\ (`37`_) +:Unit Test main: + |srarr|\ (`46`_) +:Unit Test of Action class hierarchy: + |srarr|\ (`39`_) +:Unit Test of Application class: + |srarr|\ (`44`_) +: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|\ (`25`_) +:Unit Test of Command class hierarchy: + |srarr|\ (`22`_) +:Unit Test of Command superclass: + |srarr|\ (`23`_) +: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|\ (`27`_) +: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|\ (`28`_) +:Unit Test of NamedChunk subclass: + |srarr|\ (`19`_) +:Unit Test of NamedDocumentChunk subclass: + |srarr|\ (`21`_) +:Unit Test of OutputChunk subclass: + |srarr|\ (`20`_) +:Unit Test of Reference class hierarchy: + |srarr|\ (`31`_) +:Unit Test of ReferenceCommand class for chunk references: + |srarr|\ (`30`_) +: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|\ (`24`_) +:Unit Test of UserIdXrefCommand class for a user identifier cross-reference: + |srarr|\ (`29`_) +:Unit Test of Weaver subclass of Emitter: + |srarr|\ (`5`_) +:Unit Test of Web class: + |srarr|\ (`32`_) +:Unit Test of WebReader class: + |srarr|\ (`38`_) +:Unit Test of XrefCommand superclass for all cross-reference commands: + |srarr|\ (`26`_) +:Unit Test overheads: imports, etc.: + |srarr|\ (`45`_) +:Unit test of Action Sequence class: + |srarr|\ (`40`_) +:Unit test of LoadAction class: + |srarr|\ (`43`_) +:Unit test of TangleAction class: + |srarr|\ (`42`_) +:Unit test of WeaverAction class: + |srarr|\ (`41`_) +:Weave Test evaluation of expressions: + |srarr|\ (`76`_) +:Weave Test main program: + |srarr|\ (`79`_) +:Weave Test overheads: imports, etc.: + |srarr|\ (`78`_) +:Weave Test references and definitions: + |srarr|\ (`73`_) +:Weave Test superclass to refactor common setup: + |srarr|\ (`72`_) + + + +User Identifiers +---------------- + +(None) + + +---------- + +.. class:: small + + + Created by ../pyweb.py at Tue Mar 11 10:12:14 2014. + + pyweb.__version__ '2.3'. + + Source combined.w modified Fri Mar 7 09:51:12 2014. + + Working directory '/Users/slott/Documents/Projects/pyWeb-2.3/pyweb/test'. + diff --git a/test/combined.w b/test/combined.w index ebb00a0..b3b2d0f 100644 --- a/test/combined.w +++ b/test/combined.w @@ -1,50 +1,95 @@ - +Combined Test Script +===================== -

The combined test script runs all tests in all test modules.

+.. test/combined.w + +The combined test script runs all tests in all test modules. @o test.py @{@ @ +@ @ @} -

The overheads import unittest and logging, because those are essential +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.""" +@{"""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.

+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 ) ) + 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. -

+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(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__": - import sys - logging.basicConfig( stream=sys.stdout, level=logging.CRITICAL ) - tr= unittest.TextTestRunner() - result= tr.run( suite() ) + 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) ) -@} \ No newline at end of file + sys.exit(len(result.failures) + len(result.errors)) +@} diff --git a/test/docutils.conf b/test/docutils.conf new file mode 100644 index 0000000..522baed --- /dev/null +++ b/test/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/test/func.w b/test/func.w index b189002..758e3d9 100644 --- a/test/func.w +++ b/test/func.w @@ -1,18 +1,22 @@ - +Functional Testing +================== -

There are three broad areas of functional testing.

+.. test/func.w -
    -
  • Loading
  • -
  • Tanging
  • -
  • Weaving
  • -
+There are three broad areas of functional testing. -

There are a total of 11 test cases.

+- `Tests for Loading`_ -

Tests for Loading

+- `Tests for Tangling`_ -

We need to be able to load a web from one or more source files.

+- `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 @{@ @@ -22,48 +26,66 @@ @ @} -

Parsing test cases have a common setup shown in this superclass.

+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. -

+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 ) +class ParseTestcase(unittest.TestCase): + text = "" + file_name = "" + 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. +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 +@} @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] ) +class Test_ParseErrors(ParseTestcase): + text = test1_w + file_name = "test1.w" + def setUp(self) -> None: + super().setUp() + self.logger = logging.getLogger("WebReader") + self.buffer = logging.handlers.BufferingHandler(12) + self.buffer.setLevel(logging.WARN) + self.logger.addHandler(self.buffer) + self.logger.setLevel(logging.WARN) + def test_error_should_count_1(self) -> None: + self.rdr.load(self.web, self.file_name, self.source) + self.assertEqual(3, self.rdr.errors) + messages = [r.message for r in self.buffer.buffer] + self.assertEqual( + ["At ('test1.w', 8): expected ('@@{',), found '@@o'", + "Extra '@@{' (possibly missing chunk name) near ('test1.w', 9)", + "Extra '@@{' (possibly missing chunk name) near ('test1.w', 9)"], + messages + ) + def tearDown(self) -> None: + self.logger.setLevel(logging.CRITICAL) + self.logger.removeHandler(self.buffer) + super().tearDown() + @} @d Sample Document 1... @{ -test1_w= """Some anonymous chunk +test1_w = """Some anonymous chunk @@o test1.tmp @@{@@ @@ @@ -75,45 +97,52 @@ Okay, now for an error. """ @} -

All of the parsing exceptions should be correctly identified with +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 +In order to test 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 ): +class Test_IncludeParseErrors(ParseTestcase): + text = test8_w + file_name = "test8.w" + def setUp(self) -> None: 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. -

+ temp.write(test8_inc_w) + super().setUp() + self.logger = logging.getLogger("WebReader") + self.buffer = logging.handlers.BufferingHandler(12) + self.buffer.setLevel(logging.WARN) + self.logger.addHandler(self.buffer) + self.logger.setLevel(logging.WARN) + def test_error_should_count_2(self) -> None: + self.rdr.load(self.web, self.file_name, self.source) + self.assertEqual(1, self.rdr.errors) + messages = [r.message for r in self.buffer.buffer] + self.assertEqual( + ["At ('test8_inc.tmp', 4): end of input, ('@@{', '@@[') not found", + "Errors in included file 'test8_inc.tmp', output is incomplete."], + messages + ) + def tearDown(self) -> None: + self.logger.setLevel(logging.CRITICAL) + self.logger.removeHandler(self.buffer) + os.remove('test8_inc.tmp') + super().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. +test8_w = """Some anonymous chunk. @@d title @@[the title of this document, defined with @@@@[ and @@@@]@@] A reference to @@. @@i test8_inc.tmp @@ -129,28 +158,30 @@ And now for an error - incorrect syntax in an included file!

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 +import io +import types @} -

A main program that configures logging and then runs the test.

+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 ) + logging.basicConfig(stream=sys.stdout, level=logging.WARN) unittest.main() @} -

Tests for Tangling

+Tests for Tangling +------------------ -

We need to be able to tangle a web.

+We need to be able to tangle a web. @o test_tangler.py @{@ @@ -164,36 +195,35 @@ if __name__ == "__main__": @ @} -

Tangling test cases have a common setup and teardown shown in this superclass. +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 ): +class TangleTestcase(unittest.TestCase): + text = "" + file_name = "" + error = "" + 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.tangle( self.tangler ) + self.rdr.load(self.web, self.file_name, self.source) + 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 ) + self.fail("Should not tangle") + except pyweb.Error as e: + self.assertEqual(exception_text, e.args[0]) + def tearDown(self) -> None: + name, _ = os.path.splitext(self.file_name) try: - os.remove( name + ".tmp" ) + os.remove(name + ".tmp") except OSError: pass @} @@ -202,15 +232,15 @@ class TangleTestcase( unittest.TestCase ): @{ @ -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." ) +class Test_SemanticError_2(TangleTestcase): + text = test2_w + file_name = "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 +test2_w = """Some anonymous chunk @@o test2.tmp @@{@@ @@ @@ -224,15 +254,15 @@ Okay, now for some errors: no part2! @{ @ -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." ) +class Test_SemanticError_3(TangleTestcase): + text = test3_w + file_name = "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 +test3_w = """Some anonymous chunk @@o test3.tmp @@{@@ @@ @@ -248,15 +278,15 @@ Okay, now for some errors: attempt to tangle a cross-reference! @{ @ -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...'" ) +class Test_SemanticError_4(TangleTestcase): + text = test4_w + file_name = "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 +test4_w = """Some anonymous chunk @@o test4.tmp @@{@@ @@ @@ -271,15 +301,15 @@ Okay, now for some errors: attempt to weave but no full name for part1.... @{ @ -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']" ) +class Test_SemanticError_5(TangleTestcase): + text = test5_w + file_name = "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= """ +test5_w = """ Some anonymous chunk @@o test5.tmp @@{@@ @@ -296,20 +326,20 @@ Okay, now for some errors: part1... is ambiguous @{ @ -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 ) +class Test_SemanticError_6(TangleTestcase): + text = test6_w + file_name = "test6.w" + def test_should_warn(self) -> None: + self.rdr.load(self.web, self.file_name, self.source) + 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() ) ) + 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 +test6_w = """Some anonymous chunk @@o test6.tmp @@{@@ @@ @@ -326,26 +356,26 @@ Okay, now for some warnings: @{ @ -class Test_IncludeError_7( TangleTestcase ): - text= test7_w - file_name= "test7.w" - def setUp( self ): +class Test_IncludeError_7(TangleTestcase): + text = test7_w + file_name = "test7.w" + def setUp(self) -> None: 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 ) + temp.write(test7_inc_w) + super().setUp() + def test_should_include(self) -> None: + self.rdr.load(self.web, self.file_name, self.source) + 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() + self.assertEqual(5, len(self.web.chunkSeq)) + self.assertEqual(test7_inc_w, self.web.chunkSeq[3].commands[0].text) + def tearDown(self) -> None: + os.remove('test7_inc.tmp') + super().tearDown() @} @d Sample Document 7... @{ -test7_w= """ +test7_w = """ Some anonymous chunk. @@d title @@[the title of this document, defined with @@@@[ and @@@@]@@] A reference to @@. @@ -353,32 +383,33 @@ A reference to @@. A final anonymous chunk from test7.w """ -test7_inc_w= """The test7a.tmp chunk for 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 +import io @} @d Tangle Test main program... @{ if __name__ == "__main__": import sys - logging.basicConfig( stream=sys.stdout, level= logging.WARN ) + logging.basicConfig(stream=sys.stdout, level=logging.WARN) unittest.main() @} -

Tests for Weaving

+Tests for Weaving +----------------- -

We need to be able to weave a document from one or more source files.

+We need to be able to weave a document from one or more source files. @o test_weaver.py @{@ @@ -388,31 +419,29 @@ if __name__ == "__main__": @ @} -

Weaving test cases have a common setup shown in this superclass.

+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 ): +class WeaveTestcase(unittest.TestCase): + text = "" + file_name = "" + error = "" + def setUp(self) -> None: + self.source = io.StringIO(self.text) + self.web = pyweb.Web() + self.rdr = pyweb.WebReader() + def tangle_and_check_exception(self, exception_text: str) -> None: try: - self.rdr.load() - self.web.tangle( self.tangler ) + self.rdr.load(self.web, self.file_name, self.source) + 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 ) + self.fail("Should not tangle") + except pyweb.Error as e: + self.assertEqual(exception_text, e.args[0]) + def tearDown(self) -> None: + name, _ = os.path.splitext(self.file_name) try: - os.remove( name + ".html" ) + os.remove(name + ".html") except OSError: pass @} @@ -421,26 +450,27 @@ class WeaveTestcase( unittest.TestCase ): @ @ -class Test_RefDefWeave( WeaveTestcase ): - text= test0_w +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 ) + def test_load_should_createChunks(self) -> None: + self.rdr.load(self.web, self.file_name, self.source) + self.assertEqual(3, len(self.web.chunkSeq)) + def test_weave_should_createFile(self) -> None: + self.rdr.load(self.web, self.file_name, self.source) + doc = pyweb.HTML() + doc.reference_style = pyweb.SimpleReference() + 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]) ) ) + actual = source.read() + self.maxDiff = None + self.assertEqual(test0_expected, actual) @} @d Sample Document 0... @{ -test0_w= """ +test0_w = """ @@ -449,11 +479,11 @@ test0_w= """ @@d some code @@{ -def fastExp( n, p ): - r= 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) + return n*n*fastExp(n,p/2) for i in range(24): fastExp(2,i) @@ -464,24 +494,24 @@ for i in range(24): @} @d Expected Output 0... @{ -expected= """ +test0_expected = """ - some code (1) +some code (1) - +

some code (1) =


 
-def fastExp( n, p ):
-    r= 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)
+    return n*n*fastExp(n,p/2)
 
 for i in range(24):
     fastExp(2,i)
@@ -496,24 +526,28 @@ for i in range(24):
 """
 @}
 
+Note that this really requires a mocked ``time`` module in order
+to properly provide a consistent output from ``time.asctime()``.
+
 @d Weave Test evaluation... @{
 @
 
-class TestEvaluations( WeaveTestcase ):
-    text= test9_w
+class TestEvaluations(WeaveTestcase):
+    text = test9_w
     file_name = "test9.w"
-    def test_should_evaluate( self ):
-        doc= pyweb.HTML()
-        self.web.weave( doc )
+    def test_should_evaluate(self) -> None:
+        self.rdr.load(self.web, self.file_name, self.source)
+        doc = pyweb.HTML( )
+        doc.reference_style = pyweb.SimpleReference() 
+        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] )
+            actual = source.readlines()
+        #print(actual)
+        self.assertEqual("An anonymous chunk.\n", actual[0])
+        self.assertTrue(actual[1].startswith("Time ="))
+        self.assertEqual("File = ('test9.w', 3)\n", actual[2])
+        self.assertEqual('Version = 3.1\n', actual[3])
+        self.assertEqual(f'CWD = {os.getcwd()}\n', actual[4])
 @}
 
 @d Sample Document 9...
@@ -522,27 +556,25 @@ test9_w= """An anonymous chunk.
 Time = @@(time.asctime()@@)
 File = @@(theLocation@@)
 Version = @@(__version__@@)
-OS = @@(os.name@@)
-CWD = @@(os.getcwd()@@)
+CWD = @@(os.path.realpath('.')@@)
 """
 @}
 
 @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
+import io
 @}
 
 @d Weave Test main program...
 @{
 if __name__ == "__main__":
     import sys
-    logging.basicConfig( stream=sys.stdout, level= logging.WARN )
+    logging.basicConfig(stream=sys.stderr, level=logging.WARN)
     unittest.main()
 @}
diff --git a/test/intro.w b/test/intro.w
index 4fa7d2c..23b3631 100644
--- a/test/intro.w
+++ b/test/intro.w
@@ -1,47 +1,61 @@
-
+Introduction
+============
 
-

There are two levels of testing in this document.

- +.. test/intro.w -

Other testing, like performance or security, is possible. +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. - -

-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 + +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/test/page-layout.css b/test/page-layout.css new file mode 100644 index 0000000..e11a707 --- /dev/null +++ b/test/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_test.html b/test/pyweb_test.html index be94ae9..e4f8047 100644 --- a/test/pyweb_test.html +++ b/test/pyweb_test.html @@ -1,2704 +1,3028 @@ - - + + - pyWeb Literate Programming 2.1 - Test Suite - - - - - -
+ + +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)
+→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 (49)
+
+ +
+

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, fileName: str) -> 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("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("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("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("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.

+

Unit Test Mock Chunk class (4) =

+
+class MockChunk:
+    def __init__(self, name: str, seq: int, lineNumber: int) -> None:
+        self.name = name
+        self.fullName = name
+        self.seq = seq
+        self.lineNumber = lineNumber
+        self.initial = True
+        self.commands = []
+        self.referencedBy = []
+    def __repr__(self) -> str:
+        return f"({self.name!r}, {self.seq!r})"
+    def references(self, aWeaver: pyweb.Weaver) -> list[str]:
+        return [(c.name, c.seq) for c in self.referencedBy]
+    def reference_indent(self, aWeb: "Web", aTangler: "Tangler", amount: int) -> None:
+        aTangler.addIndent(amount)
+    def reference_dedent(self, aWeb: "Web", aTangler: "Tangler") -> None:
+        aTangler.clrIndent()
+    def tangle(self, aWeb: "Web", aTangler: "Tangler") -> None:
+        aTangler.write(self.name)
+
+ +
+

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.filename = "testweaver"
+        self.aFileChunk = MockChunk("File", 123, 456)
+        self.aFileChunk.referencedBy = [ ]
+        self.aChunk = MockChunk("Chunk", 314, 278)
+        self.aChunk.referencedBy = [ self.aFileChunk ]
+    def tearDown(self) -> None:
         import os
         try:
-            os.remove( "testweaver.rst" )
+            pass #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 )
+
+    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)
+
+    def test_weaver_should_codeBegin(self) -> None:
+        self.weaver.open(self.filename)
+        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()
-        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 )
+        with open("testweaver.rst", "r") as result:
+            txt = result.read()
+        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.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 )
+        with open("testweaver.rst", "r") as result:
+            txt = result.read()
+        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 ):
-        self.weaver.open( self.filename )
+    def test_weaver_should_xref(self) -> None:
+        self.weaver.open(self.filename)
         self.weaver.xrefHead( )
-        self.weaver.xrefLine( "Chunk", [ ("Container", 123) ] )
+        self.weaver.xrefLine("Chunk", [ ("Container", 123) ])
         self.weaver.xrefFoot( )
-        self.weaver.fileEnd( self.aFileChunk )
+        #self.weaver.fileEnd(self.aFileChunk) # Why?
         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 )
+        with open("testweaver.rst", "r") as result:
+            txt = result.read()
+        self.assertEqual("\n:Chunk:\n    |srarr|\\ (`('Container', 123)`_)\n\n", txt)
 
-    def test_weaver_should_xref_def( self ):
-        self.weaver.open( self.filename )
+    def test_weaver_should_xref_def(self) -> None:
+        self.weaver.open(self.filename)
         self.weaver.xrefHead( )
-        self.weaver.xrefDefLine( "Chunk", 314, [ ("Container", 123), ("Chunk", 314) ] )
+        # 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 )
+        #self.weaver.fileEnd(self.aFileChunk) # Why?
         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). -

- - + with open("testweaver.rst", "r") as result: + txt = result.read() + 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. -

- +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 ):
+

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.filename = "testweaver"
+        self.aFileChunk = MockChunk("File", 123, 456)
+        self.aFileChunk.referencedBy = [ ]
+        self.aChunk = MockChunk("Chunk", 314, 278)
+        self.aChunk.referencedBy = [ self.aFileChunk, ]
+    def tearDown(self) -> None:
         import os
         try:
-            os.remove( "testweaver.tex" )
+            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). -

- + def test_weaver_functions_latex(self) -> None: + result = self.weaver.quote("\\end{Verbatim}") + self.assertEqual("\\end\\,{Verbatim}", result) + result = self.weaver.references(self.aChunk) + self.assertEqual("\n \\footnotesize\n Used by:\n \\begin{list}{}{}\n \n \\item Code example File (123) (Sect. \\ref{pyweb123}, p. \\pageref{pyweb123})\n\n \\end{list}\n \\normalsize\n", result) + result = self.weaver.referenceTo("Chunk", 314) + self.assertEqual("$\\triangleright$ Code Example Chunk (314)", result) +
+ +
+

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 ):
-        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 ):
+

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.filename = "testweaver"
+        self.aFileChunk = MockChunk("File", 123, 456)
+        self.aFileChunk.referencedBy = []
+        self.aChunk = MockChunk("Chunk", 314, 278)
+        self.aChunk.referencedBy = [ self.aFileChunk, ]
+    def tearDown(self) -> None:
         import os
         try:
-            os.remove( "testweaver.html" )
+            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). -

- + 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) +
+ +
+

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 just a template change.

+
+To Do 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 ):
-        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 ):
+

Unit Test of Tangler subclass of Emitter (9) =

+
+class TestTangler(unittest.TestCase):
+    def setUp(self) -> None:
+        self.tangler = pyweb.Tangler()
+        self.filename = "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:
         import os
         try:
-            os.remove( "testtangler.w" )
+            os.remove("testtangler.code")
         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). -

+ 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.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.code", "r") as result: + txt = result.read() + 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 file difference -check. If the file is different, the old version is replaced with +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. -

- -

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 )
+untouched.

+

In order to be sure that the timestamps really have changed, we either +need to wait for a full second to elapse or we need to mock the various +os and filecmp features used by TanglerMake.

+

Unit Test of TanglerMake subclass of Emitter (10) =

+
+class TestTanglerMake(unittest.TestCase):
+    def setUp(self) -> None:
+        self.tangler = pyweb.TanglerMake()
+        self.filename = "testtangler.code"
+        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 ):
+        self.time_original = os.path.getmtime(self.filename)
+        self.original = os.lstat(self.filename)
+        #time.sleep(0.75)  # Alternative to assure timestamps must be different
+
+    def tearDown(self) -> None:
         import os
         try:
-            os.remove( "testtangler.w" )
+            os.remove("testtangler.code")
         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 )
+
+    def test_same_should_leave(self) -> None:
+        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.assertTrue(os.path.samestat(self.original, os.lstat(self.filename)))
+        #self.assertEqual(self.time_original, os.path.getmtime(self.filename))
+
+    def test_different_should_update(self) -> None:
+        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

- + self.assertFalse(os.path.samestat(self.original, os.lstat(self.filename))) + #self.assertNotEqual(self.time_original, os.path.getmtime(self.filename)) +
+ +
+

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 OutputChunk subclass (20)
-Unit Test of NamedDocumentChunk subclass (21)
-
-    
-

Unit Test of Chunk class hierarchy (11). - Used by test_unit.py (1). -

- - +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) =

-

-
-class MockCommand( object ):
-    def __init__( self ):
-        self.lineNumber= 314
-    def startswith( self, text ):
+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:
+    def __init__(self) -> None:
+        self.lineNumber = 314
+    def startswith(self, text: str) -> bool:
         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 ):
+
+ +
+

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) +=

+
+class MockWeb:
+    def __init__(self) -> None:
+        self.chunks = []
+        self.wove = None
+        self.tangled = None
+    def add(self, aChunk: pyweb.Chunk) -> None:
+        self.chunks.append(aChunk)
+    def addNamed(self, aChunk: pyweb.Chunk) -> None:
+        self.chunks.append(aChunk)
+    def addOutput(self, aChunk: pyweb.Chunk) -> None:
+        self.chunks.append(aChunk)
+    def fullNameFor(self, name: str) -> str:
         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 ):
+    def fileXref(self) -> dict[str, list[int]]:
+        return {'file': [1,2,3]}
+    def chunkXref(self) -> dict[str, list[int]]:
+        return {'chunk': [4,5,6]}
+    def userNamesXref(self) -> dict[str, list[int]]:
+        return {'name': (7, [8,9,10])}
+    def getchunk(self, name: str) -> list[pyweb.Chunk]:
+        return [MockChunk(name, 1, 314)]
+    def createUsedBy(self) -> None:
         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 ):
+    def weaveChunk(self, name, weaver) -> None:
+        weaver.write(name)
+    def weave(self, weaver) -> None:
+        self.wove = weaver
+    def tangle(self, tangler) -> None:
+        self.tangled = tangler
+
+ +
+

Unit Test of Chunk superclass (13). Used by: Unit Test of Chunk class hierarchy... (11)

+
+

A MockWeaver or MockTangle can process a Chunk.

+

Unit Test of Chunk superclass (14) +=

+
+class MockWeaver:
+    def __init__(self) -> None:
+        self.begin_chunk = []
+        self.end_chunk = []
+        self.written = []
+        self.code_indent = None
+    def quote(self, text: str) -> str:
+        return text.replace("&", "&amp;") # token quoting
+    def docBegin(self, aChunk: pyweb.Chunk) -> None:
+        self.begin_chunk.append(aChunk)
+    def write(self, text: str) -> None:
+        self.written.append(text)
+    def docEnd(self, aChunk: pyweb.Chunk) -> None:
+        self.end_chunk.append(aChunk)
+    def codeBegin(self, aChunk: pyweb.Chunk) -> None:
+        self.begin_chunk.append(aChunk)
+    def codeBlock(self, text: str) -> None:
+        self.written.append(text)
+    def codeEnd(self, aChunk: pyweb.Chunk) -> None:
+        self.end_chunk.append(aChunk)
+    def fileBegin(self, aChunk: pyweb.Chunk) -> None:
+        self.begin_chunk.append(aChunk)
+    def fileEnd(self, aChunk: pyweb.Chunk) -> None:
+        self.end_chunk.append(aChunk)
+    def addIndent(self, increment=0):
         pass
-    def clrIndent( self ):
+    def setIndent(self, fixed: int | None=None, command: str | None=None) -> None:
+        self.indent = fixed
+    def addIndent(self, increment: int = 0) -> None:
+        self.indent = increment
+    def clrIndent(self) -> None:
         pass
-    def xrefHead( self ):
+    def xrefHead(self) -> None:
         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 ):
+    def xrefLine(self, name: str, refList: list[int]) -> None:
+        self.written.append(f"{name} {refList}")
+    def xrefDefLine(self, name: str, defn: int, refList: list[int]) -> None:
+        self.written.append(f"{name} {defn} {refList}")
+    def xrefFoot(self) -> None:
         pass
-    def open( self, aFile ):
+    def referenceTo(self, name: str, seq: int) -> None:
         pass
-    def close( self ):
-        pass
-    def referenceTo( self, name, seq ):
+    def open(self, aFile: str) -> "MockWeaver":
+        return self
+    def close(self) -> None:
         pass
+    def __enter__(self) -> "MockWeaver":
+        return self
+    def __exit__(self, *args: Any) -> bool:
+        return False
 
-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). -

- - +class MockTangler(MockWeaver): + def __init__(self) -> None: + super().__init__() + self.context = [0] + def addIndent(self, amount: int) -> None: + pass +
+ +
+

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 ):
-        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). -

- - +

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 ):
-    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). -

- - +

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) )
+    cmd2 = MockCommand()
+    self.theChunk.append(cmd2)
+    self.assertEqual(2, len(self.theChunk.commands) )
+
+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) )
+
+def test_append_to_web_should_work(self) -> None:
+    web = MockWeb()
+    self.theChunk.webAdd(web)
+    self.assertEqual(1, len(web.chunks))
+
+ +
+

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 ):
-    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). -

- - +

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_should_work( self ):
+

Unit Test of Chunk emission (18) =

+
+def test_weave_should_work(self) -> None:
     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 ):
+    self.theChunk.appendText("this chunk has very & many words")
+    self.theChunk.weave(web, wvr)
+    self.assertEqual(1, len(wvr.begin_chunk))
+    self.assertTrue(wvr.begin_chunk[0] is self.theChunk)
+    self.assertEqual(1, len(wvr.end_chunk))
+    self.assertTrue(wvr.end_chunk[0] is self.theChunk)
+    self.assertEqual("this chunk has very & many words", "".join( wvr.written))
+
+def test_tangle_should_fail(self) -> None:
     tnglr = MockTangler()
     web = MockWeb()
-    self.theChunk.appendText( "this chunk has very & many words" )
+    self.theChunk.appendText("this chunk has very & many words")
     try:
-        self.theChunk.tangle( web, tnglr )
+        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. + 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 ):
-        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 ):
+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_to_web_should_work(self) -> None:
+        web = MockWeb()
+        self.theChunk.webAdd(web)
+        self.assertEqual(1, len(web.chunks))
+
+    def test_weave_should_work(self) -> None:
         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 ):
+        self.theChunk.weave(web, wvr)
+        self.assertEqual(1, len(wvr.begin_chunk))
+        self.assertTrue(wvr.begin_chunk[0] is self.theChunk)
+        self.assertEqual(1, len(wvr.end_chunk))
+        self.assertTrue(wvr.end_chunk[0] is self.theChunk)
+        self.assertEqual("the words &amp; text of this Chunk", "".join( wvr.written))
+
+    def test_tangle_should_work(self) -> None:
         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. + self.theChunk.tangle(web, tnglr) + self.assertEqual(1, len(tnglr.begin_chunk)) + self.assertTrue(tnglr.begin_chunk[0] is self.theChunk) + self.assertEqual(1, len(tnglr.end_chunk)) + self.assertTrue(tnglr.end_chunk[0] is self.theChunk) + self.assertEqual("the words & text of this Chunk", "".join( tnglr.written)) +

+ +
+

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("Some 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(1, len(tnglr.begin_chunk))
+        self.assertTrue(tnglr.begin_chunk[0] is self.theChunk)
+        self.assertEqual(1, len(tnglr.end_chunk))
+        self.assertTrue(tnglr.end_chunk[0] is self.theChunk)
+        self.assertEqual("the words & text of this Chunk", "".join( tnglr.written))
+
+ +
+

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 (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 ):
+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_to_web_should_work(self) -> None:
+        web = MockWeb()
+        self.theChunk.webAdd(web)
+        self.assertEqual(1, len(web.chunks))
+
+    def test_weave_should_work(self) -> None:
         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 ):
+        self.theChunk.weave(web, wvr)
+        self.assertEqual(1, len(wvr.begin_chunk))
+        self.assertTrue(wvr.begin_chunk[0] is self.theChunk)
+        self.assertEqual(1, len(wvr.end_chunk))
+        self.assertTrue(wvr.end_chunk[0] is self.theChunk)
+        self.assertEqual("the words &amp; text of this Chunk", "".join( wvr.written))
+
+    def test_tangle_should_work(self) -> None:
         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). -

- - + self.theChunk.tangle(web, tnglr) + self.assertEqual(1, len(tnglr.begin_chunk)) + self.assertTrue(tnglr.begin_chunk[0] is self.theChunk) + self.assertEqual(1, len(tnglr.end_chunk)) + self.assertTrue(tnglr.end_chunk[0] is self.theChunk) + self.assertEqual("the words & text of this Chunk", "".join( tnglr.written)) +
+ +
+

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 (23) =

-

- 
-    
-

Unit Test of Command superclass (23). - Used by Unit Test of Command class hierarchy (22); test_unit.py (1). -

- - +

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 (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 ):
+

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.assertEquals(  "Some text & words in the document\n    ", "".join( wvr.written ) )
-    def test_tangle_should_work( self ):
+        self.cmd.weave(web, wvr)
+        self.assertEqual("Some text & words in the document\n    ", "".join( wvr.written))
+    def test_tangle_should_work(self) -> None:
         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). -

- - + self.cmd.tangle(web, tnglr) + self.assertEqual("Some text & words in the document\n ", "".join( tnglr.written)) +
+ +
+

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 (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 ):
+

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.assertEquals(  "Some text &amp; words in the document\n    ", "".join( wvr.written ) )
-    def test_tangle_should_work( self ):
+        self.cmd.weave(web, wvr)
+        self.assertEqual("Some text &amp; words in the document\n    ", "".join( wvr.written))
+    def test_tangle_should_work(self) -> None:
         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). -

- - + self.cmd.tangle(web, tnglr) + self.assertEqual("Some text & words in the document\n ", "".join( tnglr.written)) +
+ +
+

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 (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 +

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 (27) =

-

- 
-class TestFileXRefCommand( unittest.TestCase ):
-    def setUp( self ):
-        self.cmd= pyweb.FileXrefCommand( 314 )
-    def test_weave_should_work( self ):
+

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.assertEquals(  "file [1, 2, 3]", "".join( wvr.written ) )
-    def test_tangle_should_fail( self ):
+        self.cmd.weave(web, wvr)
+        self.assertEqual("file [1, 2, 3]", "".join( wvr.written))
+    def test_tangle_should_fail(self) -> None:
         tnglr = MockTangler()
         web = MockWeb()
         try:
-            self.cmd.tangle( web, tnglr )
+            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 +

+ +
+

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 (28) =

-

-
-class TestMacroXRefCommand( unittest.TestCase ):
-    def setUp( self ):
-        self.cmd= pyweb.MacroXrefCommand( 314 )
-    def test_weave_should_work( self ):
+

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.assertEquals(  "chunk [4, 5, 6]", "".join( wvr.written ) )
-    def test_tangle_should_fail( self ):
+        self.cmd.weave(web, wvr)
+        self.assertEqual("chunk [4, 5, 6]", "".join( wvr.written))
+    def test_tangle_should_fail(self) -> None:
         tnglr = MockTangler()
         web = MockWeb()
         try:
-            self.cmd.tangle( web, tnglr )
+            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 @| +

+ +
+

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 (29) =

-

-
-class TestUserIdXrefCommand( unittest.TestCase ):
-    def setUp( self ):
-        self.cmd= pyweb.UserIdXrefCommand( 314 )
-    def test_weave_should_work( self ):
+

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.assertEquals(  "name 7 [8, 9, 10]", "".join( wvr.written ) )
-    def test_tangle_should_fail( self ):
+        self.cmd.weave(web, wvr)
+        self.assertEqual("name 7 [8, 9, 10]", "".join( wvr.written))
+    def test_tangle_should_fail(self) -> None:
         tnglr = MockTangler()
         web = MockWeb()
         try:
-            self.cmd.tangle( web, tnglr )
+            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). -

- - +
+ +
+

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 (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 ):
+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.assertEquals(  "Some Name", "".join( wvr.written ) )
-    def test_tangle_should_work( self ):
+        self.cmd.weave(web, wvr)
+        self.assertEqual("Some Name", "".join( wvr.written))
+    def test_tangle_should_work(self) -> None:
         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. -

- + web.add(self.chunk) + self.cmd.tangle(web, tnglr) + self.assertEqual("Some Name", "".join( tnglr.written)) +
+ +
+

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 (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

- +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 (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] )
+

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.webFileName = "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.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 ):
+    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 ):
-    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

- +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(3, len(tangler.written))
+    self.assertEqual(['some code', 'some user2a code', 'some user1 code'], tangler.written)
+
+ +
+

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(6, len(weaver.written))
+    expected = ['some text', 'some code', None, 'some user2a code', None, 'some user1 code']
+    self.assertEqual(expected, weaver.written)
+
+ +
+

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. -

- - - - -

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 ):
+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)

+
+

Unit test of Action Sequence class (43) =

+
+class MockAction:
+    def __init__(self) -> None:
+        self.count = 0
+    def __call__(self) -> None:
         self.count += 1
-        
-class MockWebReader( object ):
-    def __init__( self ):
-        self.count= 0
-        self.theWeb= None
-    def web( self, aWeb ):
-        self.theWeb= aWeb
+
+class MockWebReader:
+    def __init__(self) -> None:
+        self.count = 0
+        self.theWeb = None
+        self.errors = 0
+    def web(self, aWeb: "Web") -> None:
+        """Deprecated"""
+        warnings.warn("deprecated", DeprecationWarning)
+        self.theWeb = aWeb
         return self
-    def load( self ):
+    def source(self, filename: str, file: TextIO) -> str:
+        """Deprecated"""
+        warnings.warn("deprecated", DeprecationWarning)
+        self.webFileName = filename
+    def load(self, aWeb: pyweb.Web, filename: str, source: TextIO | None = None) -> None:
+        self.theWeb = aWeb
+        self.webFileName = filename
         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 ):
+
+class TestActionSequence(unittest.TestCase):
+    def setUp(self) -> None:
+        self.web = MockWeb()
+        self.a1 = MockAction()
+        self.a2 = MockAction()
+        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()
         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.assertEqual(1, c.count)
+            self.assertTrue(self.web is c.web)
+
+ +
+

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() )
+    def test_should_execute_weaving(self) -> None:
         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.assertTrue(self.web.wove is 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, )
+    def test_should_execute_tangling(self) -> None:
         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.assertTrue(self.web.tangled is self.tangler)
+
+ +
+

Unit test of TangleAction class (45). Used by: Unit Test of Action class hierarchy... (42)

+
+

Unit test of LoadAction class (46) =

+
+class TestLoadAction(unittest.TestCase):
+    def setUp(self) -> None:
+        self.web = MockWeb()
+        self.action = pyweb.LoadAction()
+        self.webReader = MockWebReader()
+        self.action.web = self.web
+        self.action.options = argparse.Namespace(
+            webReader = self.webReader,
+            webFileName="TestLoadAction.w",
+            command="@",
+            permitList = [], )
+        with open("TestLoadAction.w","w") as web:
+            pass
+    def tearDown(self) -> None:
+        try:
+            os.remove("TestLoadAction.w")
+        except IOError:
+            pass
+    def test_should_execute_loading(self) -> None:
         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

- + self.assertEqual(1, self.webReader.count) + + +
+

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. -

- - - - -

Unit Test of Application class (44) =

-

- 
-    
-

Unit Test of Application class (44). - Used by test_unit.py (1). -

- - -

Overheads and Main Script

- +It's easier to simply run the various use cases.

+

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. (45) =

-

-from __future__ import print_function
+

Unit Test overheads: imports, etc. (48) =

+
 """Unit tests."""
-import pyweb
-import unittest
+import argparse
+import io
 import logging
-import StringIO
-import string
 import os
-import time
 import re
+import string
+import time
+from typing import Any, TextIO
+import unittest
+import warnings
 
-    
-

Unit Test overheads: imports, etc. (45). - Used by test_unit.py (1). -

- - - - - -

Unit Test main (46) =

-

-
+import pyweb
+
+ +
+

Unit Test overheads: imports, etc. (48). Used by: test_unit.py (1)

+
+

Unit Test main (49) =

+
 if __name__ == "__main__":
     import sys
-    logging.basicConfig( stream=sys.stdout, level= logging.WARN )
+    logging.basicConfig(stream=sys.stdout, level=logging.WARN)
     unittest.main()
-
-    
-

Unit Test main (46). - Used by test_unit.py (1). -

+
+ +
+

Unit Test main (49). Used by: test_unit.py (1)

+
+

We run the default unittest.main() to execute the entire suite of tests.

- -

Functional Testing

-
- +
+
+

Functional Testing

-

There are three broad areas of functional testing.

- -
    -
  • Loading
  • -
  • Tanging
  • -
  • Weaving
  • + -

    There are a total of 11 test cases.

    - -

    Tests for Loading

    - +
    +

    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). - -

    - - +

    test_loader.py (50) =

    +
    +→Load Test overheads: imports, etc. (52), →(57)
    +→Load Test superclass to refactor common setup (51)
    +→Load Test error handling with a few common syntax errors (53)
    +→Load Test include processing with syntax errors (55)
    +→Load Test main program (58)
    +
    + +
    +

    test_loader.py (50).

    +

    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). -

    - - +

    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 (51) =

    +
    +class ParseTestcase(unittest.TestCase):
    +    text = ""
    +    file_name = ""
    +    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 (51). Used by: test_loader.py (50)

    +

    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.@}
    +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. (52) =

    +
    +import logging.handlers
    +
    + +
    +

    Load Test overheads: imports, etc. (52). Used by: test_loader.py (50)

    +
    +

    Load Test error handling with a few common syntax errors (53) =

    +
    +→Sample Document 1 with correct and incorrect syntax (54)
    +
    +class Test_ParseErrors(ParseTestcase):
    +    text = test1_w
    +    file_name = "test1.w"
    +    def setUp(self) -> None:
    +        super().setUp()
    +        self.logger = logging.getLogger("WebReader")
    +        self.buffer = logging.handlers.BufferingHandler(12)
    +        self.buffer.setLevel(logging.WARN)
    +        self.logger.addHandler(self.buffer)
    +        self.logger.setLevel(logging.WARN)
    +    def test_error_should_count_1(self) -> None:
    +        self.rdr.load(self.web, self.file_name, self.source)
    +        self.assertEqual(3, self.rdr.errors)
    +        messages = [r.message for r in self.buffer.buffer]
    +        self.assertEqual(
    +            ["At ('test1.w', 8): expected ('@{',), found '@o'",
    +            "Extra '@{' (possibly missing chunk name) near ('test1.w', 9)",
    +            "Extra '@{' (possibly missing chunk name) near ('test1.w', 9)"],
    +            messages
    +        )
    +    def tearDown(self) -> None:
    +        self.logger.setLevel(logging.CRITICAL)
    +        self.logger.removeHandler(self.buffer)
    +        super().tearDown()
    +
    + +
    +

    Load Test error handling with a few common syntax errors (53). Used by: test_loader.py (50)

    +
    +

    Sample Document 1 with correct and incorrect syntax (54) =

    +
    +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
    -@{ @{ @] @]
    +@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). -

    - - +
    + +
    +

    Sample Document 1 with correct and incorrect syntax (54). Used by: Load Test error handling... (53)

    +

    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 ):
    +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.

    +

    Load Test include processing with syntax errors (55) =

    +
    +→Sample Document 8 and the file it includes (56)
    +
    +class Test_IncludeParseErrors(ParseTestcase):
    +    text = test8_w
    +    file_name = "test8.w"
    +    def setUp(self) -> None:
             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). -

    - - + temp.write(test8_inc_w) + super().setUp() + self.logger = logging.getLogger("WebReader") + self.buffer = logging.handlers.BufferingHandler(12) + self.buffer.setLevel(logging.WARN) + self.logger.addHandler(self.buffer) + self.logger.setLevel(logging.WARN) + def test_error_should_count_2(self) -> None: + self.rdr.load(self.web, self.file_name, self.source) + self.assertEqual(1, self.rdr.errors) + messages = [r.message for r in self.buffer.buffer] + self.assertEqual( + ["At ('test8_inc.tmp', 4): end of input, ('@{', '@[') not found", + "Errors in included file 'test8_inc.tmp', output is incomplete."], + messages + ) + def tearDown(self) -> None: + self.logger.setLevel(logging.CRITICAL) + self.logger.removeHandler(self.buffer) + os.remove('test8_inc.tmp') + super().tearDown() +
    + +
    +

    Load Test include processing with syntax errors (55). Used by: test_loader.py (50)

    +

    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
    +be given to the included document by setUp.

    +

    Sample Document 8 and the file it includes (56) =

    +
    +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
    +@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
    +
    + +
    +

    Sample Document 8 and the file it includes (56). Used by: Load Test include... (55)

    +
    +

    <p>The overheads for a Python unittest.</p>

    +

    Load Test overheads: imports, etc. (57) +=

    +
     """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). -

    - - +import io +import types +
    + +
    +

    Load Test overheads: imports, etc. (57). Used by: test_loader.py (50)

    +

    A main program that configures logging and then runs the test.

    - - - - -

    Load Test main program (54) =

    -
    
    -
    +

    Load Test main program (58) =

    +
     if __name__ == "__main__":
         import sys
    -    logging.basicConfig( stream=sys.stdout, level= logging.WARN )
    +    logging.basicConfig(stream=sys.stdout, level=logging.WARN)
         unittest.main()
    -
    -    
    -

    Load Test main program (54). - Used by test_loader.py (47). -

    - - -

    Tests for Tangling

    - +
    + +
    +

    Load Test main program (58). Used by: test_loader.py (50)

    +
    +
    +
    +

    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). - -

    - - +

    test_tangler.py (59) =

    +
    +→Tangle Test overheads: imports, etc. (73)
    +→Tangle Test superclass to refactor common setup (60)
    +→Tangle Test semantic error 2 (61)
    +→Tangle Test semantic error 3 (63)
    +→Tangle Test semantic error 4 (65)
    +→Tangle Test semantic error 5 (67)
    +→Tangle Test semantic error 6 (69)
    +→Tangle Test include error 7 (71)
    +→Tangle Test main program (74)
    +
    + +
    +

    test_tangler.py (59).

    +

    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 ):
    +The essential test case is to load and attempt to tangle, checking the
    +exceptions raised.

    +

    Tangle Test superclass to refactor common setup (60) =

    +
    +class TangleTestcase(unittest.TestCase):
    +    text = ""
    +    file_name = ""
    +    error = ""
    +    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.tangle( self.tangler )
    +            self.rdr.load(self.web, self.file_name, self.source)
    +            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 )
    +            self.fail("Should not tangle")
    +        except pyweb.Error as e:
    +            self.assertEqual(exception_text, e.args[0])
    +    def tearDown(self) -> None:
    +        name, _ = os.path.splitext(self.file_name)
             try:
    -            os.remove( name + ".tmp" )
    +            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.@}
    +
    + +
    +

    Tangle Test superclass to refactor common setup (60). Used by: test_tangler.py (59)

    +
    +

    Tangle Test semantic error 2 (61) =

    +
    +→Sample Document 2 (62)
    +
    +class Test_SemanticError_2(TangleTestcase):
    +    text = test2_w
    +    file_name = "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 (61). Used by: test_tangler.py (59)

    +
    +

    Sample Document 2 (62) =

    +
    +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.@}
    +
    + +
    +

    Sample Document 2 (62). Used by: Tangle Test semantic error 2... (61)

    +
    +

    Tangle Test semantic error 3 (63) =

    +
    +→Sample Document 3 (64)
    +
    +class Test_SemanticError_3(TangleTestcase):
    +    text = test3_w
    +    file_name = "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 (63). Used by: test_tangler.py (59)

    +
    +

    Sample Document 3 (64) =

    +
    +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.@}
    +
    + +
    +

    Sample Document 3 (64). Used by: Tangle Test semantic error 3... (63)

    +
    +

    Tangle Test semantic error 4 (65) =

    +
    +→Sample Document 4 (66)
    +
    +class Test_SemanticError_4(TangleTestcase):
    +    text = test4_w
    +    file_name = "test4.w"
    +    def test_should_raise_noFullName(self) -> None:
    +        self.tangle_and_check_exception("No full name for 'part1...'")
    +
    + +
    +

    Tangle Test semantic error 4 (65). Used by: test_tangler.py (59)

    +
    +

    Sample Document 4 (66) =

    +
    +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= """
    +
    + +
    +

    Sample Document 4 (66). Used by: Tangle Test semantic error 4... (65)

    +
    +

    Tangle Test semantic error 5 (67) =

    +
    +→Sample Document 5 (68)
    +
    +class Test_SemanticError_5(TangleTestcase):
    +    text = test5_w
    +    file_name = "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 (67). Used by: test_tangler.py (59)

    +
    +

    Sample Document 5 (68) =

    +
    +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.@}
    +@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 )
    +
    + +
    +

    Sample Document 5 (68). Used by: Tangle Test semantic error 5... (67)

    +
    +

    Tangle Test semantic error 6 (69) =

    +
    +→Sample Document 6 (70)
    +
    +class Test_SemanticError_6(TangleTestcase):
    +    text = test6_w
    +    file_name = "test6.w"
    +    def test_should_warn(self) -> None:
    +        self.rdr.load(self.web, self.file_name, self.source)
    +        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: 
    +        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 (69). Used by: test_tangler.py (59)

    +
    +

    Sample Document 6 (70) =

    +
    +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 ):
    +
    + +
    +

    Sample Document 6 (70). Used by: Tangle Test semantic error 6... (69)

    +
    +

    Tangle Test include error 7 (71) =

    +
    +→Sample Document 7 and it's included file (72)
    +
    +class Test_IncludeError_7(TangleTestcase):
    +    text = test7_w
    +    file_name = "test7.w"
    +    def setUp(self) -> None:
             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 )
    +            temp.write(test7_inc_w)
    +        super().setUp()
    +    def test_should_include(self) -> None:
    +        self.rdr.load(self.web, self.file_name, self.source)
    +        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= """
    +        self.assertEqual(5, len(self.web.chunkSeq))
    +        self.assertEqual(test7_inc_w, self.web.chunkSeq[3].commands[0].text)
    +    def tearDown(self) -> None:
    +        os.remove('test7_inc.tmp')
    +        super().tearDown()
    +
    + +
    +

    Tangle Test include error 7 (71). Used by: test_tangler.py (59)

    +
    +

    Sample Document 7 and it's included file (72) =

    +
    +test7_w = """
     Some anonymous chunk.
    -@d title @[the title of this document, defined with @@[ and @@]@]
    -A reference to @<title@>.
    -@i test7_inc.tmp
    +@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
    +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
    +
    + +
    +

    Sample Document 7 and it's included file (72). Used by: Tangle Test include error 7... (71)

    +
    +

    Tangle Test overheads: imports, etc. (73) =

    +
     """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) =

    -
    
    -
    +import io
    +
    + +
    +

    Tangle Test overheads: imports, etc. (73). Used by: test_tangler.py (59)

    +
    +

    Tangle Test main program (74) =

    +
     if __name__ == "__main__":
         import sys
    -    logging.basicConfig( stream=sys.stdout, level= logging.WARN )
    +    logging.basicConfig(stream=sys.stdout, level=logging.WARN)
         unittest.main()
    -
    -    
    -

    Tangle Test main program (70). - Used by test_tangler.py (55). -

    - - - -

    Tests for Weaving

    - +
    + +
    +

    Tangle Test main program (74). Used by: test_tangler.py (59)

    +
    +
    +
    +

    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). - -

    - - +

    test_weaver.py (75) =

    +
    +→Weave Test overheads: imports, etc. (82)
    +→Weave Test superclass to refactor common setup (76)
    +→Weave Test references and definitions (77)
    +→Weave Test evaluation of expressions (80)
    +→Weave Test main program (83)
    +
    + +
    +

    test_weaver.py (75).

    +

    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 ):
    +

    Weave Test superclass to refactor common setup (76) =

    +
    +class WeaveTestcase(unittest.TestCase):
    +    text = ""
    +    file_name = ""
    +    error = ""
    +    def setUp(self) -> None:
    +        self.source = io.StringIO(self.text)
    +        self.web = pyweb.Web()
    +        self.rdr = pyweb.WebReader()
    +    def tangle_and_check_exception(self, exception_text: str) -> None:
             try:
    -            self.rdr.load()
    -            self.web.tangle( self.tangler )
    +            self.rdr.load(self.web, self.file_name, self.source)
    +            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 )
    +            self.fail("Should not tangle")
    +        except pyweb.Error as e:
    +            self.assertEqual(exception_text, e.args[0])
    +    def tearDown(self) -> None:
    +        name, _ = os.path.splitext(self.file_name)
             try:
    -            os.remove( name + ".html" )
    +            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
    +
    + +
    +

    Weave Test superclass to refactor common setup (76). Used by: test_weaver.py (75)

    +
    +

    Weave Test references and definitions (77) =

    +
    +→Sample Document 0 (78)
    +→Expected Output 0 (79)
    +
    +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 )
    +    def test_load_should_createChunks(self) -> None:
    +        self.rdr.load(self.web, self.file_name, self.source)
    +        self.assertEqual(3, len(self.web.chunkSeq))
    +    def test_weave_should_createFile(self) -> None:
    +        self.rdr.load(self.web, self.file_name, self.source)
    +        doc = pyweb.HTML()
    +        doc.reference_style = pyweb.SimpleReference()
    +        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>
    +            actual = source.read()
    +        self.maxDiff = None
    +        self.assertEqual(test0_expected, actual)
    +
    + +
    +

    Weave Test references and definitions (77). Used by: test_weaver.py (75)

    +
    +

    Sample Document 0 (78) =

    +
    +test0_w = """<html>
     <head>
         <link rel="StyleSheet" href="pyweb.css" type="text/css" />
     </head>
     <body>
    -@<some code@>
    +@<some code@>
     
    -@d some code 
    -@{
    -def fastExp( n, p ):
    -    r= 1
    +@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)
    +    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>
    +
    + +
    +

    Sample Document 0 (78). Used by: Weave Test references... (77)

    +
    +

    Expected Output 0 (79) =

    +
    +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 href="#pyweb1">&rarr;<em>some code</em> (1)</a>
     
     
         <a name="pyweb1"></a>
    -    <!--line number 9-->
    +    <!--line number 10-->
         <p><em>some code</em> (1)&nbsp;=</p>
    -    <code><pre>
    +    <pre><code>
     
    -def fastExp( n, p ):
    -    r= 1
    +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)
    +    return n*n*fastExp(n,p/2)
     
     for i in range(24):
         fastExp(2,i)
     
    -    </pre></code>
    +    </code></pre>
         <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
    +
    + +
    +

    Expected Output 0 (79). Used by: Weave Test references... (77)

    +
    +

    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 (80) =

    +
    +→Sample Document 9 (81)
    +
    +class TestEvaluations(WeaveTestcase):
    +    text = test9_w
         file_name = "test9.w"
    -    def test_should_evaluate( self ):
    -        doc= pyweb.HTML()
    -        self.web.weave( doc )
    +    def test_should_evaluate(self) -> None:
    +        self.rdr.load(self.web, self.file_name, self.source)
    +        doc = pyweb.HTML( )
    +        doc.reference_style = pyweb.SimpleReference()
    +        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) =

    -
    
    -
    +            actual = source.readlines()
    +        #print(actual)
    +        self.assertEqual("An anonymous chunk.\n", actual[0])
    +        self.assertTrue(actual[1].startswith("Time ="))
    +        self.assertEqual("File = ('test9.w', 3)\n", actual[2])
    +        self.assertEqual('Version = 3.1\n', actual[3])
    +        self.assertEqual(f'CWD = {os.getcwd()}\n', actual[4])
    +
    + +
    +

    Weave Test evaluation of expressions (80). Used by: test_weaver.py (75)

    +
    +

    Sample Document 9 (81) =

    +
     test9_w= """An anonymous chunk.
    -Time = @(time.asctime()@)
    -File = @(theLocation@)
    -Version = @(__version__@)
    -OS = @(os.name@)
    -CWD = @(os.getcwd()@)
    +Time = @(time.asctime()@)
    +File = @(theLocation@)
    +Version = @(__version__@)
    +CWD = @(os.path.realpath('.')@)
     """
    -
    -    
    -

    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
    +
    + +
    +

    Sample Document 9 (81). Used by: Weave Test evaluation... (80)

    +
    +

    Weave Test overheads: imports, etc. (82) =

    +
     """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) =

    -
    
    -
    +import io
    +
    + +
    +

    Weave Test overheads: imports, etc. (82). Used by: test_weaver.py (75)

    +
    +

    Weave Test main program (83) =

    +
     if __name__ == "__main__":
         import sys
    -    logging.basicConfig( stream=sys.stdout, level= logging.WARN )
    +    logging.basicConfig(stream=sys.stderr, level=logging.WARN)
         unittest.main()
    -
    -    
    -

    Weave Test main program (79). - Used by test_weaver.py (71). -

    - +
    + +
    +

    Weave Test main program (83). Used by: test_weaver.py (75)

    +
    - -

    Combined Test Script

    -
    - - - +
    +
    +

    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). - -

    - - +

    test.py (84) =

    +
    +→Combined Test overheads, imports, etc. (85)
    +→Combined Test suite which imports all other test modules (86)
    +→Combined Test command line options (87)
    +→Combined Test main script (88)
    +
    + +
    +

    test.py (84).

    +

    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
    +infrastructure.  Additionally, each of the test modules is also imported.

    +

    Combined Test overheads, imports, etc. (85) =

    +
     """Combined tests."""
    +import argparse
     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). -

    - - +import sys +
    + +
    +

    Combined Test overheads, imports, etc. (85). Used by: test.py (84)

    +

    The test suite is built from each of the individual test modules.

    - - - - -

    Combined Test suite which imports all other test modules (82) =

    -
    
    -
    +

    Combined Test suite which imports all other test modules (86) =

    +
     def suite():
    -    s= unittest.TestSuite()
    -    for m in ( test_loader, test_tangler, test_weaver, test_unit ):
    -        s.addTests( unittest.defaultTestLoader.loadTestsFromModule( m ) )
    +    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) =

    -
    
    -
    +
    + +
    +

    Combined Test suite which imports all other test modules (86). Used by: test.py (84)

    +
    +

    In order to debug failing tests, we accept some command-line +parameters to the combined testing script.

    +

    Combined Test command line options (87) =

    +
    +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(namespace=defaults)
    +    return config
    +
    + +
    +

    Combined Test command line options (87). Used by: test.py (84)

    +
    +

    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.

    +

    Combined Test main script (88) =

    +
     if __name__ == "__main__":
    -    import sys
    -    logging.basicConfig( stream=sys.stdout, level=logging.CRITICAL )
    -    tr= unittest.TextTestRunner()
    -    result= tr.run( suite() )
    +    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) )
    -
    -    
    -

    Combined Test main script (83). - Used by test.py (80). -

    + sys.exit(len(result.failures) + len(result.errors)) +
    + +
    +

    Combined Test main script (88). Used by: test.py (84)

    +
    - -

    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

    -
    +
    +

    Additional 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.
    - +

    docutils.conf (89) =

    +
    +# 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 (89).

    +
    +

    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 (90) =

    +
    +/* 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 (90).

    +
    +
    +
    +

    Indices

    +
    +

    Files

    + +++ + + + + + + + + + + + + + + + + + +
    docutils.conf:→(89)
    page-layout.css:
     →(90)
    test.py:→(84)
    test_loader.py:→(50)
    test_tangler.py:
     →(59)
    test_unit.py:→(1)
    test_weaver.py:→(75)
    +
    +
    +

    Macros

    + +++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Combined Test command line options:
     →(87)
    Combined Test main script:
     →(88)
    Combined Test overheads, imports, etc.:
     →(85)
    Combined Test suite which imports all other test modules:
     →(86)
    Expected Output 0:
     →(79)
    Load Test error handling with a few common syntax errors:
     →(53)
    Load Test include processing with syntax errors:
     →(55)
    Load Test main program:
     →(58)
    Load Test overheads:
     imports, etc.: +→(52) →(57)
    Load Test superclass to refactor common setup:
     →(51)
    Sample Document 0:
     →(78)
    Sample Document 1 with correct and incorrect syntax:
     →(54)
    Sample Document 2:
     →(62)
    Sample Document 3:
     →(64)
    Sample Document 4:
     →(66)
    Sample Document 5:
     →(68)
    Sample Document 6:
     →(70)
    Sample Document 7 and it's included file:
     →(72)
    Sample Document 8 and the file it includes:
     →(56)
    Sample Document 9:
     →(81)
    Tangle Test include error 7:
     →(71)
    Tangle Test main program:
     →(74)
    Tangle Test overheads:
     imports, etc.: +→(73)
    Tangle Test semantic error 2:
     →(61)
    Tangle Test semantic error 3:
     →(63)
    Tangle Test semantic error 4:
     →(65)
    Tangle Test semantic error 5:
     →(67)
    Tangle Test semantic error 6:
     →(69)
    Tangle Test superclass to refactor common setup:
     →(60)
    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:→(49)
    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)
    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:
     →(80)
    Weave Test main program:
     →(83)
    Weave Test overheads:
     imports, etc.: +→(82)
    Weave Test references and definitions:
     →(77)
    Weave Test superclass to refactor common setup:
     →(76)
    +
    +
    +

    User Identifiers

    +

    (None)

    +
    +
    +Created by ../pyweb.py at Fri Jun 10 10:32:05 2022.
    +

    Source pyweb_test.w modified Thu Jun 9 12:12:11 2022.

    +
    +

    pyweb.__version__ '3.1'.

    +

    Working directory '/Users/slott/Documents/Projects/py-web-tool/test'.

    +
    +
    - -
    -

    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.rst b/test/pyweb_test.rst new file mode 100644 index 0000000..af32d9a --- /dev/null +++ b/test/pyweb_test.rst @@ -0,0 +1,3351 @@ +############################################ +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|\ 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 (`49`_) + +.. + + .. 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, fileName: str) -> 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("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("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("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("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. + + +.. _`4`: +.. rubric:: Unit Test Mock Chunk class (4) = +.. parsed-literal:: + :class: code + + + class MockChunk: + def \_\_init\_\_(self, name: str, seq: int, lineNumber: int) -> None: + self.name = name + self.fullName = name + self.seq = seq + self.lineNumber = lineNumber + self.initial = True + self.commands = [] + self.referencedBy = [] + def \_\_repr\_\_(self) -> str: + return f"({self.name!r}, {self.seq!r})" + def references(self, aWeaver: pyweb.Weaver) -> list[str]: + return [(c.name, c.seq) for c in self.referencedBy] + def reference\_indent(self, aWeb: "Web", aTangler: "Tangler", amount: int) -> None: + aTangler.addIndent(amount) + def reference\_dedent(self, aWeb: "Web", aTangler: "Tangler") -> None: + aTangler.clrIndent() + def tangle(self, aWeb: "Web", aTangler: "Tangler") -> None: + aTangler.write(self.name) + +.. + + .. 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.filename = "testweaver" + self.aFileChunk = MockChunk("File", 123, 456) + self.aFileChunk.referencedBy = [ ] + self.aChunk = MockChunk("Chunk", 314, 278) + self.aChunk.referencedBy = [ self.aFileChunk ] + def tearDown(self) -> None: + import os + try: + pass #os.remove("testweaver.rst") + 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) + + def test\_weaver\_should\_codeBegin(self) -> None: + self.weaver.open(self.filename) + 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() + with open("testweaver.rst", "r") as result: + txt = result.read() + 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.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.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.filename) + self.weaver.xrefHead( ) + self.weaver.xrefLine("Chunk", [ ("Container", 123) ]) + self.weaver.xrefFoot( ) + #self.weaver.fileEnd(self.aFileChunk) # Why? + self.weaver.close() + with open("testweaver.rst", "r") as result: + txt = result.read() + self.assertEqual("\\n:Chunk:\\n \|srarr\|\\\\ (\`('Container', 123)\`\_)\\n\\n", txt) + + def test\_weaver\_should\_xref\_def(self) -> None: + self.weaver.open(self.filename) + 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() + with open("testweaver.rst", "r") as result: + txt = result.read() + 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.filename = "testweaver" + self.aFileChunk = MockChunk("File", 123, 456) + self.aFileChunk.referencedBy = [ ] + self.aChunk = MockChunk("Chunk", 314, 278) + self.aChunk.referencedBy = [ self.aFileChunk, ] + def tearDown(self) -> None: + import os + try: + os.remove("testweaver.tex") + 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) + self.assertEqual("\\n \\\\footnotesize\\n Used by:\\n \\\\begin{list}{}{}\\n \\n \\\\item Code example File (123) (Sect. \\\\ref{pyweb123}, p. \\\\pageref{pyweb123})\\n\\n \\\\end{list}\\n \\\\normalsize\\n", result) + result = self.weaver.referenceTo("Chunk", 314) + self.assertEqual("$\\\\triangleright$ Code Example Chunk (314)", result) + +.. + + .. 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.filename = "testweaver" + self.aFileChunk = MockChunk("File", 123, 456) + self.aFileChunk.referencedBy = [] + self.aChunk = MockChunk("Chunk", 314, 278) + self.aChunk.referencedBy = [ self.aFileChunk, ] + def tearDown(self) -> None: + import os + try: + os.remove("testweaver.html") + 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) + + +.. + + .. 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 just a template change. + + **To Do** 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.filename = "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: + import os + try: + os.remove("testtangler.code") + except OSError: + 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.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.code", "r") as result: + txt = result.read() + 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. + +In order to be sure that the timestamps really have changed, we either +need to wait for a full second to elapse or we need to mock the various +``os`` and ``filecmp`` features used by ``TanglerMake``. + + + + +.. _`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.filename = "testtangler.code" + 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.time\_original = os.path.getmtime(self.filename) + self.original = os.lstat(self.filename) + #time.sleep(0.75) # Alternative to assure timestamps must be different + + def tearDown(self) -> None: + import os + try: + os.remove("testtangler.code") + except OSError: + pass + + def test\_same\_should\_leave(self) -> None: + 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.assertTrue(os.path.samestat(self.original, os.lstat(self.filename))) + #self.assertEqual(self.time\_original, os.path.getmtime(self.filename)) + + def test\_different\_should\_update(self) -> None: + 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.assertFalse(os.path.samestat(self.original, os.lstat(self.filename))) + #self.assertNotEqual(self.time\_original, os.path.getmtime(self.filename)) + +.. + + .. 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 + + + class MockCommand: + def \_\_init\_\_(self) -> None: + self.lineNumber = 314 + def startswith(self, text: str) -> bool: + return 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 + + + class MockWeb: + def \_\_init\_\_(self) -> None: + self.chunks = [] + self.wove = None + self.tangled = None + def add(self, aChunk: pyweb.Chunk) -> None: + self.chunks.append(aChunk) + def addNamed(self, aChunk: pyweb.Chunk) -> None: + self.chunks.append(aChunk) + def addOutput(self, aChunk: pyweb.Chunk) -> None: + self.chunks.append(aChunk) + def fullNameFor(self, name: str) -> str: + return name + def fileXref(self) -> dict[str, list[int]]: + return {'file': [1,2,3]} + def chunkXref(self) -> dict[str, list[int]]: + return {'chunk': [4,5,6]} + def userNamesXref(self) -> dict[str, list[int]]: + return {'name': (7, [8,9,10])} + def getchunk(self, name: str) -> list[pyweb.Chunk]: + return [MockChunk(name, 1, 314)] + def createUsedBy(self) -> None: + pass + def weaveChunk(self, name, weaver) -> None: + weaver.write(name) + def weave(self, weaver) -> None: + self.wove = weaver + def tangle(self, tangler) -> None: + self.tangled = tangler + +.. + + .. class:: small + + |loz| *Unit Test of Chunk superclass (13)*. Used by: Unit Test of Chunk class hierarchy... (`11`_) + + +A MockWeaver or MockTangle can process a Chunk. + + +.. _`14`: +.. rubric:: Unit Test of Chunk superclass (14) += +.. parsed-literal:: + :class: code + + + class MockWeaver: + def \_\_init\_\_(self) -> None: + self.begin\_chunk = [] + self.end\_chunk = [] + self.written = [] + self.code\_indent = None + def quote(self, text: str) -> str: + return text.replace("&", "&") # token quoting + def docBegin(self, aChunk: pyweb.Chunk) -> None: + self.begin\_chunk.append(aChunk) + def write(self, text: str) -> None: + self.written.append(text) + def docEnd(self, aChunk: pyweb.Chunk) -> None: + self.end\_chunk.append(aChunk) + def codeBegin(self, aChunk: pyweb.Chunk) -> None: + self.begin\_chunk.append(aChunk) + def codeBlock(self, text: str) -> None: + self.written.append(text) + def codeEnd(self, aChunk: pyweb.Chunk) -> None: + self.end\_chunk.append(aChunk) + def fileBegin(self, aChunk: pyweb.Chunk) -> None: + self.begin\_chunk.append(aChunk) + def fileEnd(self, aChunk: pyweb.Chunk) -> None: + self.end\_chunk.append(aChunk) + def addIndent(self, increment=0): + pass + def setIndent(self, fixed: int \| None=None, command: str \| None=None) -> None: + self.indent = fixed + def addIndent(self, increment: int = 0) -> None: + self.indent = increment + def clrIndent(self) -> None: + pass + def xrefHead(self) -> None: + pass + def xrefLine(self, name: str, refList: list[int]) -> None: + self.written.append(f"{name} {refList}") + def xrefDefLine(self, name: str, defn: int, refList: list[int]) -> None: + self.written.append(f"{name} {defn} {refList}") + def xrefFoot(self) -> None: + pass + def referenceTo(self, name: str, seq: int) -> None: + pass + def open(self, aFile: str) -> "MockWeaver": + return self + def close(self) -> None: + pass + def \_\_enter\_\_(self) -> "MockWeaver": + return self + def \_\_exit\_\_(self, \*args: Any) -> bool: + return False + + class MockTangler(MockWeaver): + def \_\_init\_\_(self) -> None: + super().\_\_init\_\_() + self.context = [0] + def addIndent(self, amount: int) -> None: + pass + +.. + + .. 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) ) + cmd2 = MockCommand() + self.theChunk.append(cmd2) + self.assertEqual(2, len(self.theChunk.commands) ) + + 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) ) + + def test\_append\_to\_web\_should\_work(self) -> None: + web = MockWeb() + self.theChunk.webAdd(web) + self.assertEqual(1, len(web.chunks)) + +.. + + .. 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\_should\_work(self) -> None: + wvr = MockWeaver() + web = MockWeb() + self.theChunk.appendText("this chunk has very & many words") + self.theChunk.weave(web, wvr) + self.assertEqual(1, len(wvr.begin\_chunk)) + self.assertTrue(wvr.begin\_chunk[0] is self.theChunk) + self.assertEqual(1, len(wvr.end\_chunk)) + self.assertTrue(wvr.end\_chunk[0] is self.theChunk) + self.assertEqual("this chunk has very & many words", "".join( wvr.written)) + + 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\_to\_web\_should\_work(self) -> None: + web = MockWeb() + self.theChunk.webAdd(web) + self.assertEqual(1, len(web.chunks)) + + def test\_weave\_should\_work(self) -> None: + wvr = MockWeaver() + web = MockWeb() + self.theChunk.weave(web, wvr) + self.assertEqual(1, len(wvr.begin\_chunk)) + self.assertTrue(wvr.begin\_chunk[0] is self.theChunk) + self.assertEqual(1, len(wvr.end\_chunk)) + self.assertTrue(wvr.end\_chunk[0] is self.theChunk) + self.assertEqual("the words & text of this Chunk", "".join( wvr.written)) + + def test\_tangle\_should\_work(self) -> None: + tnglr = MockTangler() + web = MockWeb() + self.theChunk.tangle(web, tnglr) + self.assertEqual(1, len(tnglr.begin\_chunk)) + self.assertTrue(tnglr.begin\_chunk[0] is self.theChunk) + self.assertEqual(1, len(tnglr.end\_chunk)) + self.assertTrue(tnglr.end\_chunk[0] is self.theChunk) + self.assertEqual("the words & text of this Chunk", "".join( tnglr.written)) + +.. + + .. 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("Some 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(1, len(tnglr.begin\_chunk)) + self.assertTrue(tnglr.begin\_chunk[0] is self.theChunk) + self.assertEqual(1, len(tnglr.end\_chunk)) + self.assertTrue(tnglr.end\_chunk[0] is self.theChunk) + self.assertEqual("the words & text of this Chunk", "".join( tnglr.written)) + +.. + + .. 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\_to\_web\_should\_work(self) -> None: + web = MockWeb() + self.theChunk.webAdd(web) + self.assertEqual(1, len(web.chunks)) + + def test\_weave\_should\_work(self) -> None: + wvr = MockWeaver() + web = MockWeb() + self.theChunk.weave(web, wvr) + self.assertEqual(1, len(wvr.begin\_chunk)) + self.assertTrue(wvr.begin\_chunk[0] is self.theChunk) + self.assertEqual(1, len(wvr.end\_chunk)) + self.assertTrue(wvr.end\_chunk[0] is self.theChunk) + self.assertEqual("the words & text of this Chunk", "".join( wvr.written)) + + def test\_tangle\_should\_work(self) -> None: + tnglr = MockTangler() + web = MockWeb() + self.theChunk.tangle(web, tnglr) + self.assertEqual(1, len(tnglr.begin\_chunk)) + self.assertTrue(tnglr.begin\_chunk[0] is self.theChunk) + self.assertEqual(1, len(tnglr.end\_chunk)) + self.assertTrue(tnglr.end\_chunk[0] is self.theChunk) + self.assertEqual("the words & text of this Chunk", "".join( tnglr.written)) + +.. + + .. 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("Some text & words in the document\\n ", "".join( wvr.written)) + def test\_tangle\_should\_work(self) -> None: + tnglr = MockTangler() + web = MockWeb() + self.cmd.tangle(web, tnglr) + self.assertEqual("Some text & words in the document\\n ", "".join( tnglr.written)) + +.. + + .. 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("Some text & words in the document\\n ", "".join( wvr.written)) + def test\_tangle\_should\_work(self) -> None: + tnglr = MockTangler() + web = MockWeb() + self.cmd.tangle(web, tnglr) + self.assertEqual("Some text & words in the document\\n ", "".join( tnglr.written)) + +.. + + .. 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("file [1, 2, 3]", "".join( wvr.written)) + 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("chunk [4, 5, 6]", "".join( wvr.written)) + 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("name 7 [8, 9, 10]", "".join( wvr.written)) + 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("Some Name", "".join( wvr.written)) + def test\_tangle\_should\_work(self) -> None: + tnglr = MockTangler() + web = MockWeb() + web.add(self.chunk) + self.cmd.tangle(web, tnglr) + self.assertEqual("Some Name", "".join( tnglr.written)) + +.. + + .. 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.webFileName = "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(3, len(tangler.written)) + self.assertEqual(['some code', 'some user2a code', 'some user1 code'], tangler.written) + +.. + + .. 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(6, len(weaver.written)) + expected = ['some text', 'some code', None, 'some user2a code', None, 'some user1 code'] + self.assertEqual(expected, weaver.written) + +.. + + .. 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`_) + + + +.. _`43`: +.. rubric:: Unit test of Action Sequence class (43) = +.. parsed-literal:: + :class: code + + + class MockAction: + def \_\_init\_\_(self) -> None: + self.count = 0 + def \_\_call\_\_(self) -> None: + self.count += 1 + + class MockWebReader: + def \_\_init\_\_(self) -> None: + self.count = 0 + self.theWeb = None + self.errors = 0 + def web(self, aWeb: "Web") -> None: + """Deprecated""" + warnings.warn("deprecated", DeprecationWarning) + self.theWeb = aWeb + return self + def source(self, filename: str, file: TextIO) -> str: + """Deprecated""" + warnings.warn("deprecated", DeprecationWarning) + self.webFileName = filename + def load(self, aWeb: pyweb.Web, filename: str, source: TextIO \| None = None) -> None: + self.theWeb = aWeb + self.webFileName = filename + self.count += 1 + + class TestActionSequence(unittest.TestCase): + def setUp(self) -> None: + self.web = MockWeb() + self.a1 = MockAction() + self.a2 = MockAction() + 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() + for c in self.action.opSequence: + self.assertEqual(1, c.count) + self.assertTrue(self.web is c.web) + +.. + + .. 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() ) + def test\_should\_execute\_weaving(self) -> None: + self.action() + self.assertTrue(self.web.wove is 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, ) + def test\_should\_execute\_tangling(self) -> None: + self.action() + self.assertTrue(self.web.tangled is self.tangler) + +.. + + .. class:: small + + |loz| *Unit test of TangleAction class (45)*. Used by: Unit Test of Action class hierarchy... (`42`_) + + + +.. _`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 = MockWebReader() + self.action.web = self.web + self.action.options = argparse.Namespace( + webReader = self.webReader, + webFileName="TestLoadAction.w", + command="@", + permitList = [], ) + with open("TestLoadAction.w","w") as web: + pass + def tearDown(self) -> None: + try: + os.remove("TestLoadAction.w") + except IOError: + pass + def test\_should\_execute\_loading(self) -> None: + self.action() + self.assertEqual(1, self.webReader.count) + +.. + + .. 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. + + +.. _`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 + import re + import string + import time + from typing import Any, TextIO + import unittest + import warnings + + import pyweb + +.. + + .. class:: small + + |loz| *Unit Test overheads: imports, etc. (48)*. Used by: test_unit.py (`1`_) + + + +.. _`49`: +.. rubric:: Unit Test main (49) = +.. parsed-literal:: + :class: code + + + if \_\_name\_\_ == "\_\_main\_\_": + import sys + logging.basicConfig(stream=sys.stdout, level=logging.WARN) + unittest.main() + +.. + + .. class:: small + + |loz| *Unit Test main (49)*. 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. + + +.. _`50`: +.. rubric:: test_loader.py (50) = +.. parsed-literal:: + :class: code + + |srarr|\ Load Test overheads: imports, etc. (`52`_), |srarr|\ (`57`_) + |srarr|\ Load Test superclass to refactor common setup (`51`_) + |srarr|\ Load Test error handling with a few common syntax errors (`53`_) + |srarr|\ Load Test include processing with syntax errors (`55`_) + |srarr|\ Load Test main program (`58`_) + +.. + + .. class:: small + + |loz| *test_loader.py (50)*. + + +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. + + +.. _`51`: +.. rubric:: Load Test superclass to refactor common setup (51) = +.. parsed-literal:: + :class: code + + + class ParseTestcase(unittest.TestCase): + text = "" + file\_name = "" + 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 (51)*. Used by: test_loader.py (`50`_) + + +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. + + +.. _`52`: +.. rubric:: Load Test overheads: imports, etc. (52) = +.. parsed-literal:: + :class: code + + + import logging.handlers + +.. + + .. class:: small + + |loz| *Load Test overheads: imports, etc. (52)*. Used by: test_loader.py (`50`_) + + + +.. _`53`: +.. rubric:: Load Test error handling with a few common syntax errors (53) = +.. parsed-literal:: + :class: code + + + |srarr|\ Sample Document 1 with correct and incorrect syntax (`54`_) + + class Test\_ParseErrors(ParseTestcase): + text = test1\_w + file\_name = "test1.w" + def setUp(self) -> None: + super().setUp() + self.logger = logging.getLogger("WebReader") + self.buffer = logging.handlers.BufferingHandler(12) + self.buffer.setLevel(logging.WARN) + self.logger.addHandler(self.buffer) + self.logger.setLevel(logging.WARN) + def test\_error\_should\_count\_1(self) -> None: + self.rdr.load(self.web, self.file\_name, self.source) + self.assertEqual(3, self.rdr.errors) + messages = [r.message for r in self.buffer.buffer] + self.assertEqual( + ["At ('test1.w', 8): expected ('@{',), found '@o'", + "Extra '@{' (possibly missing chunk name) near ('test1.w', 9)", + "Extra '@{' (possibly missing chunk name) near ('test1.w', 9)"], + messages + ) + def tearDown(self) -> None: + self.logger.setLevel(logging.CRITICAL) + self.logger.removeHandler(self.buffer) + super().tearDown() + + +.. + + .. class:: small + + |loz| *Load Test error handling with a few common syntax errors (53)*. Used by: test_loader.py (`50`_) + + + +.. _`54`: +.. rubric:: Sample Document 1 with correct and incorrect syntax (54) = +.. 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 (54)*. Used by: Load Test error handling... (`53`_) + + +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. + + +.. _`55`: +.. rubric:: Load Test include processing with syntax errors (55) = +.. parsed-literal:: + :class: code + + + |srarr|\ Sample Document 8 and the file it includes (`56`_) + + class Test\_IncludeParseErrors(ParseTestcase): + text = test8\_w + file\_name = "test8.w" + def setUp(self) -> None: + with open('test8\_inc.tmp','w') as temp: + temp.write(test8\_inc\_w) + super().setUp() + self.logger = logging.getLogger("WebReader") + self.buffer = logging.handlers.BufferingHandler(12) + self.buffer.setLevel(logging.WARN) + self.logger.addHandler(self.buffer) + self.logger.setLevel(logging.WARN) + def test\_error\_should\_count\_2(self) -> None: + self.rdr.load(self.web, self.file\_name, self.source) + self.assertEqual(1, self.rdr.errors) + messages = [r.message for r in self.buffer.buffer] + self.assertEqual( + ["At ('test8\_inc.tmp', 4): end of input, ('@{', '@[') not found", + "Errors in included file 'test8\_inc.tmp', output is incomplete."], + messages + ) + def tearDown(self) -> None: + self.logger.setLevel(logging.CRITICAL) + self.logger.removeHandler(self.buffer) + os.remove('test8\_inc.tmp') + super().tearDown() + +.. + + .. class:: small + + |loz| *Load Test include processing with syntax errors (55)*. Used by: test_loader.py (`50`_) + + +The sample document must reference the correct name that will +be given to the included document by ``setUp``. + + +.. _`56`: +.. rubric:: Sample Document 8 and the file it includes (56) = +.. 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 (56)*. Used by: Load Test include... (`55`_) + + +

    The overheads for a Python unittest.

    + + +.. _`57`: +.. rubric:: Load Test overheads: imports, etc. (57) += +.. parsed-literal:: + :class: code + + + """Loader and parsing tests.""" + import pyweb + import unittest + import logging + import os + import io + import types + +.. + + .. class:: small + + |loz| *Load Test overheads: imports, etc. (57)*. Used by: test_loader.py (`50`_) + + +A main program that configures logging and then runs the test. + + +.. _`58`: +.. rubric:: Load Test main program (58) = +.. parsed-literal:: + :class: code + + + if \_\_name\_\_ == "\_\_main\_\_": + import sys + logging.basicConfig(stream=sys.stdout, level=logging.WARN) + unittest.main() + +.. + + .. class:: small + + |loz| *Load Test main program (58)*. Used by: test_loader.py (`50`_) + + +Tests for Tangling +------------------ + +We need to be able to tangle a web. + + +.. _`59`: +.. rubric:: test_tangler.py (59) = +.. parsed-literal:: + :class: code + + |srarr|\ Tangle Test overheads: imports, etc. (`73`_) + |srarr|\ Tangle Test superclass to refactor common setup (`60`_) + |srarr|\ Tangle Test semantic error 2 (`61`_) + |srarr|\ Tangle Test semantic error 3 (`63`_) + |srarr|\ Tangle Test semantic error 4 (`65`_) + |srarr|\ Tangle Test semantic error 5 (`67`_) + |srarr|\ Tangle Test semantic error 6 (`69`_) + |srarr|\ Tangle Test include error 7 (`71`_) + |srarr|\ Tangle Test main program (`74`_) + +.. + + .. class:: small + + |loz| *test_tangler.py (59)*. + + +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. + + + +.. _`60`: +.. rubric:: Tangle Test superclass to refactor common setup (60) = +.. parsed-literal:: + :class: code + + + class TangleTestcase(unittest.TestCase): + text = "" + file\_name = "" + error = "" + 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\_name, 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: + name, \_ = os.path.splitext(self.file\_name) + try: + os.remove(name + ".tmp") + except OSError: + pass + +.. + + .. class:: small + + |loz| *Tangle Test superclass to refactor common setup (60)*. Used by: test_tangler.py (`59`_) + + + +.. _`61`: +.. rubric:: Tangle Test semantic error 2 (61) = +.. parsed-literal:: + :class: code + + + |srarr|\ Sample Document 2 (`62`_) + + class Test\_SemanticError\_2(TangleTestcase): + text = test2\_w + file\_name = "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 (61)*. Used by: test_tangler.py (`59`_) + + + +.. _`62`: +.. rubric:: Sample Document 2 (62) = +.. 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 (62)*. Used by: Tangle Test semantic error 2... (`61`_) + + + +.. _`63`: +.. rubric:: Tangle Test semantic error 3 (63) = +.. parsed-literal:: + :class: code + + + |srarr|\ Sample Document 3 (`64`_) + + class Test\_SemanticError\_3(TangleTestcase): + text = test3\_w + file\_name = "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 (63)*. Used by: test_tangler.py (`59`_) + + + +.. _`64`: +.. rubric:: Sample Document 3 (64) = +.. 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 (64)*. Used by: Tangle Test semantic error 3... (`63`_) + + + + +.. _`65`: +.. rubric:: Tangle Test semantic error 4 (65) = +.. parsed-literal:: + :class: code + + + |srarr|\ Sample Document 4 (`66`_) + + class Test\_SemanticError\_4(TangleTestcase): + text = test4\_w + file\_name = "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 (65)*. Used by: test_tangler.py (`59`_) + + + +.. _`66`: +.. rubric:: Sample Document 4 (66) = +.. 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 (66)*. Used by: Tangle Test semantic error 4... (`65`_) + + + +.. _`67`: +.. rubric:: Tangle Test semantic error 5 (67) = +.. parsed-literal:: + :class: code + + + |srarr|\ Sample Document 5 (`68`_) + + class Test\_SemanticError\_5(TangleTestcase): + text = test5\_w + file\_name = "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 (67)*. Used by: test_tangler.py (`59`_) + + + +.. _`68`: +.. rubric:: Sample Document 5 (68) = +.. 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 (68)*. Used by: Tangle Test semantic error 5... (`67`_) + + + +.. _`69`: +.. rubric:: Tangle Test semantic error 6 (69) = +.. parsed-literal:: + :class: code + + + |srarr|\ Sample Document 6 (`70`_) + + class Test\_SemanticError\_6(TangleTestcase): + text = test6\_w + file\_name = "test6.w" + def test\_should\_warn(self) -> None: + self.rdr.load(self.web, self.file\_name, 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 (69)*. Used by: test_tangler.py (`59`_) + + + +.. _`70`: +.. rubric:: Sample Document 6 (70) = +.. 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 (70)*. Used by: Tangle Test semantic error 6... (`69`_) + + + +.. _`71`: +.. rubric:: Tangle Test include error 7 (71) = +.. parsed-literal:: + :class: code + + + |srarr|\ Sample Document 7 and it's included file (`72`_) + + class Test\_IncludeError\_7(TangleTestcase): + text = test7\_w + file\_name = "test7.w" + def setUp(self) -> None: + with open('test7\_inc.tmp','w') as temp: + temp.write(test7\_inc\_w) + super().setUp() + def test\_should\_include(self) -> None: + self.rdr.load(self.web, self.file\_name, 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: + os.remove('test7\_inc.tmp') + super().tearDown() + +.. + + .. class:: small + + |loz| *Tangle Test include error 7 (71)*. Used by: test_tangler.py (`59`_) + + + +.. _`72`: +.. rubric:: Sample Document 7 and it's included file (72) = +.. 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 (72)*. Used by: Tangle Test include error 7... (`71`_) + + + +.. _`73`: +.. rubric:: Tangle Test overheads: imports, etc. (73) = +.. parsed-literal:: + :class: code + + + """Tangler tests exercise various semantic features.""" + import pyweb + import unittest + import logging + import os + import io + +.. + + .. class:: small + + |loz| *Tangle Test overheads: imports, etc. (73)*. Used by: test_tangler.py (`59`_) + + + +.. _`74`: +.. rubric:: Tangle Test main program (74) = +.. 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 (74)*. Used by: test_tangler.py (`59`_) + + + +Tests for Weaving +----------------- + +We need to be able to weave a document from one or more source files. + + +.. _`75`: +.. rubric:: test_weaver.py (75) = +.. parsed-literal:: + :class: code + + |srarr|\ Weave Test overheads: imports, etc. (`82`_) + |srarr|\ Weave Test superclass to refactor common setup (`76`_) + |srarr|\ Weave Test references and definitions (`77`_) + |srarr|\ Weave Test evaluation of expressions (`80`_) + |srarr|\ Weave Test main program (`83`_) + +.. + + .. class:: small + + |loz| *test_weaver.py (75)*. + + +Weaving test cases have a common setup shown in this superclass. + + +.. _`76`: +.. rubric:: Weave Test superclass to refactor common setup (76) = +.. parsed-literal:: + :class: code + + + class WeaveTestcase(unittest.TestCase): + text = "" + file\_name = "" + error = "" + def setUp(self) -> None: + self.source = io.StringIO(self.text) + self.web = pyweb.Web() + self.rdr = pyweb.WebReader() + def tangle\_and\_check\_exception(self, exception\_text: str) -> None: + try: + self.rdr.load(self.web, self.file\_name, 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: + name, \_ = os.path.splitext(self.file\_name) + try: + os.remove(name + ".html") + except OSError: + pass + +.. + + .. class:: small + + |loz| *Weave Test superclass to refactor common setup (76)*. Used by: test_weaver.py (`75`_) + + + +.. _`77`: +.. rubric:: Weave Test references and definitions (77) = +.. parsed-literal:: + :class: code + + + |srarr|\ Sample Document 0 (`78`_) + |srarr|\ Expected Output 0 (`79`_) + + class Test\_RefDefWeave(WeaveTestcase): + text = test0\_w + file\_name = "test0.w" + def test\_load\_should\_createChunks(self) -> None: + self.rdr.load(self.web, self.file\_name, self.source) + self.assertEqual(3, len(self.web.chunkSeq)) + def test\_weave\_should\_createFile(self) -> None: + self.rdr.load(self.web, self.file\_name, self.source) + doc = pyweb.HTML() + doc.reference\_style = pyweb.SimpleReference() + self.web.weave(doc) + with open("test0.html","r") as source: + actual = source.read() + self.maxDiff = None + self.assertEqual(test0\_expected, actual) + + +.. + + .. class:: small + + |loz| *Weave Test references and definitions (77)*. Used by: test_weaver.py (`75`_) + + + +.. _`78`: +.. rubric:: Sample Document 0 (78) = +.. 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 (78)*. Used by: Weave Test references... (`77`_) + + + +.. _`79`: +.. rubric:: Expected Output 0 (79) = +.. 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 (79)*. Used by: Weave Test references... (`77`_) + + +Note that this really requires a mocked ``time`` module in order +to properly provide a consistent output from ``time.asctime()``. + + +.. _`80`: +.. rubric:: Weave Test evaluation of expressions (80) = +.. parsed-literal:: + :class: code + + + |srarr|\ Sample Document 9 (`81`_) + + class TestEvaluations(WeaveTestcase): + text = test9\_w + file\_name = "test9.w" + def test\_should\_evaluate(self) -> None: + self.rdr.load(self.web, self.file\_name, self.source) + doc = pyweb.HTML( ) + doc.reference\_style = pyweb.SimpleReference() + self.web.weave(doc) + with open("test9.html","r") as source: + actual = source.readlines() + #print(actual) + self.assertEqual("An anonymous chunk.\\n", actual[0]) + self.assertTrue(actual[1].startswith("Time =")) + self.assertEqual("File = ('test9.w', 3)\\n", actual[2]) + self.assertEqual('Version = 3.1\\n', actual[3]) + self.assertEqual(f'CWD = {os.getcwd()}\\n', actual[4]) + +.. + + .. class:: small + + |loz| *Weave Test evaluation of expressions (80)*. Used by: test_weaver.py (`75`_) + + + +.. _`81`: +.. rubric:: Sample Document 9 (81) = +.. 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 (81)*. Used by: Weave Test evaluation... (`80`_) + + + +.. _`82`: +.. rubric:: Weave Test overheads: imports, etc. (82) = +.. parsed-literal:: + :class: code + + + """Weaver tests exercise various weaving features.""" + import pyweb + import unittest + import logging + import os + import string + import io + +.. + + .. class:: small + + |loz| *Weave Test overheads: imports, etc. (82)*. Used by: test_weaver.py (`75`_) + + + +.. _`83`: +.. rubric:: Weave Test main program (83) = +.. parsed-literal:: + :class: code + + + if \_\_name\_\_ == "\_\_main\_\_": + import sys + logging.basicConfig(stream=sys.stderr, level=logging.WARN) + unittest.main() + +.. + + .. class:: small + + |loz| *Weave Test main program (83)*. Used by: test_weaver.py (`75`_) + + + +Combined Test Script +===================== + +.. test/combined.w + +The combined test script runs all tests in all test modules. + + +.. _`84`: +.. rubric:: test.py (84) = +.. parsed-literal:: + :class: code + + |srarr|\ Combined Test overheads, imports, etc. (`85`_) + |srarr|\ Combined Test suite which imports all other test modules (`86`_) + |srarr|\ Combined Test command line options (`87`_) + |srarr|\ Combined Test main script (`88`_) + +.. + + .. class:: small + + |loz| *test.py (84)*. + + +The overheads import unittest and logging, because those are essential +infrastructure. Additionally, each of the test modules is also imported. + + +.. _`85`: +.. rubric:: Combined Test overheads, imports, etc. (85) = +.. parsed-literal:: + :class: code + + """Combined tests.""" + import argparse + import unittest + import test\_loader + import test\_tangler + import test\_weaver + import test\_unit + import logging + import sys + + +.. + + .. class:: small + + |loz| *Combined Test overheads, imports, etc. (85)*. Used by: test.py (`84`_) + + +The test suite is built from each of the individual test modules. + + +.. _`86`: +.. rubric:: Combined Test suite which imports all other test modules (86) = +.. parsed-literal:: + :class: code + + + def suite(): + s = unittest.TestSuite() + for m in (test\_loader, test\_tangler, test\_weaver, test\_unit): + s.addTests(unittest.defaultTestLoader.loadTestsFromModule(m)) + return s + +.. + + .. class:: small + + |loz| *Combined Test suite which imports all other test modules (86)*. Used by: test.py (`84`_) + + +In order to debug failing tests, we accept some command-line +parameters to the combined testing script. + + +.. _`87`: +.. rubric:: Combined Test command line options (87) = +.. parsed-literal:: + :class: code + + + 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(namespace=defaults) + return config + +.. + + .. class:: small + + |loz| *Combined Test command line options (87)*. Used by: test.py (`84`_) + + +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. + + + +.. _`88`: +.. rubric:: Combined Test main script (88) = +.. parsed-literal:: + :class: code + + + 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)) + +.. + + .. class:: small + + |loz| *Combined Test main script (88)*. Used by: test.py (`84`_) + + + +Additional 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. + + +.. _`89`: +.. rubric:: docutils.conf (89) = +.. 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 (89)*. + + +``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. + + +.. _`90`: +.. rubric:: page-layout.css (90) = +.. 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 (90)*. + + +Indices +======= + +Files +----- + + +:docutils.conf: + |srarr|\ (`89`_) +:page-layout.css: + |srarr|\ (`90`_) +:test.py: + |srarr|\ (`84`_) +:test_loader.py: + |srarr|\ (`50`_) +:test_tangler.py: + |srarr|\ (`59`_) +:test_unit.py: + |srarr|\ (`1`_) +:test_weaver.py: + |srarr|\ (`75`_) + + + +Macros +------ + + +:Combined Test command line options: + |srarr|\ (`87`_) +:Combined Test main script: + |srarr|\ (`88`_) +:Combined Test overheads, imports, etc.: + |srarr|\ (`85`_) +:Combined Test suite which imports all other test modules: + |srarr|\ (`86`_) +:Expected Output 0: + |srarr|\ (`79`_) +:Load Test error handling with a few common syntax errors: + |srarr|\ (`53`_) +:Load Test include processing with syntax errors: + |srarr|\ (`55`_) +:Load Test main program: + |srarr|\ (`58`_) +:Load Test overheads: imports, etc.: + |srarr|\ (`52`_) |srarr|\ (`57`_) +:Load Test superclass to refactor common setup: + |srarr|\ (`51`_) +:Sample Document 0: + |srarr|\ (`78`_) +:Sample Document 1 with correct and incorrect syntax: + |srarr|\ (`54`_) +:Sample Document 2: + |srarr|\ (`62`_) +:Sample Document 3: + |srarr|\ (`64`_) +:Sample Document 4: + |srarr|\ (`66`_) +:Sample Document 5: + |srarr|\ (`68`_) +:Sample Document 6: + |srarr|\ (`70`_) +:Sample Document 7 and it's included file: + |srarr|\ (`72`_) +:Sample Document 8 and the file it includes: + |srarr|\ (`56`_) +:Sample Document 9: + |srarr|\ (`81`_) +:Tangle Test include error 7: + |srarr|\ (`71`_) +:Tangle Test main program: + |srarr|\ (`74`_) +:Tangle Test overheads: imports, etc.: + |srarr|\ (`73`_) +:Tangle Test semantic error 2: + |srarr|\ (`61`_) +:Tangle Test semantic error 3: + |srarr|\ (`63`_) +:Tangle Test semantic error 4: + |srarr|\ (`65`_) +:Tangle Test semantic error 5: + |srarr|\ (`67`_) +:Tangle Test semantic error 6: + |srarr|\ (`69`_) +:Tangle Test superclass to refactor common setup: + |srarr|\ (`60`_) +: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|\ (`49`_) +: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`_) +: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|\ (`80`_) +:Weave Test main program: + |srarr|\ (`83`_) +:Weave Test overheads: imports, etc.: + |srarr|\ (`82`_) +:Weave Test references and definitions: + |srarr|\ (`77`_) +:Weave Test superclass to refactor common setup: + |srarr|\ (`76`_) + + + +User Identifiers +---------------- + +(None) + + +---------- + +.. class:: small + + Created by ../pyweb.py at Fri Jun 10 10:32:05 2022. + + Source pyweb_test.w modified Thu Jun 9 12:12:11 2022. + + pyweb.__version__ '3.1'. + + Working directory '/Users/slott/Documents/Projects/py-web-tool/test'. diff --git a/test/pyweb_test.w b/test/pyweb_test.w index 746c167..57045c9 100644 --- a/test/pyweb_test.w +++ b/test/pyweb_test.w @@ -1,64 +1,93 @@ - - - - - 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

    -
    +############################################ +pyWeb Literate Programming 3.1 - Test Suite +############################################ + + +================================================= +Yet Another Literate Programming Tool +================================================= + +.. include:: +.. include:: + +.. contents:: + + @i intro.w -
    -

    Unit Testing

    -
    @i unit.w -
    -

    Functional Testing

    -
    @i func.w -
    -

    Combined Test Script

    -
    @i combined.w -
    -

    Indices

    -
    -

    Files

    +Additional 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. + +@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

    + +Macros +------ + @m -

    User Identifiers

    + +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('.')@)'.

    +---------- + +.. 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/test/test.py b/test/test.py index a48e749..ed5f7c6 100644 --- a/test/test.py +++ b/test/test.py @@ -1,25 +1,45 @@ -from __future__ import print_function """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 ) ) + 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(namespace=defaults) + return config + + if __name__ == "__main__": - import sys - logging.basicConfig( stream=sys.stdout, level=logging.CRITICAL ) - tr= unittest.TextTestRunner() - result= tr.run( suite() ) + 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) ) + sys.exit(len(result.failures) + len(result.errors)) diff --git a/test/test_latex.log b/test/test_latex.log new file mode 100644 index 0000000..2c632f1 --- /dev/null +++ b/test/test_latex.log @@ -0,0 +1,103 @@ +This is XeTeXk, Version 3.1415926-2.2-0.999.6 (Web2C 7.5.7) (format=xelatex 2008.12.28) 13 FEB 2010 08:46 +entering extended mode + %&-line parsing enabled. +**test_latex.tex +(./test_latex.tex +LaTeX2e <2005/12/01> +Babel and hyphenation patterns for english, usenglishmax, dumylang, noh +yphenation, german-x-2008-06-18, ngerman-x-2008-06-18, ancientgreek, ibycus, ar +abic, basque, bulgarian, catalan, pinyin, coptic, croatian, czech, danish, dutc +h, esperanto, estonian, farsi, finnish, french, galician, german, ngerman, mono +greek, greek, hungarian, icelandic, indonesian, interlingua, irish, italian, la +tin, lithuanian, mongolian, mongolian2a, bokmal, nynorsk, polish, portuguese, r +omanian, russian, sanskrit, serbian, slovak, slovenian, spanish, swedish, turki +sh, ukenglish, ukrainian, uppersorbian, welsh, loaded. +(/usr/local/texlive/2008/texmf-dist/tex/latex/base/article.cls +Document Class: article 2005/09/16 v1.4f Standard LaTeX document class +(/usr/local/texlive/2008/texmf-dist/tex/latex/base/size10.clo +File: size10.clo 2005/09/16 v1.4f Standard LaTeX file (size option) +) +\c@part=\count79 +\c@section=\count80 +\c@subsection=\count81 +\c@subsubsection=\count82 +\c@paragraph=\count83 +\c@subparagraph=\count84 +\c@figure=\count85 +\c@table=\count86 +\abovecaptionskip=\skip41 +\belowcaptionskip=\skip42 +\bibindent=\dimen102 +) +(/usr/local/texlive/2008/texmf-dist/tex/latex/fancyvrb/fancyvrb.sty +Package: fancyvrb 2008/02/07 + +Style option: `fancyvrb' v2.7a, with DG/SPQR fixes, and firstline=lastline fix +<2008/02/07> (tvz) +(/usr/local/texlive/2008/texmf-dist/tex/latex/graphics/keyval.sty +Package: keyval 1999/03/16 v1.13 key=value parser (DPC) +\KV@toks@=\toks14 +) +\FV@CodeLineNo=\count87 +\FV@InFile=\read1 +\FV@TabBox=\box26 +\c@FancyVerbLine=\count88 +\FV@StepNumber=\count89 +\FV@OutFile=\write3 +) +(./test_latex.aux) +\openout1 = `test_latex.aux'. + +LaTeX Font Info: Checking defaults for OML/cmm/m/it on input line 6. +LaTeX Font Info: ... okay on input line 6. +LaTeX Font Info: Checking defaults for T1/cmr/m/n on input line 6. +LaTeX Font Info: ... okay on input line 6. +LaTeX Font Info: Checking defaults for OT1/cmr/m/n on input line 6. +LaTeX Font Info: ... okay on input line 6. +LaTeX Font Info: Checking defaults for OMS/cmsy/m/n on input line 6. +LaTeX Font Info: ... okay on input line 6. +LaTeX Font Info: Checking defaults for OMX/cmex/m/n on input line 6. +LaTeX Font Info: ... okay on input line 6. +LaTeX Font Info: Checking defaults for U/cmr/m/n on input line 6. +LaTeX Font Info: ... okay on input line 6. +LaTeX Font Info: External font `cmex10' loaded for size +(Font) <12> on input line 8. +LaTeX Font Info: External font `cmex10' loaded for size +(Font) <8> on input line 8. +LaTeX Font Info: External font `cmex10' loaded for size +(Font) <6> on input line 8. + (./test_latex.toc +LaTeX Font Info: External font `cmex10' loaded for size +(Font) <7> on input line 3. +LaTeX Font Info: External font `cmex10' loaded for size +(Font) <5> on input line 3. +) +\tf@toc=\write4 +\openout4 = `test_latex.toc'. + + +Runaway argument? +commandchars=\\{\},codes={\catcode `$=3\catcode `^=7}] $\triangleright \ETC. +! Paragraph ended before \FV@GetKeyValues was complete. + + \par +l.35 + +? +! Emergency stop. + + \par +l.35 + +End of file on the terminal! + + +Here is how much of TeX's memory you used: + 638 strings out of 494701 + 8728 string characters out of 1164828 + 56992 words of memory out of 3000000 + 3920 multiletter control sequences out of 10000+50000 + 8409 words of font info for 30 fonts, out of 3000000 for 5000 + 669 hyphenation exceptions out of 8191 + 23i,6n,19p,147b,187s stack positions out of 5000i,500n,10000p,200000b,50000s +No pages of output. diff --git a/test/test_latex.w b/test/test_latex.w index f0811fc..fd9faf9 100644 --- a/test/test_latex.w +++ b/test/test_latex.w @@ -72,7 +72,7 @@ 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 @@ -86,7 +86,7 @@ they have: @d Print the message @{ -print msg +print(msg) @} \end{document} diff --git a/test/test_loader.py b/test/test_loader.py index 11e3a11..ac06ea2 100644 --- a/test/test_loader.py +++ b/test/test_loader.py @@ -1,24 +1,26 @@ -from __future__ import print_function + +import logging.handlers + """Loader and parsing tests.""" import pyweb import unittest import logging -import StringIO import os +import io +import types -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 ) +class ParseTestcase(unittest.TestCase): + text = "" + file_name = "" + def setUp(self) -> None: + self.source = io.StringIO(self.text) + self.web = pyweb.Web() + self.rdr = pyweb.WebReader() -test1_w= """Some anonymous chunk +test1_w = """Some anonymous chunk @o test1.tmp @{@ @ @@ -30,19 +32,35 @@ def setUp( self ): """ -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. +class Test_ParseErrors(ParseTestcase): + text = test1_w + file_name = "test1.w" + def setUp(self) -> None: + super().setUp() + self.logger = logging.getLogger("WebReader") + self.buffer = logging.handlers.BufferingHandler(12) + self.buffer.setLevel(logging.WARN) + self.logger.addHandler(self.buffer) + self.logger.setLevel(logging.WARN) + def test_error_should_count_1(self) -> None: + self.rdr.load(self.web, self.file_name, self.source) + self.assertEqual(3, self.rdr.errors) + messages = [r.message for r in self.buffer.buffer] + self.assertEqual( + ["At ('test1.w', 8): expected ('@{',), found '@o'", + "Extra '@{' (possibly missing chunk name) near ('test1.w', 9)", + "Extra '@{' (possibly missing chunk name) near ('test1.w', 9)"], + messages + ) + def tearDown(self) -> None: + self.logger.setLevel(logging.CRITICAL) + self.logger.removeHandler(self.buffer) + super().tearDown() + + + + +test8_w = """Some anonymous chunk. @d title @[the title of this document, defined with @@[ and @@]@] A reference to @. @i test8_inc.tmp @@ -55,26 +73,36 @@ def test_should_raise_syntax( self ): """ -class Test_IncludeParseErrors( ParseTestcase ): - text= test8_w - file_name= "test8.w" - def setUp( self ): +class Test_IncludeParseErrors(ParseTestcase): + text = test8_w + file_name = "test8.w" + def setUp(self) -> None: 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() + temp.write(test8_inc_w) + super().setUp() + self.logger = logging.getLogger("WebReader") + self.buffer = logging.handlers.BufferingHandler(12) + self.buffer.setLevel(logging.WARN) + self.logger.addHandler(self.buffer) + self.logger.setLevel(logging.WARN) + def test_error_should_count_2(self) -> None: + self.rdr.load(self.web, self.file_name, self.source) + self.assertEqual(1, self.rdr.errors) + messages = [r.message for r in self.buffer.buffer] + self.assertEqual( + ["At ('test8_inc.tmp', 4): end of input, ('@{', '@[') not found", + "Errors in included file 'test8_inc.tmp', output is incomplete."], + messages + ) + def tearDown(self) -> None: + self.logger.setLevel(logging.CRITICAL) + self.logger.removeHandler(self.buffer) + os.remove('test8_inc.tmp') + super().tearDown() if __name__ == "__main__": import sys - logging.basicConfig( stream=sys.stdout, level= logging.WARN ) + logging.basicConfig(stream=sys.stdout, level=logging.WARN) unittest.main() diff --git a/test/test_rest.tex b/test/test_rest.tex new file mode 100644 index 0000000..5e6f739 --- /dev/null +++ b/test/test_rest.tex @@ -0,0 +1,207 @@ +% generated by Docutils +\documentclass[a4paper,english]{article} +\usepackage{fixltx2e} % LaTeX patches, \textsubscript +\usepackage{cmap} % fix search and cut-and-paste in PDF +\usepackage[T1]{fontenc} +\usepackage[utf8]{inputenc} +\usepackage{ifthen} +\usepackage{babel} +\usepackage{textcomp} % text symbol macros + +%%% Custom LaTeX preamble +% PDF Standard Fonts +\usepackage{mathptmx} % Times +\usepackage[scaled=.90]{helvet} +\usepackage{courier} + +%%% User specified packages and stylesheets + +%%% Fallback definitions for Docutils-specific commands + +% rubric (informal heading) +\providecommand*{\DUrubric}[2][class-arg]{% + \subsubsection*{\centering\textit{\textmd{#2}}}} + +% hyperlinks: +\ifthenelse{\isundefined{\hypersetup}}{ + \usepackage[unicode,colorlinks=true,linkcolor=blue,urlcolor=blue]{hyperref} + \urlstyle{same} % normal text font (alternatives: tt, rm, sf) +}{} +\hypersetup{ + pdftitle={Test Program}, +} + +%%% Body +\begin{document} + +% Document title +\title{Test Program% + \phantomsection% + \label{test-program}% + \\ % subtitle% + \large{Jason R. Fruit}% + \label{jason-r-fruit}} +\author{} +\date{} +\maketitle + +% This data file has been placed in the public domain. + +% Derived from the Unicode character mappings available from +% . +% Processed by unicode2rstsubs.py, part of Docutils: +% . + +\phantomsection\label{contents} +\pdfbookmark[1]{Contents}{contents} +\tableofcontents + + + +%___________________________________________________________________________ + +\section*{Introduction% + \phantomsection% + \addcontentsline{toc}{section}{Introduction}% + \label{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 \texttt{os} module. It builds the message string +in two different ways, and writes separate versions of the program to +two different files. + + +%___________________________________________________________________________ + +\section*{Implementation% + \phantomsection% + \addcontentsline{toc}{section}{Implementation}% + \label{implementation}% +} + + +%___________________________________________________________________________ + +\subsection*{Output files% + \phantomsection% + \addcontentsline{toc}{subsection}{Output files}% + \label{output-files}% +} + +This document contains the makings of two files; the first, +\texttt{test.py}, uses simple string concatenation to build its output +message: + +\DUrubric{test.py (1)} +% +\begin{quote}{\ttfamily \raggedright \noindent +→~Import~the~os~module~(\hyperref[id3]{3})\\ +~→~Get~the~OS~description~(\hyperref[id4]{4})\\ +~→~Construct~the~message~with~Concatenation~(\hyperref[id5]{5})\\ +~→~Print~the~message~(\hyperref[id7]{7}) +} +\end{quote} + +The second uses string substitution: + +\DUrubric{test2.py (2)} +% +\begin{quote}{\ttfamily \raggedright \noindent +→~Import~the~os~module~(\hyperref[id3]{3})\\ +~→~Get~the~OS~description~(\hyperref[id4]{4})\\ +~→~Construct~the~message~with~Substitution~(\hyperref[id6]{6})\\ +~→~Print~the~message~(\hyperref[id7]{7}) +} +\end{quote} + + +%___________________________________________________________________________ + +\subsection*{Retrieving the OS description% + \phantomsection% + \addcontentsline{toc}{subsection}{Retrieving the OS description}% + \label{retrieving-the-os-description}% +} + +First we must import the os module so we can learn about the OS: + +\DUrubric{Import the os module (3)} +% +\begin{quote}{\ttfamily \raggedright \noindent +import~os +} +\end{quote} + +Used by: test.py (\hyperref[id1]{1}); test2.py (\hyperref[id2]{2}) + +That having been done, we can retrieve Python's name for the OS type: + +\DUrubric{Get the OS description (4)} +% +\begin{quote}{\ttfamily \raggedright \noindent +os\_name~=~os.name +} +\end{quote} + +Used by: test.py (\hyperref[id1]{1}); test2.py (\hyperref[id2]{2}) + + +%___________________________________________________________________________ + +\subsection*{Building the message% + \phantomsection% + \addcontentsline{toc}{subsection}{Building the message}% + \label{building-the-message}% +} + +Now, we're ready for the meat of the application: concatenating two strings: + +\DUrubric{Construct the message with Concatenation (5)} +% +\begin{quote}{\ttfamily \raggedright \noindent +msg~=~"Hello,~"~+~os\_name~+~"!" +} +\end{quote} + +Used by: test.py (\hyperref[id1]{1}) + +But wait! Is there a better way? Using string substitution might be +better: + +\DUrubric{Construct the message with Substitution (6)} +% +\begin{quote}{\ttfamily \raggedright \noindent +msg~=~"Hello,~\%s!"~\%~os\_name +} +\end{quote} + +Used by: test2.py (\hyperref[id2]{2}) + +We'll use the first of these methods in \texttt{test.py}, and the +other in \texttt{test2.py}. + + +%___________________________________________________________________________ + +\subsection*{Printing the message% + \phantomsection% + \addcontentsline{toc}{subsection}{Printing the message}% + \label{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: + +\DUrubric{Print the message (7)} +% +\begin{quote}{\ttfamily \raggedright \noindent +print~msg +} +\end{quote} + +Used by: test.py (\hyperref[id1]{1}); test2.py (\hyperref[id2]{2}) + +\end{document} diff --git a/test/test_rst.log b/test/test_rst.log new file mode 100644 index 0000000..c33bf51 --- /dev/null +++ b/test/test_rst.log @@ -0,0 +1,809 @@ +This is XeTeX, Version 3.1415926-2.2-0.9997.4 (TeX Live 2010) (format=xelatex 2011.6.1) 26 AUG 2011 06:48 +entering extended mode + restricted \write18 enabled. + %&-line parsing enabled. +**test_rst.tex +(./test_rst.tex +LaTeX2e <2009/09/24> +Babel and hyphenation patterns for english, dumylang, nohyphenation, ge +rman-x-2009-06-19, ngerman-x-2009-06-19, afrikaans, ancientgreek, ibycus, arabi +c, armenian, basque, bulgarian, catalan, pinyin, coptic, croatian, czech, danis +h, dutch, ukenglish, usenglishmax, esperanto, estonian, ethiopic, farsi, finnis +h, french, galician, german, ngerman, swissgerman, monogreek, greek, hungarian, + icelandic, assamese, bengali, gujarati, hindi, kannada, malayalam, marathi, or +iya, panjabi, tamil, telugu, indonesian, interlingua, irish, italian, kurmanji, + lao, latin, latvian, lithuanian, mongolian, mongolianlmc, bokmal, nynorsk, pol +ish, portuguese, romanian, russian, sanskrit, serbian, slovak, slovenian, spani +sh, swedish, turkish, turkmen, ukrainian, uppersorbian, welsh, loaded. +(/usr/local/texlive/2010/texmf-dist/tex/latex/base/article.cls +Document Class: article 2007/10/19 v1.4h Standard LaTeX document class +(/usr/local/texlive/2010/texmf-dist/tex/latex/base/size10.clo +File: size10.clo 2007/10/19 v1.4h Standard LaTeX file (size option) +) +\c@part=\count80 +\c@section=\count81 +\c@subsection=\count82 +\c@subsubsection=\count83 +\c@paragraph=\count84 +\c@subparagraph=\count85 +\c@figure=\count86 +\c@table=\count87 +\abovecaptionskip=\skip41 +\belowcaptionskip=\skip42 +\bibindent=\dimen102 +) +(/usr/local/texlive/2010/texmf-dist/tex/latex/base/fixltx2e.sty +Package: fixltx2e 2006/09/13 v1.1m fixes to LaTeX +LaTeX Info: Redefining \em on input line 420. +) +(/usr/local/texlive/2010/texmf-dist/tex/latex/cmap/cmap.sty +Package: cmap 2008/03/06 v1.0h CMap support: searchable PDF + + +Package cmap Warning: pdftex not detected - exiting. + +) (/usr/local/texlive/2010/texmf-dist/tex/latex/base/fontenc.sty +Package: fontenc 2005/09/27 v1.99g Standard LaTeX package + +(/usr/local/texlive/2010/texmf-dist/tex/latex/base/t1enc.def +File: t1enc.def 2005/09/27 v1.99g Standard LaTeX file +LaTeX Font Info: Redeclaring font encoding T1 on input line 43. +)) +(/usr/local/texlive/2010/texmf-dist/tex/latex/base/inputenc.sty +Package: inputenc 2008/03/30 v1.1d Input encoding file +\inpenc@prehook=\toks14 +\inpenc@posthook=\toks15 + +(/usr/local/texlive/2010/texmf-dist/tex/latex/base/utf8.def +File: utf8.def 2008/04/05 v1.1m UTF-8 support for inputenc +Now handling font encoding OML ... +... no UTF-8 mapping file for font encoding OML +Now handling font encoding T1 ... +... processing UTF-8 mapping file for font encoding T1 + +(/usr/local/texlive/2010/texmf-dist/tex/latex/base/t1enc.dfu +File: t1enc.dfu 2008/04/05 v1.1m UTF-8 support for inputenc + defining Unicode char U+00A1 (decimal 161) + defining Unicode char U+00A3 (decimal 163) + defining Unicode char U+00AB (decimal 171) + defining Unicode char U+00BB (decimal 187) + defining Unicode char U+00BF (decimal 191) + defining Unicode char U+00C0 (decimal 192) + defining Unicode char U+00C1 (decimal 193) + defining Unicode char U+00C2 (decimal 194) + defining Unicode char U+00C3 (decimal 195) + defining Unicode char U+00C4 (decimal 196) + defining Unicode char U+00C5 (decimal 197) + defining Unicode char U+00C6 (decimal 198) + defining Unicode char U+00C7 (decimal 199) + defining Unicode char U+00C8 (decimal 200) + defining Unicode char U+00C9 (decimal 201) + defining Unicode char U+00CA (decimal 202) + defining Unicode char U+00CB (decimal 203) + defining Unicode char U+00CC (decimal 204) + defining Unicode char U+00CD (decimal 205) + defining Unicode char U+00CE (decimal 206) + defining Unicode char U+00CF (decimal 207) + defining Unicode char U+00D0 (decimal 208) + defining Unicode char U+00D1 (decimal 209) + defining Unicode char U+00D2 (decimal 210) + defining Unicode char U+00D3 (decimal 211) + defining Unicode char U+00D4 (decimal 212) + defining Unicode char U+00D5 (decimal 213) + defining Unicode char U+00D6 (decimal 214) + defining Unicode char U+00D8 (decimal 216) + defining Unicode char U+00D9 (decimal 217) + defining Unicode char U+00DA (decimal 218) + defining Unicode char U+00DB (decimal 219) + defining Unicode char U+00DC (decimal 220) + defining Unicode char U+00DD (decimal 221) + defining Unicode char U+00DE (decimal 222) + defining Unicode char U+00DF (decimal 223) + defining Unicode char U+00E0 (decimal 224) + defining Unicode char U+00E1 (decimal 225) + defining Unicode char U+00E2 (decimal 226) + defining Unicode char U+00E3 (decimal 227) + defining Unicode char U+00E4 (decimal 228) + defining Unicode char U+00E5 (decimal 229) + defining Unicode char U+00E6 (decimal 230) + defining Unicode char U+00E7 (decimal 231) + defining Unicode char U+00E8 (decimal 232) + defining Unicode char U+00E9 (decimal 233) + defining Unicode char U+00EA (decimal 234) + defining Unicode char U+00EB (decimal 235) + defining Unicode char U+00EC (decimal 236) + defining Unicode char U+00ED (decimal 237) + defining Unicode char U+00EE (decimal 238) + defining Unicode char U+00EF (decimal 239) + defining Unicode char U+00F0 (decimal 240) + defining Unicode char U+00F1 (decimal 241) + defining Unicode char U+00F2 (decimal 242) + defining Unicode char U+00F3 (decimal 243) + defining Unicode char U+00F4 (decimal 244) + defining Unicode char U+00F5 (decimal 245) + defining Unicode char U+00F6 (decimal 246) + defining Unicode char U+00F8 (decimal 248) + defining Unicode char U+00F9 (decimal 249) + defining Unicode char U+00FA (decimal 250) + defining Unicode char U+00FB (decimal 251) + defining Unicode char U+00FC (decimal 252) + defining Unicode char U+00FD (decimal 253) + defining Unicode char U+00FE (decimal 254) + defining Unicode char U+00FF (decimal 255) + defining Unicode char U+0102 (decimal 258) + defining Unicode char U+0103 (decimal 259) + defining Unicode char U+0104 (decimal 260) + defining Unicode char U+0105 (decimal 261) + defining Unicode char U+0106 (decimal 262) + defining Unicode char U+0107 (decimal 263) + defining Unicode char U+010C (decimal 268) + defining Unicode char U+010D (decimal 269) + defining Unicode char U+010E (decimal 270) + defining Unicode char U+010F (decimal 271) + defining Unicode char U+0110 (decimal 272) + defining Unicode char U+0111 (decimal 273) + defining Unicode char U+0118 (decimal 280) + defining Unicode char U+0119 (decimal 281) + defining Unicode char U+011A (decimal 282) + defining Unicode char U+011B (decimal 283) + defining Unicode char U+011E (decimal 286) + defining Unicode char U+011F (decimal 287) + defining Unicode char U+0130 (decimal 304) + defining Unicode char U+0131 (decimal 305) + defining Unicode char U+0132 (decimal 306) + defining Unicode char U+0133 (decimal 307) + defining Unicode char U+0139 (decimal 313) + defining Unicode char U+013A (decimal 314) + defining Unicode char U+013D (decimal 317) + defining Unicode char U+013E (decimal 318) + defining Unicode char U+0141 (decimal 321) + defining Unicode char U+0142 (decimal 322) + defining Unicode char U+0143 (decimal 323) + defining Unicode char U+0144 (decimal 324) + defining Unicode char U+0147 (decimal 327) + defining Unicode char U+0148 (decimal 328) + defining Unicode char U+014A (decimal 330) + defining Unicode char U+014B (decimal 331) + defining Unicode char U+0150 (decimal 336) + defining Unicode char U+0151 (decimal 337) + defining Unicode char U+0152 (decimal 338) + defining Unicode char U+0153 (decimal 339) + defining Unicode char U+0154 (decimal 340) + defining Unicode char U+0155 (decimal 341) + defining Unicode char U+0158 (decimal 344) + defining Unicode char U+0159 (decimal 345) + defining Unicode char U+015A (decimal 346) + defining Unicode char U+015B (decimal 347) + defining Unicode char U+015E (decimal 350) + defining Unicode char U+015F (decimal 351) + defining Unicode char U+0160 (decimal 352) + defining Unicode char U+0161 (decimal 353) + defining Unicode char U+0162 (decimal 354) + defining Unicode char U+0163 (decimal 355) + defining Unicode char U+0164 (decimal 356) + defining Unicode char U+0165 (decimal 357) + defining Unicode char U+016E (decimal 366) + defining Unicode char U+016F (decimal 367) + defining Unicode char U+0170 (decimal 368) + defining Unicode char U+0171 (decimal 369) + defining Unicode char U+0178 (decimal 376) + defining Unicode char U+0179 (decimal 377) + defining Unicode char U+017A (decimal 378) + defining Unicode char U+017B (decimal 379) + defining Unicode char U+017C (decimal 380) + defining Unicode char U+017D (decimal 381) + defining Unicode char U+017E (decimal 382) + defining Unicode char U+200C (decimal 8204) + defining Unicode char U+2013 (decimal 8211) + defining Unicode char U+2014 (decimal 8212) + defining Unicode char U+2018 (decimal 8216) + defining Unicode char U+2019 (decimal 8217) + defining Unicode char U+201A (decimal 8218) + defining Unicode char U+201C (decimal 8220) + defining Unicode char U+201D (decimal 8221) + defining Unicode char U+201E (decimal 8222) + defining Unicode char U+2030 (decimal 8240) + defining Unicode char U+2031 (decimal 8241) + defining Unicode char U+2039 (decimal 8249) + defining Unicode char U+203A (decimal 8250) + defining Unicode char U+2423 (decimal 9251) +) +Now handling font encoding OT1 ... +... processing UTF-8 mapping file for font encoding OT1 + +(/usr/local/texlive/2010/texmf-dist/tex/latex/base/ot1enc.dfu +File: ot1enc.dfu 2008/04/05 v1.1m UTF-8 support for inputenc + defining Unicode char U+00A1 (decimal 161) + defining Unicode char U+00A3 (decimal 163) + defining Unicode char U+00B8 (decimal 184) + defining Unicode char U+00BF (decimal 191) + defining Unicode char U+00C5 (decimal 197) + defining Unicode char U+00C6 (decimal 198) + defining Unicode char U+00D8 (decimal 216) + defining Unicode char U+00DF (decimal 223) + defining Unicode char U+00E6 (decimal 230) + defining Unicode char U+00EC (decimal 236) + defining Unicode char U+00ED (decimal 237) + defining Unicode char U+00EE (decimal 238) + defining Unicode char U+00EF (decimal 239) + defining Unicode char U+00F8 (decimal 248) + defining Unicode char U+0131 (decimal 305) + defining Unicode char U+0141 (decimal 321) + defining Unicode char U+0142 (decimal 322) + defining Unicode char U+0152 (decimal 338) + defining Unicode char U+0153 (decimal 339) + defining Unicode char U+2013 (decimal 8211) + defining Unicode char U+2014 (decimal 8212) + defining Unicode char U+2018 (decimal 8216) + defining Unicode char U+2019 (decimal 8217) + defining Unicode char U+201C (decimal 8220) + defining Unicode char U+201D (decimal 8221) +) +Now handling font encoding OMS ... +... processing UTF-8 mapping file for font encoding OMS + +(/usr/local/texlive/2010/texmf-dist/tex/latex/base/omsenc.dfu +File: omsenc.dfu 2008/04/05 v1.1m UTF-8 support for inputenc + defining Unicode char U+00A7 (decimal 167) + defining Unicode char U+00B6 (decimal 182) + defining Unicode char U+00B7 (decimal 183) + defining Unicode char U+2020 (decimal 8224) + defining Unicode char U+2021 (decimal 8225) + defining Unicode char U+2022 (decimal 8226) +) +Now handling font encoding OMX ... +... no UTF-8 mapping file for font encoding OMX +Now handling font encoding U ... +... no UTF-8 mapping file for font encoding U + defining Unicode char U+00A9 (decimal 169) + defining Unicode char U+00AA (decimal 170) + defining Unicode char U+00AE (decimal 174) + defining Unicode char U+00BA (decimal 186) + defining Unicode char U+02C6 (decimal 710) + defining Unicode char U+02DC (decimal 732) + defining Unicode char U+200C (decimal 8204) + defining Unicode char U+2026 (decimal 8230) + defining Unicode char U+2122 (decimal 8482) + defining Unicode char U+2423 (decimal 9251) +)) +(/usr/local/texlive/2010/texmf-dist/tex/latex/base/ifthen.sty +Package: ifthen 2001/05/26 v1.1c Standard LaTeX ifthen package (DPC) +) +(/usr/local/texlive/2010/texmf-dist/tex/generic/babel/babel.sty +Package: babel 2008/07/06 v3.8l The Babel package + +(/usr/local/texlive/2010/texmf-dist/tex/generic/babel/english.ldf +Language: english 2005/03/30 v3.3o English support from the babel system + +(/usr/local/texlive/2010/texmf-dist/tex/generic/babel/babel.def +File: babel.def 2008/07/06 v3.8l Babel common definitions +\babel@savecnt=\count88 +\U@D=\dimen103 +) +\l@canadian = a dialect from \language\l@american +\l@australian = a dialect from \language\l@british +\l@newzealand = a dialect from \language\l@british +)) +(/usr/local/texlive/2010/texmf-dist/tex/latex/base/textcomp.sty +Package: textcomp 2005/09/27 v1.99g Standard LaTeX package +Package textcomp Info: Sub-encoding information: +(textcomp) 5 = only ISO-Adobe without \textcurrency +(textcomp) 4 = 5 + \texteuro +(textcomp) 3 = 4 + \textohm +(textcomp) 2 = 3 + \textestimated + \textcurrency +(textcomp) 1 = TS1 - \textcircled - \t +(textcomp) 0 = TS1 (full) +(textcomp) Font families with sub-encoding setting implement +(textcomp) only a restricted character set as indicated. +(textcomp) Family '?' is the default used for unknown fonts. +(textcomp) See the documentation for details. +Package textcomp Info: Setting ? sub-encoding to TS1/1 on input line 71. + +(/usr/local/texlive/2010/texmf-dist/tex/latex/base/ts1enc.def +File: ts1enc.def 2001/06/05 v3.0e (jk/car/fm) Standard LaTeX file +Now handling font encoding TS1 ... +... processing UTF-8 mapping file for font encoding TS1 + +(/usr/local/texlive/2010/texmf-dist/tex/latex/base/ts1enc.dfu +File: ts1enc.dfu 2008/04/05 v1.1m UTF-8 support for inputenc + defining Unicode char U+00A2 (decimal 162) + defining Unicode char U+00A3 (decimal 163) + defining Unicode char U+00A4 (decimal 164) + defining Unicode char U+00A5 (decimal 165) + defining Unicode char U+00A6 (decimal 166) + defining Unicode char U+00A7 (decimal 167) + defining Unicode char U+00A8 (decimal 168) + defining Unicode char U+00A9 (decimal 169) + defining Unicode char U+00AA (decimal 170) + defining Unicode char U+00AC (decimal 172) + defining Unicode char U+00AE (decimal 174) + defining Unicode char U+00AF (decimal 175) + defining Unicode char U+00B0 (decimal 176) + defining Unicode char U+00B1 (decimal 177) + defining Unicode char U+00B2 (decimal 178) + defining Unicode char U+00B3 (decimal 179) + defining Unicode char U+00B4 (decimal 180) + defining Unicode char U+00B5 (decimal 181) + defining Unicode char U+00B6 (decimal 182) + defining Unicode char U+00B7 (decimal 183) + defining Unicode char U+00B9 (decimal 185) + defining Unicode char U+00BA (decimal 186) + defining Unicode char U+00BC (decimal 188) + defining Unicode char U+00BD (decimal 189) + defining Unicode char U+00BE (decimal 190) + defining Unicode char U+00D7 (decimal 215) + defining Unicode char U+00F7 (decimal 247) + defining Unicode char U+0192 (decimal 402) + defining Unicode char U+02C7 (decimal 711) + defining Unicode char U+02D8 (decimal 728) + defining Unicode char U+02DD (decimal 733) + defining Unicode char U+0E3F (decimal 3647) + defining Unicode char U+2016 (decimal 8214) + defining Unicode char U+2020 (decimal 8224) + defining Unicode char U+2021 (decimal 8225) + defining Unicode char U+2022 (decimal 8226) + defining Unicode char U+2030 (decimal 8240) + defining Unicode char U+2031 (decimal 8241) + defining Unicode char U+203B (decimal 8251) + defining Unicode char U+203D (decimal 8253) + defining Unicode char U+2044 (decimal 8260) + defining Unicode char U+204E (decimal 8270) + defining Unicode char U+2052 (decimal 8274) + defining Unicode char U+20A1 (decimal 8353) + defining Unicode char U+20A4 (decimal 8356) + defining Unicode char U+20A6 (decimal 8358) + defining Unicode char U+20A9 (decimal 8361) + defining Unicode char U+20AB (decimal 8363) + defining Unicode char U+20AC (decimal 8364) + defining Unicode char U+20B1 (decimal 8369) + defining Unicode char U+2103 (decimal 8451) + defining Unicode char U+2116 (decimal 8470) + defining Unicode char U+2117 (decimal 8471) + defining Unicode char U+211E (decimal 8478) + defining Unicode char U+2120 (decimal 8480) + defining Unicode char U+2122 (decimal 8482) + defining Unicode char U+2126 (decimal 8486) + defining Unicode char U+2127 (decimal 8487) + defining Unicode char U+212E (decimal 8494) + defining Unicode char U+2190 (decimal 8592) + defining Unicode char U+2191 (decimal 8593) + defining Unicode char U+2192 (decimal 8594) + defining Unicode char U+2193 (decimal 8595) + defining Unicode char U+2329 (decimal 9001) + defining Unicode char U+232A (decimal 9002) + defining Unicode char U+2422 (decimal 9250) + defining Unicode char U+25E6 (decimal 9702) + defining Unicode char U+25EF (decimal 9711) + defining Unicode char U+266A (decimal 9834) +)) +LaTeX Info: Redefining \oldstylenums on input line 266. +Package textcomp Info: Setting cmr sub-encoding to TS1/0 on input line 281. +Package textcomp Info: Setting cmss sub-encoding to TS1/0 on input line 282. +Package textcomp Info: Setting cmtt sub-encoding to TS1/0 on input line 283. +Package textcomp Info: Setting cmvtt sub-encoding to TS1/0 on input line 284. +Package textcomp Info: Setting cmbr sub-encoding to TS1/0 on input line 285. +Package textcomp Info: Setting cmtl sub-encoding to TS1/0 on input line 286. +Package textcomp Info: Setting ccr sub-encoding to TS1/0 on input line 287. +Package textcomp Info: Setting ptm sub-encoding to TS1/4 on input line 288. +Package textcomp Info: Setting pcr sub-encoding to TS1/4 on input line 289. +Package textcomp Info: Setting phv sub-encoding to TS1/4 on input line 290. +Package textcomp Info: Setting ppl sub-encoding to TS1/3 on input line 291. +Package textcomp Info: Setting pag sub-encoding to TS1/4 on input line 292. +Package textcomp Info: Setting pbk sub-encoding to TS1/4 on input line 293. +Package textcomp Info: Setting pnc sub-encoding to TS1/4 on input line 294. +Package textcomp Info: Setting pzc sub-encoding to TS1/4 on input line 295. +Package textcomp Info: Setting bch sub-encoding to TS1/4 on input line 296. +Package textcomp Info: Setting put sub-encoding to TS1/5 on input line 297. +Package textcomp Info: Setting uag sub-encoding to TS1/5 on input line 298. +Package textcomp Info: Setting ugq sub-encoding to TS1/5 on input line 299. +Package textcomp Info: Setting ul8 sub-encoding to TS1/4 on input line 300. +Package textcomp Info: Setting ul9 sub-encoding to TS1/4 on input line 301. +Package textcomp Info: Setting augie sub-encoding to TS1/5 on input line 302. +Package textcomp Info: Setting dayrom sub-encoding to TS1/3 on input line 303. +Package textcomp Info: Setting dayroms sub-encoding to TS1/3 on input line 304. + +Package textcomp Info: Setting pxr sub-encoding to TS1/0 on input line 305. +Package textcomp Info: Setting pxss sub-encoding to TS1/0 on input line 306. +Package textcomp Info: Setting pxtt sub-encoding to TS1/0 on input line 307. +Package textcomp Info: Setting txr sub-encoding to TS1/0 on input line 308. +Package textcomp Info: Setting txss sub-encoding to TS1/0 on input line 309. +Package textcomp Info: Setting txtt sub-encoding to TS1/0 on input line 310. +Package textcomp Info: Setting futs sub-encoding to TS1/4 on input line 311. +Package textcomp Info: Setting futx sub-encoding to TS1/4 on input line 312. +Package textcomp Info: Setting futj sub-encoding to TS1/4 on input line 313. +Package textcomp Info: Setting hlh sub-encoding to TS1/3 on input line 314. +Package textcomp Info: Setting hls sub-encoding to TS1/3 on input line 315. +Package textcomp Info: Setting hlst sub-encoding to TS1/3 on input line 316. +Package textcomp Info: Setting hlct sub-encoding to TS1/5 on input line 317. +Package textcomp Info: Setting hlx sub-encoding to TS1/5 on input line 318. +Package textcomp Info: Setting hlce sub-encoding to TS1/5 on input line 319. +Package textcomp Info: Setting hlcn sub-encoding to TS1/5 on input line 320. +Package textcomp Info: Setting hlcw sub-encoding to TS1/5 on input line 321. +Package textcomp Info: Setting hlcf sub-encoding to TS1/5 on input line 322. +Package textcomp Info: Setting pplx sub-encoding to TS1/3 on input line 323. +Package textcomp Info: Setting pplj sub-encoding to TS1/3 on input line 324. +Package textcomp Info: Setting ptmx sub-encoding to TS1/4 on input line 325. +Package textcomp Info: Setting ptmj sub-encoding to TS1/4 on input line 326. +) +(/usr/local/texlive/2010/texmf-dist/tex/latex/psnfss/mathptmx.sty +Package: mathptmx 2005/04/12 PSNFSS-v9.2a Times w/ Math, improved (SPQR, WaS) +LaTeX Font Info: Redeclaring symbol font `operators' on input line 28. +LaTeX Font Info: Overwriting symbol font `operators' in version `normal' +(Font) OT1/cmr/m/n --> OT1/ztmcm/m/n on input line 28. +LaTeX Font Info: Overwriting symbol font `operators' in version `bold' +(Font) OT1/cmr/bx/n --> OT1/ztmcm/m/n on input line 28. +LaTeX Font Info: Redeclaring symbol font `letters' on input line 29. +LaTeX Font Info: Overwriting symbol font `letters' in version `normal' +(Font) OML/cmm/m/it --> OML/ztmcm/m/it on input line 29. +LaTeX Font Info: Overwriting symbol font `letters' in version `bold' +(Font) OML/cmm/b/it --> OML/ztmcm/m/it on input line 29. +LaTeX Font Info: Redeclaring symbol font `symbols' on input line 30. +LaTeX Font Info: Overwriting symbol font `symbols' in version `normal' +(Font) OMS/cmsy/m/n --> OMS/ztmcm/m/n on input line 30. +LaTeX Font Info: Overwriting symbol font `symbols' in version `bold' +(Font) OMS/cmsy/b/n --> OMS/ztmcm/m/n on input line 30. +LaTeX Font Info: Redeclaring symbol font `largesymbols' on input line 31. +LaTeX Font Info: Overwriting symbol font `largesymbols' in version `normal' +(Font) OMX/cmex/m/n --> OMX/ztmcm/m/n on input line 31. +LaTeX Font Info: Overwriting symbol font `largesymbols' in version `bold' +(Font) OMX/cmex/m/n --> OMX/ztmcm/m/n on input line 31. +\symbold=\mathgroup4 +\symitalic=\mathgroup5 +LaTeX Font Info: Redeclaring math alphabet \mathbf on input line 34. +LaTeX Font Info: Overwriting math alphabet `\mathbf' in version `normal' +(Font) OT1/cmr/bx/n --> OT1/ptm/bx/n on input line 34. +LaTeX Font Info: Overwriting math alphabet `\mathbf' in version `bold' +(Font) OT1/cmr/bx/n --> OT1/ptm/bx/n on input line 34. +LaTeX Font Info: Redeclaring math alphabet \mathit on input line 35. +LaTeX Font Info: Overwriting math alphabet `\mathit' in version `normal' +(Font) OT1/cmr/m/it --> OT1/ptm/m/it on input line 35. +LaTeX Font Info: Overwriting math alphabet `\mathit' in version `bold' +(Font) OT1/cmr/bx/it --> OT1/ptm/m/it on input line 35. +LaTeX Info: Redefining \hbar on input line 50. +) +(/usr/local/texlive/2010/texmf-dist/tex/latex/psnfss/helvet.sty +Package: helvet 2005/04/12 PSNFSS-v9.2a (WaS) + +(/usr/local/texlive/2010/texmf-dist/tex/latex/graphics/keyval.sty +Package: keyval 1999/03/16 v1.13 key=value parser (DPC) +\KV@toks@=\toks16 +)) +(/usr/local/texlive/2010/texmf-dist/tex/latex/psnfss/courier.sty +Package: courier 2005/04/12 PSNFSS-v9.2a (WaS) +) +(/usr/local/texlive/2010/texmf-dist/tex/latex/hyperref/hyperref.sty +Package: hyperref 2011/04/17 v6.82g Hypertext links for LaTeX + +(/usr/local/texlive/2010/texmf-dist/tex/generic/oberdiek/hobsub-hyperref.sty +Package: hobsub-hyperref 2011/04/23 v1.4 Bundle oberdiek, subset hyperref (HO) + +(/usr/local/texlive/2010/texmf-dist/tex/generic/oberdiek/hobsub-generic.sty +Package: hobsub-generic 2011/04/23 v1.4 Bundle oberdiek, subset generic (HO) +Package: hobsub 2011/04/23 v1.4 Subsetting bundle oberdiek (HO) +Package: infwarerr 2010/04/08 v1.3 Providing info/warning/message (HO) +Package: ltxcmds 2011/04/18 v1.20 LaTeX kernel commands for general use (HO) +Package: ifluatex 2010/03/01 v1.3 Provides the ifluatex switch (HO) +Package ifluatex Info: LuaTeX not detected. +Package: ifvtex 2010/03/01 v1.5 Switches for detecting VTeX and its modes (HO) +Package ifvtex Info: VTeX not detected. +Package: intcalc 2007/09/27 v1.1 Expandable integer calculations (HO) +Package: ifpdf 2011/01/30 v2.3 Provides the ifpdf switch (HO) +Package ifpdf Info: pdfTeX in PDF mode is not detected. +Package: etexcmds 2011/02/16 v1.5 Prefix for e-TeX command names (HO) +Package etexcmds Info: Could not find \expanded. +(etexcmds) That can mean that you are not using pdfTeX 1.50 or +(etexcmds) that some package has redefined \expanded. +(etexcmds) In the latter case, load this package earlier. +Package: kvsetkeys 2011/04/07 v1.13 Key value parser (HO) +Package: kvdefinekeys 2011/04/07 v1.3 Defining keys (HO) +Package: pdftexcmds 2011/04/22 v0.16 Utilities of pdfTeX for LuaTeX (HO) +Package pdftexcmds Info: LuaTeX not detected. +Package pdftexcmds Info: pdfTeX >= 1.30 not detected. +Package pdftexcmds Info: \pdf@primitive is available. +Package pdftexcmds Info: \pdf@ifprimitive is available. +Package pdftexcmds Info: \pdfdraftmode not found. +Package: pdfescape 2011/04/04 v1.12 Provides string conversions (HO) +Package: bigintcalc 2011/01/30 v1.2 Expandable big integer calculations (HO) +Package: bitset 2011/01/30 v1.1 Data type bit set (HO) +Package: uniquecounter 2011/01/30 v1.2 Provides unlimited unique counter (HO) +) +Package hobsub Info: Skipping package `hobsub' (already loaded). +Package: letltxmacro 2010/09/02 v1.4 Let assignment for LaTeX macros (HO) +Package: hopatch 2011/01/30 v1.0 Wrapper for package hooks (HO) +Package: xcolor-patch 2011/01/30 xcolor patch +Package: atveryend 2011/04/23 v1.7 Hooks at very end of document (HO) +Package atveryend Info: \enddocument detected (standard). +Package: atbegshi 2011/01/30 v1.15 At begin shipout hook (HO) +Package: refcount 2010/12/01 v3.2 Data extraction from references (HO) +Package: hycolor 2011/01/30 v1.7 Color options of hyperref/bookmark (HO) +) +(/usr/local/texlive/2010/texmf-dist/tex/generic/ifxetex/ifxetex.sty +Package: ifxetex 2010/09/12 v0.6 Provides ifxetex conditional +) +(/usr/local/texlive/2010/texmf-dist/tex/latex/oberdiek/kvoptions.sty +Package: kvoptions 2010/12/23 v3.10 Keyval support for LaTeX options (HO) +) +\@linkdim=\dimen104 +\Hy@linkcounter=\count89 +\Hy@pagecounter=\count90 + +(/usr/local/texlive/2010/texmf-dist/tex/latex/hyperref/pd1enc.def +File: pd1enc.def 2011/04/17 v6.82g Hyperref: PDFDocEncoding definition (HO) +Now handling font encoding PD1 ... +... no UTF-8 mapping file for font encoding PD1 +) +\Hy@SavedSpaceFactor=\count91 + +(/usr/local/texlive/2010/texmf-dist/tex/xelatex/xetexconfig/hyperref.cfg +File: hyperref.cfg 2008/07/11 v1.2 hyperref configuration for XeLaTeX +) +Package hyperref Info: Option `unicode' set `true' on input line 3905. + +(/usr/local/texlive/2010/texmf-dist/tex/latex/hyperref/puenc.def +File: puenc.def 2011/04/17 v6.82g Hyperref: PDF Unicode definition (HO) +Now handling font encoding PU ... +... no UTF-8 mapping file for font encoding PU +) +Package hyperref Info: Option `colorlinks' set `true' on input line 3905. +Package hyperref Info: Hyper figures OFF on input line 4026. +Package hyperref Info: Link nesting OFF on input line 4031. +Package hyperref Info: Hyper index ON on input line 4034. +Package hyperref Info: Plain pages OFF on input line 4041. +Package hyperref Info: Backreferencing OFF on input line 4046. +Package hyperref Info: Implicit mode ON; LaTeX internals redefined. +Package hyperref Info: Bookmarks ON on input line 4264. +\c@Hy@tempcnt=\count92 + +(/usr/local/texlive/2010/texmf-dist/tex/latex/url/url.sty +\Urlmuskip=\muskip10 +Package: url 2006/04/12 ver 3.3 Verb mode for urls, etc. +) +LaTeX Info: Redefining \url on input line 4617. +\Fld@menulength=\count93 +\Field@Width=\dimen105 +\Fld@charsize=\dimen106 +Package hyperref Info: Hyper figures OFF on input line 5701. +Package hyperref Info: Link nesting OFF on input line 5706. +Package hyperref Info: Hyper index ON on input line 5709. +Package hyperref Info: backreferencing OFF on input line 5716. +Package hyperref Info: Link coloring ON on input line 5719. +Package hyperref Info: Link coloring with OCG OFF on input line 5726. +Package hyperref Info: PDF/A mode OFF on input line 5731. +LaTeX Info: Redefining \ref on input line 5771. +LaTeX Info: Redefining \pageref on input line 5775. +\Hy@abspage=\count94 +\c@Item=\count95 +\c@Hfootnote=\count96 +) + +Package hyperref Message: Driver (autodetected): hxetex. + +(/usr/local/texlive/2010/texmf-dist/tex/latex/hyperref/hxetex.def +File: hxetex.def 2011/04/17 v6.82g Hyperref driver for XeTeX + +(/usr/local/texlive/2010/texmf-dist/tex/generic/oberdiek/stringenc.sty +Package: stringenc 2010/03/01 v1.8 Converts strings between encodings (HO) +) +\pdfm@box=\box26 +\c@Hy@AnnotLevel=\count97 +\HyField@AnnotCount=\count98 +\Fld@listcount=\count99 +\c@bookmark@seq@number=\count100 + +(/usr/local/texlive/2010/texmf-dist/tex/latex/oberdiek/rerunfilecheck.sty +Package: rerunfilecheck 2011/04/15 v1.7 Rerun checks for auxiliary files (HO) +Package rerunfilecheck Info: Feature \pdfmdfivesum is not available +(rerunfilecheck) (e.g. pdfTeX or LuaTeX with package `pdftexcmds'). + +(rerunfilecheck) Therefore file contents cannot be checked efficien +tly +(rerunfilecheck) and the loading of the package is aborted. +) +\Hy@SectionHShift=\skip43 +) +(./test_rst.aux) +\openout1 = `test_rst.aux'. + +LaTeX Font Info: Checking defaults for OML/cmm/m/it on input line 35. +LaTeX Font Info: ... okay on input line 35. +LaTeX Font Info: Checking defaults for T1/cmr/m/n on input line 35. +LaTeX Font Info: ... okay on input line 35. +LaTeX Font Info: Checking defaults for OT1/cmr/m/n on input line 35. +LaTeX Font Info: ... okay on input line 35. +LaTeX Font Info: Checking defaults for OMS/cmsy/m/n on input line 35. +LaTeX Font Info: ... okay on input line 35. +LaTeX Font Info: Checking defaults for OMX/cmex/m/n on input line 35. +LaTeX Font Info: ... okay on input line 35. +LaTeX Font Info: Checking defaults for U/cmr/m/n on input line 35. +LaTeX Font Info: ... okay on input line 35. +LaTeX Font Info: Checking defaults for TS1/cmr/m/n on input line 35. +LaTeX Font Info: Try loading font information for TS1+cmr on input line 35. + (/usr/local/texlive/2010/texmf-dist/tex/latex/base/ts1cmr.fd +File: ts1cmr.fd 1999/05/25 v2.5h Standard LaTeX font definitions +) +LaTeX Font Info: ... okay on input line 35. +LaTeX Font Info: Checking defaults for PD1/pdf/m/n on input line 35. +LaTeX Font Info: ... okay on input line 35. +LaTeX Font Info: Checking defaults for PU/pdf/m/n on input line 35. +LaTeX Font Info: ... okay on input line 35. +LaTeX Font Info: Try loading font information for T1+ptm on input line 35. + +(/usr/local/texlive/2010/texmf-dist/tex/latex/psnfss/t1ptm.fd +File: t1ptm.fd 2001/06/04 font definitions for T1/ptm. +) +\big@size=\dimen107 +\AtBeginShipoutBox=\box27 + +(/usr/local/texlive/2010/texmf-dist/tex/latex/graphics/color.sty +Package: color 2005/11/14 v1.0j Standard LaTeX Color (DPC) + +(/usr/local/texlive/2010/texmf-dist/tex/latex/latexconfig/color.cfg +File: color.cfg 2007/01/18 v1.5 color configuration of teTeX/TeXLive +) +Package color Info: Driver file: xetex.def on input line 130. + +(/usr/local/texlive/2010/texmf-dist/tex/xelatex/xetex-def/xetex.def +File: xetex.def 2009/11/22 v0.94 LaTeX color/graphics driver for XeTeX (RRM/JK) + +)) +Package hyperref Info: Link coloring ON on input line 35. + +(/usr/local/texlive/2010/texmf-dist/tex/latex/hyperref/nameref.sty +Package: nameref 2010/04/30 v2.40 Cross-referencing by name of section + +(/usr/local/texlive/2010/texmf-dist/tex/generic/oberdiek/gettitlestring.sty +Package: gettitlestring 2010/12/03 v1.4 Cleanup title references (HO) +) +\c@section@level=\count101 +) +LaTeX Info: Redefining \ref on input line 35. +LaTeX Info: Redefining \pageref on input line 35. +LaTeX Info: Redefining \nameref on input line 35. + +(./test_rst.out) (./test_rst.out) +\@outlinefile=\write3 +\openout3 = `test_rst.out'. + +LaTeX Font Info: Try loading font information for OT1+ztmcm on input line 47 +. + +(/usr/local/texlive/2010/texmf-dist/tex/latex/psnfss/ot1ztmcm.fd +File: ot1ztmcm.fd 2000/01/03 Fontinst v1.801 font definitions for OT1/ztmcm. +) +LaTeX Font Info: Try loading font information for OML+ztmcm on input line 47 +. + +(/usr/local/texlive/2010/texmf-dist/tex/latex/psnfss/omlztmcm.fd +File: omlztmcm.fd 2000/01/03 Fontinst v1.801 font definitions for OML/ztmcm. +) +LaTeX Font Info: Try loading font information for OMS+ztmcm on input line 47 +. + +(/usr/local/texlive/2010/texmf-dist/tex/latex/psnfss/omsztmcm.fd +File: omsztmcm.fd 2000/01/03 Fontinst v1.801 font definitions for OMS/ztmcm. +) +LaTeX Font Info: Try loading font information for OMX+ztmcm on input line 47 +. + +(/usr/local/texlive/2010/texmf-dist/tex/latex/psnfss/omxztmcm.fd +File: omxztmcm.fd 2000/01/03 Fontinst v1.801 font definitions for OMX/ztmcm. +) +LaTeX Font Info: Try loading font information for OT1+ptm on input line 47. + +(/usr/local/texlive/2010/texmf-dist/tex/latex/psnfss/ot1ptm.fd +File: ot1ptm.fd 2001/06/04 font definitions for OT1/ptm. +) +LaTeX Font Info: Font shape `OT1/ptm/bx/n' in size <12> not available +(Font) Font shape `OT1/ptm/b/n' tried instead on input line 47. +LaTeX Font Info: Font shape `OT1/ptm/bx/n' in size <9> not available +(Font) Font shape `OT1/ptm/b/n' tried instead on input line 47. +LaTeX Font Info: Font shape `OT1/ptm/bx/n' in size <7> not available +(Font) Font shape `OT1/ptm/b/n' tried instead on input line 47. +LaTeX Font Info: Font shape `T1/ptm/bx/n' in size <14.4> not available +(Font) Font shape `T1/ptm/b/n' tried instead on input line 57. + (./test_rst.toc +LaTeX Font Info: Font shape `T1/ptm/bx/n' in size <10> not available +(Font) Font shape `T1/ptm/b/n' tried instead on input line 2. +LaTeX Font Info: Font shape `OT1/ptm/bx/n' in size <10> not available +(Font) Font shape `OT1/ptm/b/n' tried instead on input line 4. +LaTeX Font Info: Font shape `OT1/ptm/bx/n' in size <7.4> not available +(Font) Font shape `OT1/ptm/b/n' tried instead on input line 4. +LaTeX Font Info: Font shape `OT1/ptm/bx/n' in size <6> not available +(Font) Font shape `OT1/ptm/b/n' tried instead on input line 4. +) +\tf@toc=\write4 +\openout4 = `test_rst.toc'. + +LaTeX Font Info: Try loading font information for T1+pcr on input line 71. + (/usr/local/texlive/2010/texmf-dist/tex/latex/psnfss/t1pcr.fd +File: t1pcr.fd 2001/06/04 font definitions for T1/pcr. +) +LaTeX Font Info: Font shape `T1/ptm/bx/n' in size <12> not available +(Font) Font shape `T1/ptm/b/n' tried instead on input line 91. +LaTeX Font Info: Font shape `T1/ptm/bx/it' in size <10> not available +(Font) Font shape `T1/ptm/b/it' tried instead on input line 97. +Missing character: There is no → in font pcrr8t! + + +LaTeX Warning: Hyper reference `id3' on page 1 undefined on input line 100. + +Missing character: There is no → in font pcrr8t! + +LaTeX Warning: Hyper reference `id4' on page 1 undefined on input line 101. + +Missing character: There is no → in font pcrr8t! + +LaTeX Warning: Hyper reference `id5' on page 1 undefined on input line 102. + +Missing character: There is no → in font pcrr8t! + +LaTeX Warning: Hyper reference `id7' on page 1 undefined on input line 103. + +Missing character: There is no → in font pcrr8t! + +LaTeX Warning: Hyper reference `id3' on page 1 undefined on input line 112. + +Missing character: There is no → in font pcrr8t! + +LaTeX Warning: Hyper reference `id4' on page 1 undefined on input line 113. + +Missing character: There is no → in font pcrr8t! + +LaTeX Warning: Hyper reference `id6' on page 1 undefined on input line 114. + +Missing character: There is no → in font pcrr8t! + +LaTeX Warning: Hyper reference `id7' on page 1 undefined on input line 115. + +[1 + +] + +LaTeX Warning: Hyper reference `id1' on page 2 undefined on input line 137. + + +LaTeX Warning: Hyper reference `id2' on page 2 undefined on input line 137. + + +LaTeX Warning: Hyper reference `id1' on page 2 undefined on input line 148. + + +LaTeX Warning: Hyper reference `id2' on page 2 undefined on input line 148. + + +LaTeX Warning: Hyper reference `id1' on page 2 undefined on input line 168. + + +LaTeX Warning: Hyper reference `id2' on page 2 undefined on input line 180. + + +LaTeX Warning: Hyper reference `id1' on page 2 undefined on input line 205. + + +LaTeX Warning: Hyper reference `id2' on page 2 undefined on input line 205. + +Package atveryend Info: Empty hook `BeforeClearDocument' on input line 207. +[2] +Package atveryend Info: Empty hook `AfterLastShipout' on input line 207. + (./test_rst.aux) +Package atveryend Info: Empty hook `AtVeryEndDocument' on input line 207. +Package atveryend Info: Empty hook `AtEndAfterFileList' on input line 207. + + +LaTeX Warning: There were undefined references. + + +LaTeX Warning: Label(s) may have changed. Rerun to get cross-references right. + +Package atveryend Info: Empty hook `AtVeryVeryEnd' on input line 207. + ) +Here is how much of TeX's memory you used: + 6960 strings out of 494522 + 97035 string characters out of 3156643 + 165314 words of memory out of 3000000 + 10143 multiletter control sequences out of 15000+200000 + 36033 words of font info for 61 fonts, out of 3000000 for 9000 + 670 hyphenation exceptions out of 8191 + 31i,5n,27p,253b,394s stack positions out of 5000i,500n,10000p,200000b,50000s + +Output written on test_rst.pdf (2 pages). diff --git a/test/test_rst.pdf b/test/test_rst.pdf new file mode 100644 index 0000000000000000000000000000000000000000..f42e290ad22c4d4d4035b393b4a3df2163859faa GIT binary patch literal 22850 zcmbTdQNvk#ojYJCeArG74-|A$jr(j zl@}JJWu#*TkRIJ#zX6z-2p9-#4J-iM+yHt}b1Nrf2YOK}eJ5jKV?$dbV*tIhv5l#d z837{`!(RX|FTl~s!C2oK;I?tBCS`m07oq1>?Tq3Nl$w{Mm2Ib4!1iQ`#%=~4N~T26 zMgsK$_R+5ESibSZ{fIpp?Hp}@AIh=2&m*kZx*q1VBtMNq#m-mak?H%=8B9{T@+v-_<@szIL|*BfL6OFw9B!`(TUpU zR!e_q(zaVMCrq7V_AetzMgG3tQAnV&OV8@02Fz7dMFI~A3}+zDs~{l9N;gH=lBHLZ zei~q4Cd<^LWH-gWEm0Yvj1+`QtO>e5i`}{-{ADwdKpV|-CRh-}BwCba_U9~U#V6kQ zotR!sk-4Bz3U)iyokud6AndN$UdR$a_E>A}8vCyuBGBvo3v4yG{xM(2w=ql+)KZ2j zTl4J}=uH!JWUdFq532*_rTjJ1rQI#(Gr7K{olWeKR-bGB)ezyNS^et3#1=wmwWvg3 ze0k30rn7(T{L|OJJ~Gq$uwlK|n!_}x9Q}5}+fqRuV9P8GfUl;VDMxFEegs}-(i?Rr z#Hr;eL(>Bl|K2yufAmq`8-z9d>ZeKNu^Phsr#tag6M7oZPcQSee67S1$Z530TG@7a z`=a}7$;_fI@w3INVQS|^`JDvpD0SBjWux+5qLtR)(F0#{VOB9wo3kaf`+hI-_*FP4 z1k*aRdHrltpuAbE=(ge%X*Q5Sahm;<)781x+4SKFMpnkuw!HZk*wn@`&)uWPcP)`SAaTe06Qbe?EvtW-0~D*z&0tIZ$ya4$Hvua>Sa@KeO|RKJZGB0BZ`e5kx_T5Uf+$KVilK4pr)c)erFf*-w)8 zdTHMrBQ=uxq=8Xj9YrKgC`f9eQnukoElTq+30(_Wlp(@=?g|eryly>uxX4m zOjRR-8`}qBLyrs}K#lD6U0^q7vVPK>1rhQgXDN;*pUk4KIgSTw$5p9#Z;8^;xsRhP z#TH|Nl%oDFy4d##);|dm(bixx!J=v?-j_VPy_0oEpDX2`{d#BkwoeAyDy72x0+PFZ zFFJo1KsaTr2#8LQGIG2%i?nJH3MHg|lsjcYqN1L|fLHRMQ{mlYA!o*iBJv0s)BGmX z^dCq`;p5(p@EXHFrZGHAziwy}B8S~uMVHO#YUk=rHyGVLiY-fqBg(g$)n#$Tl)9ws zDaBzDg@;^IbA!`m8W|(FdgX6y8Wo&~q@1Az;Ufg8V=eU$Qpz~CxFkbYwY`v`rt(7) z5d(6j*+|RZ1d}L22h<^Icwdd5m&IKzns@_%v5nFH1F-*W|2OrRS^gLC$H>C)-|#oA zre(YN7sY3{Rvs87elG1$#~@UnKZ5ldZ$(P`N*@n=*u*Noqi;gI1z?q#GZN4rhabZNa>d;BGEU*SGX6xdxL(}N8whgG>vdV=>9zBy-omy zTG)JyNs=>XS{Dm3APK>xF^?h=Jc200y2WQ+M&~T(?ya{ViwtBU`7r>1>Hli#YAqFIN|82wuON{sRFs&+Xu{urVMs4c`)nThj;_}v_^fEf*j8tpd zW!KB(SBvI$>ht}H%&cNhV^R??B6v#!iamM|d^Sh^W9Ss-uoe2IA-{xD64D$o2@xGx z_NE{u_0GgOnEgMsdA|YkdDuaS7>S|(K`;~!@@b?Kjr3l-zJkQaluBBE=>6C*EhZa%bJ z|GmWfb>)o#N~AW`!xMP~{s)#n%{#Hq^HW=^tz29#;e$nC~G!?Oi z7bdf53N*Pel(Nhu@Al%|Os04$wI5|3eJt6?G- zL!}(wE?3WMW?aTX8j9Tni{hS)R~iRNgY&(;`^`JwQ6XptKVIS|>C>%DPn zoSXi-Pks;T0~z=YiBBIO^k!F;)ykpg@Y7h^!c+_hW>h;z+G>t zmu8fzOg9}f8wYwyVl;+CD$pQYu1)H#m{2vAVzl*5D4{%-Xk?KtSffe0UXm~*r7US) z07IW(fmAH%@-JAJa(X7IT=}h2xFn&-XmoOUfh1g$Fn%1F*&#vQ?aQ}8uqK6~5!H@TW84TK;*zx7@z<{;-T=Fw;H6bwIm zwzA9A{qV@aAt9o~;|6hOvHss`KuvH~@Gvx=#?yeoyus+ftv9vz>~3ZIYvt$4uIq!( z{CDY>&!Oq#XO3RI(LK^&g01&?YQG2{uh~=HnQU+E>D&*;5BB$yV?b-#t9#vO9-q8` zdHjM1UfDd?J}SW5EAgwLgxGMdizp1#lWu1d{#}&q-ulhEwJvn;BBu`Uk+B#8zCv#YffP|Wqles_6YKQ;& z=)E9wm~XDujhzA%3=lK_RS`l3!TxQ^%G1!)H6=tOzgp{RRl92oTRT%@3Yl-6NT5_b zF=jP+j%{Xpd8y=V5>b!%@$ib%WSRA;_XO8p+;51!$L&nz{AE`mNh@W$8WmRcud+dP zz2Z-WUqxev)KFZBIFwY$-Nt6D2Rpfq!+iMWp8#vdbdB+P|Jf2ckQ2OrZ-sI`4{P&j zHCgLVA!Pd@t0;pLl@(GUulv|>h(Or%=xuWt@GB+5FLU%jiJvH;`)V>%$ z;B!2FDI~tnQ`JGp8rFy)BEbXjfgMVoEiP9sR{q9!VZ8k9a)?Rk4Lm^)te$}0SE0($ zcQK#YuajR7!h_bINMe-VvEK($o%pE_gFer;Uu*#r7uAq1=FZmM%Enf{N@$2Mx=|Rn zb$77X$-2_TH6@c*3w+#l`OC8Ma+`YA)iwKsi{|r#cM7Bh$)(`o4Fer_FH%|W+Q=<+ z6|+!elyB7W43AtJ-`Kz(r$j00IU!(gYlpI)H-1FrSap``7)BU$$92Phlqn zB8LRzMdOX9fW8&Q{UJS3a5@lH*S}8L7=MRrG+R^F$Acgv&mhG0mTdX8m4n-Gnv!BZ zUGXtSK6k$bzpduvNe@1}gW5z6zqa}I=5+8fIDeJS9PG{}7B^T5`GV=5(FNH=_0aE< zdlB0967c6-@tO)_A6KMozvg~1(US{-{0|i$htBTPnSo|j{1c;z?NCOZh%a82_V->G z@^W_S*$C=6N_z|2+l63c9Rpk1$22tIotOHgcgG*e2V5fwRDI>0k3~-kF!YXXf10F0@M$+_9Wx z+!Ve{mjl!5mp%_etL3Y_kPRY$oRpDNEVi~ms`25Eit#A)YFJjiMUr`Z!wl;} zXmn9>+mdA5)ZRK9P*COzVS9h3^`WX33b8hh#WwO;W^SIU{3RJ}Sk`O7W$ zz9T#GkGyogEc0=Y*69QdT>_Dqf1jl5&`!Hi!KOq{NSB!drtUkY-Fc)yrmEeCt$n$| zHi#NUlHW8fOb;tR(03ew98>O&yw%#hjU#Npk7N&8r=r(XgHy|UOtz{8$iBPDUcTkT zKDD^JP{Mi`*8x>RKHWw7~u^K_3mwyaD+Ar9miHDnV zE|ER7(^o>1pTKEm`Y}7<5Y~2s*p4lav${iR?7S1oGXF?7|6$Dd3wh>(G_xtT9WC40 zXc9F{w-9G|Jj0|`FQ%3Y>`8eNd(`E(^2VUW(nV>PB8^jU@|y4^?xtJ{#bAgYu_lh* zG}wyLopi*u@OcvZ`Hy8A@_Uy!i*TrrjTjI8JNg;}BCU9;P`V036|RaG*MaVAu}IEO zP{E8cf#BcwX_6p20m1s zeJf!DpE2=;mVj>S;@#qbot4+Q;TkpXMzO*gINQMecbZl?j6KaEbiOICvY@>Ix@OS) zQ+f2=KOXvyO!{IZsXKge_N8h_+4AH*lQCEFYQz^QZ>Vk9DOC=eR`ajI7YyfW=cdV} zD7-HB)0tAg0u1E@h)!}xMu(E^lPk?dwU&6SLhZKh37R;qyURO5qF>d3RJ1p2;5LZP zjRZTK(#L$2?)khQ9a?+pC|?O-ZOR%ff?MkNNI|ug*QcGAC1XEBV&NF`a#>pYUSQ7Q zJ@KjGS!Mv9*fwa#?^b*=QP@PZ-29M9aYm|I9pPN`k(=2`+0>!yG1-Fmq@E;xAII2n zaye`WFoU3gi~#O1zO)P3YVjaq7`1p}smal(5U5oeuU}W-Zu|_HljP-db9-Kai`Z}v zfhM#-tuUdUn<)#x2BA8nOk8;gZg zw3Yc%HW?!P#=|BieMO+DXr}iG{oT}*NiLieSMJX>9Hb!LfeC&%utpl{~MLYNIg zUv)@H>~-3bCanARRuaf0MM-ut$-;oN@l;M!H3H;3?!@6cNQr`5wBYRy<@21%YNwEJR&^sX^e;>ex@4OEiGoLbd2tmc z&d=OQQ@2sS5*5plRB_GMd@_ZVkNDX^zRDB$)72L^KKd=V!-gjU1g`5W=Y?L&p%n!7 zY)DRFFs_fOPi)Q`}fmb`;-Mq+8A0M>9UOh!y6K}JYnHM$9< zkD;}&)Qtj0z)HwIBlj5hx91BwZfP05rKno)m0|v*22L zy4n0Lxi+)oYngzB3%BZqqqrSC-2)|Y%}R?t@Fihy`!2Vh@Ts==7eb^RLESS%r`wd- z)bJMpaYNgKuiYM|r;T&M^k@4{yTa~{+E2*}eks!*Eb;HjsPZwHD&dBr(!H`Ns-q}H zSg$B==n2j$s%0P+O*?4>CaQ@Ge~JT>+m5J4Lfz9e9G*3fNO?gFbMg_n& zSdLDOv5)z|aJ~?>BfVsMUVkRZ;e#8A2HY^mArLqQ^40!5BQzd|dBFz_#OuDVe=#op z8wLaD>?L>UUrpnU;e=a9`%$nb(I?|d%7YJhL?CuQ;t+}G^)0KBBMLCC&I9}pKjzNB z+JJB)Mocx{!;iNc{-Ep*zQcXG#>1gAyb?vY&TR9MS%8(+i|iyV_g&n%xJrGBYp?p; z>q4rT44Vum7b)B1t(5vsX&>}MhZI~JtKYG9+?AWy^{D`K1beEE43A9bnem=3Wt0Bu zZVsNiW25R`-s~@!H_0>fq)&lVlg_3nkt=T`H^1F!Hd}vMFoHTu?Mg2moua2nEgpSv zj|Fap6coDqBTlf0mkiX7dJ}d4Ry$3|M?W$l{=v}UWo((~gQ^lzKj%Yn2M%sKKPR_H zLbp+4QCBknkZXQ!`tep7vI2G%T(*IR(@yXaU@~}FazQWT0zZ=CXeFX%nF+fW4a{0u zWpfHF=5xVnUtng#KJqK`C>u$e3DFYDKpvLXzVB_lC_5^n@VTemUE!_=^R)tdP^4#` zSYtt)E0~Q#)51r-N6O$C59^9L#uz%=;pmhoLcT1&&QC@95o#m0L z^u|*JWs<=x{4*-3If!!P$n`_p*^5B6#_Zw=RSiK}{F13hbk49|@`hW#YIZ-aaeiyf zxV(k?xV`7ZBF|Ulx7AhPe{i3J?F~ew23!jD$uqGFBfaWdhGy`{)d36f6l!_Om8cPa zo#kvjwQOTMbbjF5@%IkAxJR3}PXKkrR5Z5`*B==7UUX21$H$wIb+p})QOj;9);)uv;5u#|cah)!9sSvIoQWH`D`iqO6`{RMMZFXh`B9*Xj5mgc_wkolC}QJZZiG5NGl zjYr!6#ATcC-U|&%pVJ$`bCIQOEH8tIt0!zB_wX4JO!06A*mrl+=4w%>F)P!NBJA64 z`)M#hR0_9;PEqGMn9*QlVq*^PUoFi#9aER)rJp?jHc?*3v~`_xvos^rKf^|9SYw)G z->)BG0$ll;NT{-D+AdzO*=Tp^-z1E@saDb-1G3+)66$aE^J?Yu~uPN}LfENhS`d9yHX`>JYvgtA0jO!^2+N+^Mk016kygvh8GS_{8K z6tsif(d>1yh~TII9nUg&1`{XV5gH@LXr8AHJlR|P_{UgnBYmjn%H>H+#K*l{d@-M~ z+sR)+qNmH!-hU}!&w&1|%1QeIFSy$cD%pUem@TMbtNl%NfUo`60cs0N+SExvidRxN zSMRNRZ>1x}DOqn*j*qy!o^hZ@UWk=wR;Th5MIhmt^gTX3e=*6g9dvuMd4%^XExo~)XZ5M3}=V%tS(BqN|c(k{|(iXtSOw?4Ds{^zUcK-b_ciYV&wEv7qxu#%jIOE8K+f0Cj<%8e zNfvg$kV0K_$}Hww`I#AKx9N!4!Mqxunk_OIrspCcKG9`K>s+I~{ug$(r+Pj(Y_6Wg z!!%5&nUON0=l#Tt!Nw@RA^k>w1t^o$RMe>`evVVq7rUIj#dJ}#gX&}YHM)h;GIjn< z{h`v>{La9LPW-~&eNbP>f_|QxWxLGsQ9GHN;izI&DFrR=QU~EhpHH3AOyE=J*QK## z5JVY9iO-{0K8KmeiYk!KOIQdc8~!qR`t#Tp!>w?$BuF#7lz=TK+(V5Od|z^NQ%Yi7R| zLYovBzLa2y`uA|E&_4QOGIkUb6}^ns)ctFm)0G9j}|76 zG*sFJ8O3jWFIs_J(ckD|IsOvCX{bfw5%(^*7N=3#c)+W-x;7U)ZjJO4&d8oN+o=r#_vua;x-Dk(r{^rE2aFBvWqEnSIyw?fr&R;HYDFY6-gx{Ms1+hW z@qfrl|LdLmeZ~@+fK@xBHCVClk%WIQHHj~6OG3yqV z=F)L}%U716X;tgxAA3s++2^VEk1wCz+t=sU<|qJUo)OdfEHc~hwL7tiM%7AO3hfmO zrh!cSJpIt!xu&^mHG3`+`q)r#e^S{KvWD+{=^r=TpNG$aTUQ9WB8px|ISI>mxgwQc z5{>fHaU=uCV&HEIOjL@>)a@j1(o9{<`h(&-Hdiw4R;%>~88XKnk<57WV^vX6Q3vz) zEqJai6WuuzjCRr^qR`dMDexV$=slNvZqw-_j%d|c$IOF+SDWbQgMmVW-J=3GVq1mt z_O~mRZq;cK%jGVGz1%|iC8)ThOY&a9NGOxvKS~Z4X z#MZ|a_^}E}7HhWH60A`-S~T}XR5m-z0CK(vKqK{Rru?y|93Wd z-Cr$Xz(igEf<2PQP;eo| zx$+t95D}@ty%)+KzXJN+>Sg3!SpYA@pd^%J= z5yw}JOW*SL!u{>o7&ea4S*2N2)b_7Fe6RqFqkJ`G^tL@70^L%f65SwOsob}3%1E{+ zM9pZ`Jn<^wTE^MV(iIAf)ZiZbwq^vffOBpvGPL!-DSt#w1zgRW@W8f2{Aq9|xf0hi z-6bzK4uGTZ{D0z^?^^fthe*GmjcI;_?ImZ<@O0gfKY4OnT&*Sh!xdDk0B)Ph4w~a< zwlBp!eFLS=WM7Gr2u#J}o~9NtaxQq*_qm!;DbxpsaDdN;sJ}YFP&UEyAR|zRM)%fl zwEP#AWAI?qqx<4NkbDU{<=AULU1dWw^fOJHk9iK;QrL1AsT z#jC7b8GE6A5-5=%oN_pi-d9rnNl2obZ#0#0mDIa1#aqZzz0b(fB;0((o z=)55ttn&L>ka)=%5?NdHUWL97O*I*B@5as<=zjFTa=Au3Plb>EtSVsZ_mIM&M2Up8 z+}D%l;20vz3VwkpEJisN%D8_1RMX^SRD{rL*7az=_S7R@^;zUZ^eT7M&<49@VHUuK z`}e=KQYtw#X`tF$uDc2ch^@=x*-VH?Z!k80#KG31=baHUHmjhSiI@jyu)t$tguFZ3 zU-)?B8?xQ)ozVZ7<`x(BP3YY}YBO-(q8#9}C*9TOvrgEXEfTT1_L&&V=ocbV@{o!L z!DfVxLM+-!NhzwTs(3u0r>LoH?HW#5KZhX_H~}DRBt0o7^;wuV_fORO_Yr8BoAcTl z@RtIfv7Y?n+-+ONF~)2Q>=cH2H6sZYWAg*b2wXmU84pO@t1kBe%=N^#+&KQ-H~Gwd z@6fVsMGTjY*1(`4SvUGyg0oDf0sD$Tb=&IkdGIysAKJ8q_^x^+&7K4{c@&|-28uX~-{skh>yD7~56 zlr!GwBJN>Zc259`AV;z6_54vupOYnL3Uln1`VqJ1k9>C5L9ECjM5t5ge-bU^s#0C3 zQK`EGJ{n>y-hk6jUN@jRXlwbUyuHN1Q+;?VTC1hiW0lMbWC28(s3CLjp@F^6ZE?Qy zGuZ2QCtS_2fjO_~+cbH}Da2e=>4V>b^x86%)^c4LgaF<3j;!DfG@a6NAv0RcSshc= zM}|Ko*;BnHU{h=_W}!2{i+wSjnkW$~KAiWbo{Xnx)UD&b@tbpw6{Uu0xLllY-Ou0F zk7=1p>T0K$>`%3aVH~J{+9yh`IcKN0pF<^1+jDqwpkgKBaU6%yTX1x ztMgxU9MH1pkY?EFX&Kz?9K&n`m@I}{gL^_O3rRq8tf>ZUy0Ph>iUPiMM@GeV2nz>f z7e9n0DF!uFE_9yxV7Hy|zxmTyYaKS?plk4VOsYEa5-00=)9)Gocxn9~lAK*2h;89Tfwr zOnO2kH&c+acicHs3#lBjOGvRgL1Mv+FKMCMIf|k;bg6D@q<9H$9Q^TTyxFX z|MZ)uvf7CHW^@Y^>nF#ZX(m*xpK(%l91cmJ#Yu(bLaD407%VCHZ9>VM}UKw4CIbL-!bd)MEZoF=i-b0G3I4ER!hL^QT*?3?EGb{WS6lf?idIuTRPu1sk zQE?OJ(Ph}TF4nVxS>JK;N1nMRbV!Oybi%%U6EDcEt@aH3SaDC)UAO|7_&7HJ|7B^9 zZEkXQ!n^eQH-H@K83QM2O z+O;e+y0W3UhwA4ths;CFYfNlK<%-slp%X&~3Et@;+c*`$SWPd{j{a~`#8%Ymf8b*u zEw}PbtLSqbiyQBZae2dmn*a52D{y{IY0xe`&$SX*aznSRt;^fNMN=no;# zL3rus04tf2J#ZK^WfC3jl>c8tE{4f`ZBJ2g%eQ}zlCp;&-&_9D!`v!q1Y&CGOJE2!Y>{cH za$;AUNA}1#u=oBQBQ_@cn{@`};<>{W8RSC5hR=8byd31*Is9o+ zegc?c=9bLn%Ty6d722y*6b+~I=+*>YWnxfss)6YX*cxfibnXi`D@9th?L_#!VqIk2pE>h;e zfI+d~G9<&NjrNNojzV;b!{>nok{_M`e42w@S5mihkXgS zL<}T4cx5t5)_JH0d2i_eDpg)F)Jo|EDxPC|i)ilrPf@%|S;aCZD(8e1t(($hvB~D~ z33XH-iFBk&X*9I1qj5=@0?EqZ-D9e1Ze8T;A|7S=Ao6;CJRt&^Ob92|tp zFEtx2m7~3qr7C1Y?ym-SyZH+;s2nE|n!I9kb)SIdEtLRgg+Y-U!`p=+8Dk+yPmuM81c|cLg@qBM!WDo&aqb*4 z#&nvrmq;ZS3AV>DuHhcph54}RH=6Yz|1-_;-B!{alAL=@#dgBT+V&U_2!#+>L>OFd%HZzCN2ECXyVXnh)GRfOA8^txcm4Fkx-f#Xc%r(SD%rk za(Z1@K2&}x5wTZFbUHQ4GMQk%_#nZs#|Z z(;EDzpKqWV^)9GjeGTbo60Dv2au(#x0nWa&`g9+`f&o=+%><>Kz5;@_>Y{vMvOf>B$5;kfTirtsZOm^<@u>Di)T(sd|i5VP@-JeIf zI{Yik?LQmk=U`<$K8XZG&Pgd!+T29v|k1q6%V77g|$(_Ls_q>MI=LWWAg|Q~>Jz_wB zR09`+--d?PuWB&lQZ}L3fRo+nT(?G7i`nk*ctNZ4Z-YDbF!mPJp(yQ`Rn!fwdiBqM zH?l^id`@fZgCL0Hr^CmgaJp`NAR^J1^5GCaLhRQwCYCkWmE<=U*f>ttbVIaUfh;96 z_bYj$fqvHe6BN+C?}H;#OjMQ@1*_&93fr=;MMz;y`3~pZ>zGWOQRph4!7NzQrB!M6AJH{ABMTZ_SaughydPbrJk zFe;edHupD7v{@k>r=#AT%ga<^ny- zVgmgf+1UpGpIy;|!G*qumiY6lY=tj$RnXx5|9a@CJS2<)@b5~$ zNX)Kk2ls^6TlWlfKJltUKzT@A>4U()r?eS#2}(vL)Bs*?6U8k9-&sA$J2SJVmFZ@56?7=___DBA zp~ce$Yt@zux6@q-?yQbdxX4KOXRN_qgJaR~3;JC#jB_nQFSW>RRmYxl%QyYE~<}!j7 zj?+HAy#+@boo1dMBXh@V{2KLgxP<0AH-`iq2bzEU0AU-B2Vc#By$#iS;yXJy6SytHDgpP6I!6xoQvz z4Nn4m2rU-jd|)6w#y{Ss4+yj4iJ}con_=azTwJ$1pTlGfM6$-tzGMBq_D|M6q_-18 zSFo)?dw09I$HClley8|@0#_n-^B!I8^9}8A%8Dl3*Yzb<%?9;5*bAE|0KI zZO+J?b@2VmX9*(1=EmTiCpl*4q68u)xhL*$KQ`3w3O#cvB|1_j@lv%jyVsG{ol!V3 zbq&s5pgQWBj>uWoyk5arpMnPP2;m9)QYDWwNrW`ro^MIxIfJD;o|HvqiM5zGIN_*m zXrfGebKsL7+xPG#IQYRb@EAAI9|iusu)()H{xzYQ_wf{&)96b~d@jo{-`e{yEXNNY zY$)V4o*vhkaC3ibf>xsPiztEu@g48rQmrNr>S-f8zi_JV6;jbyp-Al?@S-V%c^(5~ zX}rI&*xMVgiGndC{=E(GV-{RxM3cn&dpA#iX1J(agW`z5AB$xr&EdZsh3@Nxl#nsg z*L+4ly*xj^ymY@GyhmrwTJzXc7FCb7Lh=0gPOKSg?0B30U22#sY^^NJi?1remBJ=E=S4M)9+it`O~ znF}8{vQByug_nQt^6I-*KH-*dvo-eH7X)YC8eUc`Cd`G&vT{?*tCR9OD(;Ga5gakI z24a_SC#Dx-fD^8F%k|XGa~2;_MBnQjAVqkA3-d&ZdSlzsemqrUJ?*#A=olOzl4>h{ z1t+Q6aGY5JzO_HirAAd>^J`q6Zl&kq)I$SarCq3Az}Ham1I<65KTklZf-3PmYDORnPXidKcy6T~`ThKG(37-**xG7(z~o z3k{GX-x(1+emN+go4XZEotKm6TX(7>aaRBmp%4C3nl`hUcCO^bBPKm8-Rt>?-qINb zmBkf>DydGw5KXk$;KFi0;!hxgmE^vuDuK10&J-o zShFRstqxZvnOL3xR>Ezl`Fv6Bp@b9$B(=PQTz0cB3TSwX(AosIY-6={YkPl$X*4ih zIsx;0$U+RCy@qlQk1mfkj}F0ZFMVji?$M47-F$Iw#>y*11EU`KCjPfesL@_TwVGs( z&4b(tq4heO1>j!miQr0c`GY=i-hZw|VWl7a&Apq-q z_f&)3ptUEgXs&GU(VqWMU8Oq`C=<6FwN=HEIn{lV$MxgP0&|ER01c``!Xrj?bU=Qt zzXt22`O1U-{v{^fa>a0()@c0xPe|XUNANTkEd*i_y)(9P4v^?6u|vBNYxiD1V|8kv zy8!rM?%Qe_{3|ltNd#qnT9i?=i8cc*0RyZoeC2OQ!+lIzkNjrgaIqTc8#n)h-rqi= zL~dWI&R8n^wdD`*rbZsQtv^+t`fqd$$jO_Ag;OMscZ?lb*Y)G$mQ8akRHsIEbbPe# z0jk0Y=?y!u@?fHUv8N7`;R(%`WeRHEm*?@z-25+;j90c&bG9`EmkMb-yhj+>K&UMF zm5aEC>r&i8v+B-h#C@HnM0%s)aNV!{wc}j6BgfgrMElDz7x))_W2O|}|6t7j{-3>K zu&}cI_qWw2;lg|9dX|GeSU++_%QyyD~ zTkn@dfybG6V~u)XyUU3Iwu6;NGTkD&TT+ST za|jV4!3`1Wts3-#Bf?W!cRNvSYuU`EvpJ2NLNzMlii^qxVWd(iVQ>~AznX|55y=V4 z>1Leob794p4^H*cta42mWC)u@w05yIsZn%?qM z5=hnwB;m%A_tFZIA%Y~|$DtIILMb!~5==%D?ghz`3I*bn=;RV(g!V28>Ui2|#0in8 z=Sal~UXdh{3Kc{OB}n9v%9upqG!hlEa24dgrAA}QMD0X@Ri%ljL{ywY<%(pKBo4`8 zW>GOwAH$+5ngvv4YLtYN1yLvs%pAlLxKAg$mKDO`(v*tfjVb~>5W@vV;fi!JOGw?q zN!o;%tOC_ZV~W3p-ZA2YvNFmfQo|Dp?fqq6qkQwf51a~gsTN|hCQ&yZtv=zBlqn@s zpK@?Nz8ddf>_h=s!s%gtVoee#!m%t0ga9J(gePA%c@)b7m=$iSfW{0>sp`^}h8Bfz zLq(w?LzF8TNvS;M6f7~B0x~7-3)Tp+ZdsZ9w7s*qAsA6jEu2=JK(v-+vg>3pKB zCzEp|aG{PQR+?$SIkKLk5u-nbSvFgRYMi6BIJdcVE`py!#mJlF)j2E=7URNC%VU{; zRCi6Snh(D4e<#aGuf-h0|)S#<-RvLm@xAq5VnTCkJsKiR7F0+s3_as%bp z2o6G#P+JVA;Jp*)3+a#K+H>xYV_CB|^)ZVyl^l~Rio7u;;$SNpo;)Gy#_rW zY>!N4d!wEgmPaABOERoMJVZtVUHA>;ihXrw6Hhet{XB8C%_^o}qlpaDtM5MbEq(i!6=#cJ4dVryXp~0aWq(d6% zk{X%;=@=Sm7#ffNb9K%->-+BZ+H2pu`|4TG`|RK2*9hJN_pQ!9`HxLM;xCRsY`ip5!W z{Oa)AAzZC;x(o=3dx(CwR`FRNB3}`E5d-F@o!s&1$>2sUZ3@x$biEkE27_2Lzz2Q`WAlu z{<91#I`QWFyKiOklw@$nkd+h%Lv#`nE7DbEigQz`V~9e374Gp9>4hYt9?P@tC}_um z_%Bay7bJ}c&R^sIf{~$pM{XTZ@4A>Z3;W?uot{2WYD(XZy~*^uKG@-w5dXrRZX}xz zs>1&Fv(9@$;Uo~Wt=B6gIyeegP(${ld#F(tw^aooc(Ur``t15MYX1Q0^((+IHX|5Txu)tz|1{ zBb+OVdQl?T-O1h{0nNJhwrwV<}wh0iKV)=8R4PXnWu0Rk^GUOUnY;XispWbpx5jIjE zre4R8MEe9y$<8i{q1u4VJ+3VQ!3hDGigNU#oogp`2D2qt4VZS7on`tsq2gB$sEu#C zVTfz!(#4&TlUk{LLX$i&4P`SuVQ(|;c2#hQz?5PwG9_nns?vb&BYu8y+j%{nB_yAM z$}uW$c17tC_2-z#VTGPFRY&)W1c+gIskSsH zyEK1CAe>+U$x<&`7s0!0=O{;d1F0AhAJ;~9Wkka3#7zO6!k@BN9QSF}qdQm>f4~%Q-;}c#8EA=y4HGluKGuoetdl{$erMrk?Rjn$Vs;*%pi%9gxC{(^vB~w+*`2u<=TM|030#g?D~;;Pr!_P!LsjqM|f#GYTz z5%TBds%=k6HCL|edbWM57+`nXUr*#a^W zc`3hd>gQQ+jxJl17qu>h$T$v^{~Sxd*bFDHamBkt@e|ekB=7Nt!h;qM0z3Y)y9B?D zODlPt6z;S$-I-oXn3VW=tw=G|rBoz^ms7IMg3*D*u0p((t8~2IRy{AX3bbx zQbm8s9kMn2^;(#kl_;uI)Ttp;Rap3by08VBSjatgL$)9d0{v(SM@IT}Oit~oonHOn zu&w9MR$+f9D|w%%Z9BR2&&l-W{laQB&H7yRUP|Js5nt@PAnaAHsPZ>ZD{yvcZfa_# z!Xf!Z*eH6{8n#nv9Md@m(*-3Vv&~Tx#&x^}_;6X;&Oqwo%Fn(oCFV zi}pT;DBGod7Q7y~IP8;YY~wan4mY3$UrxdO5$)z_^iqFoFFCGmu;?bStD0GE}Dzk@?UQ zEkPv=BlGe5yW1Rg1(=V2%PT=Y$o1yWUv=M=vP^N9;9wn?E~k4Y{`u8jHosj#?cV;~ zn0k$=IG&gFkBD?9;aFy;KCF-Fm6)Nuo<0+6RHAS^FTe6dz&3u740V|2Ei|>miD&je z_a{>?O7>v~kFh@NFCC?-TxVW;_qO1RmSflVa0pZD+Qh)Z`0&Crs>_GyWypU51<VA&N6gZK@{2A2bD2?sAK-*?$2G$0juGNg zU1RS$Su;jdAz3q-N~fX{Gtap5{enB+tJLRP@0OwU%Sl`jt#tJZg$2D##Vb!#FUPd4 z*>?|{#p-jso>bs4j3d*(K{ch+s(aR(5d|Q%=t!W2$ zxMikxK>q8>avsD{ifiiRK$FAX}M=zF*>lb?AYL@8$+z-`D zDRpW%QAZ^ATk%7BrJ=EFd7#@6a}fpg{Vv*__=f#G0`iYzRU1J)_I{hVGZoAozcWsW zeuoX(tH!T)9MygL+_cz>;o|0(eZ^xd=;Cg$qE^bdlI)_2&R$)^5IaYh!@2P6#Ik$3 zRy>OtJ#DH*Ph;KwXuHq;jM&)e_o4@dY+^Cph)Isoc_1=m!GCk8uiC8IIcvZ2$Pr5( zugdR>1~W2~PqrK&s|(voTP7B1)#W^jN?^?4XlMQxc}2m=d|w5Nmn@t&+ddEB%%**e zF^(W7L1U-aRm1~9YU3IeC6!C(t$dPByf@$C$++_+ap4IRJAPlVTUPk|9>XO_9lI72 z)kc%36n10VDh5KEK^a27)7ZB^hh{(q$$pS^kC*}L5>Hi;hRTZ23hH(Vb`CcGI>6gO{6(8o>FLgL43Y98g%k|MXzTXI#2GcYUDLDaC*yQvk2MIf4x1qmaCF z_r(MK<+)FqjN|gzS;i!A`$+@zprzaram@k=tbT8Mp_13C6f4=M{xgJCJ;P@>3EL*k zy|IK}8I5E2+O#<#<5wk&8=2ZqbPIU${d674;LGvK%==4@DZ5zuH+w#ZIl0Qv;U7C$ zN>^mxrAem_vuTO{@DG_#E&x`LH!+G3Z4;@d0iPp7hAB(^UT6!~-ImYmKl29ADdx&E#)4DGw&n>Bc zi3OH4=Qozs&<(?|1STZa5g?cfe-&H-qB>Z?6E9I611lWGYWUY&3>>RD@o<}9Ifx9&H#{|Ed^pA^J|~uuv%FwTZy9 zWJ4WPGwCXmwYPXTnN>iLsG_X$LPe^PA8MZ(P>iLUO>IN-^R}l-O1p?MnvbXkQ;Rws zsA8}ICvv7YF3O1zraE?-QYq?27n0PR$TyS3^TT=)i{%a3qNn)7a#}$6gwBGBK~_jq zOz@Ns_MDv3@!Vk4TzQL$@mjQ@jH1UH6?K3IEZI7(qx{=`TD_0ug~Cj~5ST!oB?=&C zq(rO5Mwl!-%)c>`#L6>Y&@IEDQ5zOEhC&=~PQb(>l#*{3)e(m4M#WJt%JT+Wc|#{m z(e0q8DvFb&XVV>Z1+@>CJF)lgd>%Ezw=0on`4fl7z`8B5z>Q%shz`NwZcd*EFKdXHXQtjTYB4_c6NK_3#J zQc@IA>HNwx7>&o@6(EwL3|mh}^~5Dq=HBpEJ~H5{T5qw9m=_-MFbn`v<~mj)j|~c} z2$UCc$wv1g+dKWBM&X3-iIexxw&!!PQdcP3fC0_vX}p8?ct6s7`vH2L#G{A0vllY8 z#ha@hylrfw(`Ty(i)TUSB{W@K)aw9dR=rTC)(kn`W&rupU*1IK#ZP|ek| z%I~eK>x~+fnmqByLmch65k-2@yEW&*=(nZg-uTky4g)qplOPkv>c^$|pSdwqvlBL4 zOEC3dYqpPhtK*~a{S7Z(;SRQ;F+NA9U|%d5u3tWbK=f>#{6FSsUV|ORJ8%0J!Ad9e z86KdNlvfjUsmnOy0eljJ#!Q_2=A+lSKXT{lF97V_iFz#{aQ;16U*rL<{L#$YVaUt+p%UMz zzfH1y&p8L~zweImO=9%vq)1GVZ*mU;zm%ofxeU zZH=EuCMfKLOS9Gg&Tmd5EOB$wqir%pBra5dw#!%RTW$;WKXL+hVz`qpHC+7T zU6~l0dDf8qJHh4@@}VkU>=5z3vQu@eLa{%1#hp|_UD(QHVt!t5R3h$NKyL!fJjiLR zz>_K?rWNy(n4NicF?du=Gsb5rq!{(qqAoZa7twHcWT#A#RiGd~9!thkDyf^Pxo@O4 zP^^i@(e0XlnOvo8_m)*0uAQ4mtxFFV?2GA9f+EbMY=RIoeDw7PRkr>r|Bhk-EbiUOrW>Ds1y=5WZeuL;(A* z37aw9Hss!&Z_}=wZ@n){m&_`y#`0$Nz?>9x;OlovGQB;a%{rFI!RD)=H%xCC?fod2 z7|YufDmxTbWe=IPB@CP?aQm%)7>aT*55{x>WZ~`&&jjt0_{+^$-X`&42MfI0r(0ua3hrVb){={D448s;}j3bnYsnp98fDWjSQUQb-=(IkhY2~ zA`QZQu3#a(V&@x&DN%K!!#rkT%mu~XL);8vBl8UIGJ+y+cj{YhZV)4MQ^`$z#k@^( z|3HRbHM3_4rvSWJFT4zKm6)C0w1d2chlA@?3giRoqbnt%kNX7~HuNjrcaCllSN84K7Dre0k68Qg#*iSl()) zxd4f3hku=2ZXYFKjDPdb*-~VS`BU7<9mEjbRX*rq5kgb<#pG&aq?Kl!%Vp~~uY2v& zUkNsXLYtarP1DSAvsP&7I+B6c!HP!sYTx&9Q~$z5H<@UrQ!n+8jIJ8vmGbpm?ivT$ z+@L=0x8mcdZR)O^)0OzH=??%mPR9OGS^zx zn;n~p68u0XlX9QR}*%`S1asO;zR`ymFz!%f}Yma2DC(=xd(RdY8y`Cg2^$a8qJhYiNdzM&e`DQ{ zPr;M0$rmqr63~|0t&dIF} zl~Rb@0YvT|-7b)6daj0t_Cdig+gmE}oYgbMf6h_c!KSv|ntIucU;)&Xl+7p{X>{cz zt(&D$k{PS?12Wyxozvm}+^< zEXxSwvlDQEA@S)goQ6I|zzj*zx$TgtyYoQ7%1ac{_Aaivks(y`cTo%c-}}zi2_g0R z$)WB=cM>s1#2WusSPK8ogUA0?mik)OHhA1xuC88hc>k$REj(^T7aP|%|C#$&Yuo!< zzY*rg4P*--ULhz3k-Js zL8|#5p`1>Z0L$O~xJdU9OPazMhB>!r?Q8_oZtzT6%a6yjLoQ&K_Uh1~e1(tb7B06h nB(IzP5&e3aQt1Eg(9`R)hnKI%zqRr43I0n^GctZsm&5xnkvfex literal 0 HcmV?d00001 diff --git a/test/test_rst.tex b/test/test_rst.tex index bf9171b..5e6f739 100644 --- a/test/test_rst.tex +++ b/test/test_rst.tex @@ -1,198 +1,207 @@ -#################### -Test Program -#################### - -=============== -Jason R. Fruit -=============== - -.. toc:: - - -Introduction -============ - -This test program prints the word "hello", followed by the name of +% generated by Docutils +\documentclass[a4paper,english]{article} +\usepackage{fixltx2e} % LaTeX patches, \textsubscript +\usepackage{cmap} % fix search and cut-and-paste in PDF +\usepackage[T1]{fontenc} +\usepackage[utf8]{inputenc} +\usepackage{ifthen} +\usepackage{babel} +\usepackage{textcomp} % text symbol macros + +%%% Custom LaTeX preamble +% PDF Standard Fonts +\usepackage{mathptmx} % Times +\usepackage[scaled=.90]{helvet} +\usepackage{courier} + +%%% User specified packages and stylesheets + +%%% Fallback definitions for Docutils-specific commands + +% rubric (informal heading) +\providecommand*{\DUrubric}[2][class-arg]{% + \subsubsection*{\centering\textit{\textmd{#2}}}} + +% hyperlinks: +\ifthenelse{\isundefined{\hypersetup}}{ + \usepackage[unicode,colorlinks=true,linkcolor=blue,urlcolor=blue]{hyperref} + \urlstyle{same} % normal text font (alternatives: tt, rm, sf) +}{} +\hypersetup{ + pdftitle={Test Program}, +} + +%%% Body +\begin{document} + +% Document title +\title{Test Program% + \phantomsection% + \label{test-program}% + \\ % subtitle% + \large{Jason R. Fruit}% + \label{jason-r-fruit}} +\author{} +\date{} +\maketitle + +% This data file has been placed in the public domain. + +% Derived from the Unicode character mappings available from +% . +% Processed by unicode2rstsubs.py, part of Docutils: +% . + +\phantomsection\label{contents} +\pdfbookmark[1]{Contents}{contents} +\tableofcontents + + + +%___________________________________________________________________________ + +\section*{Introduction% + \phantomsection% + \addcontentsline{toc}{section}{Introduction}% + \label{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 +Python and uses the \texttt{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: +\section*{Implementation% + \phantomsection% + \addcontentsline{toc}{section}{Implementation}% + \label{implementation}% +} -\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} +\subsection*{Output files% + \phantomsection% + \addcontentsline{toc}{subsection}{Output files}% + \label{output-files}% +} +This document contains the makings of two files; the first, +\texttt{test.py}, uses simple string concatenation to build its output +message: -The second uses string substitution: +\DUrubric{test.py (1)} +% +\begin{quote}{\ttfamily \raggedright \noindent +→~Import~the~os~module~(\hyperref[id3]{3})\\ +~→~Get~the~OS~description~(\hyperref[id4]{4})\\ +~→~Construct~the~message~with~Concatenation~(\hyperref[id5]{5})\\ +~→~Print~the~message~(\hyperref[id7]{7}) +} +\end{quote} -\label{pyweb2} - \begin{flushleft} - \textit{Code example test2.py (2)} - \begin{Verbatim}[commandchars=\\\{\},codes={\catcode`$=3\catcode`^=7},frame=single] +The second uses string substitution: -$\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) +\DUrubric{test2.py (2)} +% +\begin{quote}{\ttfamily \raggedright \noindent +→~Import~the~os~module~(\hyperref[id3]{3})\\ +~→~Get~the~OS~description~(\hyperref[id4]{4})\\ +~→~Construct~the~message~with~Substitution~(\hyperref[id6]{6})\\ +~→~Print~the~message~(\hyperref[id7]{7}) +} +\end{quote} - \end{Verbatim} - - \end{flushleft} +%___________________________________________________________________________ -Retrieving the OS description -------------------------------- +\subsection*{Retrieving the OS description% + \phantomsection% + \addcontentsline{toc}{subsection}{Retrieving the OS description}% + \label{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} +\DUrubric{Import the os module (3)} +% +\begin{quote}{\ttfamily \raggedright \noindent +import~os +} +\end{quote} +Used by: test.py (\hyperref[id1]{1}); test2.py (\hyperref[id2]{2}) 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] +\DUrubric{Get the OS description (4)} +% +\begin{quote}{\ttfamily \raggedright \noindent +os\_name~=~os.name +} +\end{quote} -os_name = os.name +Used by: test.py (\hyperref[id1]{1}); test2.py (\hyperref[id2]{2}) - \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 ---------------------- +\subsection*{Building the message% + \phantomsection% + \addcontentsline{toc}{subsection}{Building the message}% + \label{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} +\DUrubric{Construct the message with Concatenation (5)} +% +\begin{quote}{\ttfamily \raggedright \noindent +msg~=~"Hello,~"~+~os\_name~+~"!" +} +\end{quote} +Used by: test.py (\hyperref[id1]{1}) 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 +\DUrubric{Construct the message with Substitution (6)} +% +\begin{quote}{\ttfamily \raggedright \noindent +msg~=~"Hello,~\%s!"~\%~os\_name +} +\end{quote} - \end{Verbatim} - - \footnotesize - Used by: - \begin{list}{}{} - - \item Code example test2.py (2) (Sect. \ref{pyweb2}, p. \pageref{pyweb2}) +Used by: test2.py (\hyperref[id2]{2}) - \end{list} - \normalsize - - \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 ``test.py``, and the -other in ``test2.py``. +%___________________________________________________________________________ -Printing the message ----------------------- +\subsection*{Printing the message% + \phantomsection% + \addcontentsline{toc}{subsection}{Printing the message}% + \label{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} +\DUrubric{Print the message (7)} +% +\begin{quote}{\ttfamily \raggedright \noindent +print~msg +} +\end{quote} +Used by: test.py (\hyperref[id1]{1}); test2.py (\hyperref[id2]{2}) +\end{document} diff --git a/test/test_rst.w b/test/test_rst.w index 0811081..71522ec 100644 --- a/test/test_rst.w +++ b/test/test_rst.w @@ -80,7 +80,7 @@ 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 @@ -95,6 +95,5 @@ they have: @d Print the message @{ -print msg +print(msg) @} - diff --git a/test/test_tangler.py b/test/test_tangler.py index 23a09cc..256dac8 100644 --- a/test/test_tangler.py +++ b/test/test_tangler.py @@ -1,40 +1,39 @@ -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 ): +import io + + +class TangleTestcase(unittest.TestCase): + text = "" + file_name = "" + error = "" + 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.tangle( self.tangler ) + self.rdr.load(self.web, self.file_name, self.source) + 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 ) + self.fail("Should not tangle") + except pyweb.Error as e: + self.assertEqual(exception_text, e.args[0]) + def tearDown(self) -> None: + name, _ = os.path.splitext(self.file_name) try: - os.remove( name + ".tmp" ) + os.remove(name + ".tmp") except OSError: pass -test2_w= """Some anonymous chunk +test2_w = """Some anonymous chunk @o test2.tmp @{@ @ @@ -44,15 +43,15 @@ def tearDown( self ): """ -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." ) +class Test_SemanticError_2(TangleTestcase): + text = test2_w + file_name = "test2.w" + def test_should_raise_undefined(self) -> None: + self.tangle_and_check_exception("Attempt to tangle an undefined Chunk, part2.") -test3_w= """Some anonymous chunk +test3_w = """Some anonymous chunk @o test3.tmp @{@ @ @@ -63,15 +62,15 @@ def test_should_raise_undefined( self ): """ -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." ) +class Test_SemanticError_3(TangleTestcase): + text = test3_w + file_name = "test3.w" + def test_should_raise_bad_xref(self) -> None: + self.tangle_and_check_exception("Illegal tangling of a cross reference command.") -test4_w= """Some anonymous chunk +test4_w = """Some anonymous chunk @o test4.tmp @{@ @ @@ -82,15 +81,15 @@ def test_should_raise_bad_xref( self ): """ -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...'" ) +class Test_SemanticError_4(TangleTestcase): + text = test4_w + file_name = "test4.w" + def test_should_raise_noFullName(self) -> None: + self.tangle_and_check_exception("No full name for 'part1...'") -test5_w= """ +test5_w = """ Some anonymous chunk @o test5.tmp @{@ @@ -103,15 +102,15 @@ def test_should_raise_noFullName( self ): """ -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']" ) +class Test_SemanticError_5(TangleTestcase): + text = test5_w + file_name = "test5.w" + def test_should_raise_ambiguous(self) -> None: + self.tangle_and_check_exception("Ambiguous abbreviation 'part1...', matches ['part1a', 'part1b']") -test6_w= """Some anonymous chunk +test6_w = """Some anonymous chunk @o test6.tmp @{@ @ @@ -124,20 +123,20 @@ def test_should_raise_ambiguous( self ): """ -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 ) +class Test_SemanticError_6(TangleTestcase): + text = test6_w + file_name = "test6.w" + def test_should_warn(self) -> None: + self.rdr.load(self.web, self.file_name, self.source) + 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() ) ) + self.assertEqual(1, len(self.web.no_reference())) + self.assertEqual(1, len(self.web.multi_reference())) + self.assertEqual(0, len(self.web.no_definition())) -test7_w= """ +test7_w = """ Some anonymous chunk. @d title @[the title of this document, defined with @@[ and @@]@] A reference to @. @@ -145,30 +144,30 @@ def test_should_warn( self ): A final anonymous chunk from test7.w """ -test7_inc_w= """The test7a.tmp chunk for 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 ): +class Test_IncludeError_7(TangleTestcase): + text = test7_w + file_name = "test7.w" + def setUp(self) -> None: 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 ) + temp.write(test7_inc_w) + super().setUp() + def test_should_include(self) -> None: + self.rdr.load(self.web, self.file_name, self.source) + 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() + self.assertEqual(5, len(self.web.chunkSeq)) + self.assertEqual(test7_inc_w, self.web.chunkSeq[3].commands[0].text) + def tearDown(self) -> None: + os.remove('test7_inc.tmp') + super().tearDown() if __name__ == "__main__": import sys - logging.basicConfig( stream=sys.stdout, level= logging.WARN ) + logging.basicConfig(stream=sys.stdout, level=logging.WARN) unittest.main() diff --git a/test/test_unit.py b/test/test_unit.py index dc2b497..54be13c 100644 --- a/test/test_unit.py +++ b/test/test_unit.py @@ -1,821 +1,948 @@ -from __future__ import print_function """Unit tests.""" -import pyweb -import unittest +import argparse +import io import logging -import StringIO -import string import os -import time import re +import string +import time +from typing import Any, TextIO +import unittest +import warnings + +import pyweb -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 MockChunk: + def __init__(self, name: str, seq: int, lineNumber: int) -> None: + self.name = name + self.fullName = name + self.seq = seq + self.lineNumber = lineNumber + self.initial = True + self.commands = [] + self.referencedBy = [] + def __repr__(self) -> str: + return f"({self.name!r}, {self.seq!r})" + def references(self, aWeaver: pyweb.Weaver) -> list[str]: + return [(c.name, c.seq) for c in self.referencedBy] + def reference_indent(self, aWeb: "Web", aTangler: "Tangler", amount: int) -> None: + aTangler.addIndent(amount) + def reference_dedent(self, aWeb: "Web", aTangler: "Tangler") -> None: + aTangler.clrIndent() + def tangle(self, aWeb: "Web", aTangler: "Tangler") -> None: + aTangler.write(self.name) -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 EmitterExtension(pyweb.Emitter): + def doOpen(self, fileName: str) -> None: + self.theFile = io.StringIO() + def doClose(self) -> None: + self.theFile.flush() -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" ) +class TestEmitter(unittest.TestCase): + def setUp(self) -> None: + self.emitter = EmitterExtension() + def test_emitter_should_open_close_write(self) -> None: + 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.assertEqual("Something", self.emitter.theFile.getvalue()) + def test_emitter_should_codeBlock(self) -> None: + self.emitter.open("test.tmp") + self.emitter.codeBlock("Some") + self.emitter.codeBlock(" 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.assertEqual("Some Code\n", self.emitter.theFile.getvalue()) + def test_emitter_should_indent(self) -> None: + self.emitter.open("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.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 ): + self.assertEqual("Begin\n More Code\nEnd\n", self.emitter.theFile.getvalue()) + def test_emitter_should_noindent(self) -> None: + self.emitter.open("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 TestWeaver(unittest.TestCase): + def setUp(self) -> None: + self.weaver = pyweb.Weaver() + self.weaver.reference_style = pyweb.SimpleReference() + self.filename = "testweaver" + self.aFileChunk = MockChunk("File", 123, 456) + self.aFileChunk.referencedBy = [ ] + self.aChunk = MockChunk("Chunk", 314, 278) + self.aChunk.referencedBy = [ self.aFileChunk ] + def tearDown(self) -> None: import os try: - os.remove( "testweaver.rst" ) + pass #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_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) - 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 ) + def test_weaver_should_codeBegin(self) -> None: + self.weaver.open(self.filename) + 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() - 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 ) + with open("testweaver.rst", "r") as result: + txt = result.read() + 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 ): - self.weaver.open( self.filename ) - self.weaver.fileBegin( self.aFileChunk ) - self.weaver.codeBlock( self.weaver.quote( "*The* `Code`\n" ) ) - self.weaver.fileEnd( self.aFileChunk ) + def test_weaver_should_fileBegin(self) -> None: + 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 ) + with open("testweaver.rst", "r") as result: + txt = result.read() + 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 ): - self.weaver.open( self.filename ) + def test_weaver_should_xref(self) -> None: + self.weaver.open(self.filename) self.weaver.xrefHead( ) - self.weaver.xrefLine( "Chunk", [ ("Container", 123) ] ) + self.weaver.xrefLine("Chunk", [ ("Container", 123) ]) self.weaver.xrefFoot( ) - self.weaver.fileEnd( self.aFileChunk ) + #self.weaver.fileEnd(self.aFileChunk) # Why? 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 ) + with open("testweaver.rst", "r") as result: + txt = result.read() + self.assertEqual("\n:Chunk:\n |srarr|\\ (`('Container', 123)`_)\n\n", txt) - def test_weaver_should_xref_def( self ): - self.weaver.open( self.filename ) + def test_weaver_should_xref_def(self) -> None: + self.weaver.open(self.filename) self.weaver.xrefHead( ) - self.weaver.xrefDefLine( "Chunk", 314, [ ("Container", 123), ("Chunk", 314) ] ) + # 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 ) + #self.weaver.fileEnd(self.aFileChunk) # Why? 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 ) + with open("testweaver.rst", "r") as result: + txt = result.read() + self.assertEqual("\n:Chunk:\n `123`_ [`314`_] `567`_\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 ): +class TestLaTeX(unittest.TestCase): + def setUp(self) -> None: + self.weaver = pyweb.LaTeX() + self.weaver.reference_style = pyweb.SimpleReference() + self.filename = "testweaver" + self.aFileChunk = MockChunk("File", 123, 456) + self.aFileChunk.referencedBy = [ ] + self.aChunk = MockChunk("Chunk", 314, 278) + self.aChunk.referencedBy = [ self.aFileChunk, ] + def tearDown(self) -> None: import os try: - os.remove( "testweaver.tex" ) + 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 ) + def test_weaver_functions_latex(self) -> None: + result = self.weaver.quote("\\end{Verbatim}") + self.assertEqual("\\end\\,{Verbatim}", result) + result = self.weaver.references(self.aChunk) + self.assertEqual("\n \\footnotesize\n Used by:\n \\begin{list}{}{}\n \n \\item Code example File (123) (Sect. \\ref{pyweb123}, p. \\pageref{pyweb123})\n\n \\end{list}\n \\normalsize\n", result) + result = self.weaver.referenceTo("Chunk", 314) + self.assertEqual("$\\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 ): +class TestHTML(unittest.TestCase): + def setUp(self) -> None: + self.weaver = pyweb.HTML( ) + self.weaver.reference_style = pyweb.SimpleReference() + self.filename = "testweaver" + self.aFileChunk = MockChunk("File", 123, 456) + self.aFileChunk.referencedBy = [] + self.aChunk = MockChunk("Chunk", 314, 278) + self.aChunk.referencedBy = [ self.aFileChunk, ] + def tearDown(self) -> None: import os try: - os.remove( "testweaver.html" ) + 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 ) + 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) +# TODO: Finish this - -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 ): +class TestTangler(unittest.TestCase): + def setUp(self) -> None: + self.tangler = pyweb.Tangler() + self.filename = "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: import os try: - os.remove( "testtangler.w" ) + os.remove("testtangler.code") 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 ) + 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.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 ) + with open("testtangler.code", "r") as result: + txt = result.read() + self.assertEqual("*The* `Code`\n", txt) + + +class TestTanglerMake(unittest.TestCase): + def setUp(self) -> None: + self.tangler = pyweb.TanglerMake() + self.filename = "testtangler.code" + 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 ): + self.time_original = os.path.getmtime(self.filename) + self.original = os.lstat(self.filename) + #time.sleep(0.75) # Alternative to assure timestamps must be different + + def tearDown(self) -> None: import os try: - os.remove( "testtangler.w" ) + os.remove("testtangler.code") 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 ) + def test_same_should_leave(self) -> None: + 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 ) ) + self.assertTrue(os.path.samestat(self.original, os.lstat(self.filename))) + #self.assertEqual(self.time_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 ) + def test_different_should_update(self) -> None: + 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 ) ) + self.assertFalse(os.path.samestat(self.original, os.lstat(self.filename))) + #self.assertNotEqual(self.time_original, os.path.getmtime(self.filename)) -class MockCommand( object ): - def __init__( self ): - self.lineNumber= 314 - def startswith( self, text ): +class MockCommand: + def __init__(self) -> None: + self.lineNumber = 314 + def startswith(self, text: str) -> bool: 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 ): +class MockWeb: + def __init__(self) -> None: + self.chunks = [] + self.wove = None + self.tangled = None + def add(self, aChunk: pyweb.Chunk) -> None: + self.chunks.append(aChunk) + def addNamed(self, aChunk: pyweb.Chunk) -> None: + self.chunks.append(aChunk) + def addOutput(self, aChunk: pyweb.Chunk) -> None: + self.chunks.append(aChunk) + def fullNameFor(self, name: str) -> str: 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 ): + def fileXref(self) -> dict[str, list[int]]: + return {'file': [1,2,3]} + def chunkXref(self) -> dict[str, list[int]]: + return {'chunk': [4,5,6]} + def userNamesXref(self) -> dict[str, list[int]]: + return {'name': (7, [8,9,10])} + def getchunk(self, name: str) -> list[pyweb.Chunk]: + return [MockChunk(name, 1, 314)] + def createUsedBy(self) -> None: pass - def clrIndent( self ): + def weaveChunk(self, name, weaver) -> None: + weaver.write(name) + def weave(self, weaver) -> None: + self.wove = weaver + def tangle(self, tangler) -> None: + self.tangled = tangler + +class MockWeaver: + def __init__(self) -> None: + self.begin_chunk = [] + self.end_chunk = [] + self.written = [] + self.code_indent = None + def quote(self, text: str) -> str: + return text.replace("&", "&") # token quoting + def docBegin(self, aChunk: pyweb.Chunk) -> None: + self.begin_chunk.append(aChunk) + def write(self, text: str) -> None: + self.written.append(text) + def docEnd(self, aChunk: pyweb.Chunk) -> None: + self.end_chunk.append(aChunk) + def codeBegin(self, aChunk: pyweb.Chunk) -> None: + self.begin_chunk.append(aChunk) + def codeBlock(self, text: str) -> None: + self.written.append(text) + def codeEnd(self, aChunk: pyweb.Chunk) -> None: + self.end_chunk.append(aChunk) + def fileBegin(self, aChunk: pyweb.Chunk) -> None: + self.begin_chunk.append(aChunk) + def fileEnd(self, aChunk: pyweb.Chunk) -> None: + self.end_chunk.append(aChunk) + def addIndent(self, increment=0): pass - def xrefHead( self ): + def setIndent(self, fixed: int | None=None, command: str | None=None) -> None: + self.indent = fixed + def addIndent(self, increment: int = 0) -> None: + self.indent = increment + def clrIndent(self) -> None: 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 ): + def xrefHead(self) -> None: pass - def open( self, aFile ): + def xrefLine(self, name: str, refList: list[int]) -> None: + self.written.append(f"{name} {refList}") + def xrefDefLine(self, name: str, defn: int, refList: list[int]) -> None: + self.written.append(f"{name} {defn} {refList}") + def xrefFoot(self) -> None: pass - def close( self ): + def referenceTo(self, name: str, seq: int) -> None: pass - def referenceTo( self, name, seq ): + def open(self, aFile: str) -> "MockWeaver": + return self + def close(self) -> None: pass + def __enter__(self) -> "MockWeaver": + return self + def __exit__(self, *args: Any) -> bool: + return False -class MockTangler( MockWeaver ): - def __init__( self ): - super( MockTangler, self ).__init__() - self.context= [0] +class MockTangler(MockWeaver): + def __init__(self) -> None: + super().__init__() + self.context = [0] + def addIndent(self, amount: int) -> None: + pass -class TestChunk( unittest.TestCase ): - def setUp( self ): - self.theChunk= pyweb.Chunk() +class TestChunk(unittest.TestCase): + def setUp(self) -> None: + 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_command_should_work(self) -> None: + cmd1 = MockCommand() + self.theChunk.append(cmd1) + self.assertEqual(1, len(self.theChunk.commands) ) + cmd2 = MockCommand() + self.theChunk.append(cmd2) + self.assertEqual(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_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 ): - cmd1= MockCommand() - self.theChunk.append( cmd1 ) - self.theChunk.appendText( "hi mom" ) - self.assertEquals( 2, len(self.theChunk.commands ) ) + 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) ) - def test_append_to_web_should_work( self ): - web= MockWeb() - self.theChunk.webAdd( web ) - self.assertEquals( 1, len(web.chunks) ) + def test_append_to_web_should_work(self) -> None: + web = MockWeb() + self.theChunk.webAdd(web) + self.assertEqual(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_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 ): - 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_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 ): - 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_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 ): - self.assertTrue( self.theChunk.lineNumber is None ) - cmd1= MockCommand() - self.theChunk.append( cmd1 ) - self.assertEqual( 314, self.theChunk.lineNumber ) + 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) - def test_weave_should_work( self ): + def test_weave_should_work(self) -> None: 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 ) ) + self.theChunk.appendText("this chunk has very & many words") + self.theChunk.weave(web, wvr) + self.assertEqual(1, len(wvr.begin_chunk)) + self.assertTrue(wvr.begin_chunk[0] is self.theChunk) + self.assertEqual(1, len(wvr.end_chunk)) + self.assertTrue(wvr.end_chunk[0] is self.theChunk) + self.assertEqual("this chunk has very & many words", "".join( wvr.written)) - def test_tangle_should_fail( self ): + def test_tangle_should_fail(self) -> None: tnglr = MockTangler() web = MockWeb() - self.theChunk.appendText( "this chunk has very & many words" ) + self.theChunk.appendText("this chunk has very & many words") try: - self.theChunk.tangle( web, tnglr ) + self.theChunk.tangle(web, tnglr) self.fail() - except pyweb.Error, e: - self.assertEquals( "Cannot tangle an anonymous chunk", e.args[0] ) + except pyweb.Error as e: + self.assertEqual("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" ) +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 ): - self.assertEquals( 2, len(self.theChunk.getUserIDRefs()) ) - self.assertEquals( "index", self.theChunk.getUserIDRefs()[0] ) - self.assertEquals( "terms", self.theChunk.getUserIDRefs()[1] ) + 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_to_web_should_work( self ): - web= MockWeb() - self.theChunk.webAdd( web ) - self.assertEquals( 1, len(web.chunks) ) + def test_append_to_web_should_work(self) -> None: + web = MockWeb() + self.theChunk.webAdd(web) + self.assertEqual(1, len(web.chunks)) - def test_weave_should_work( self ): + def test_weave_should_work(self) -> None: 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 ): + self.theChunk.weave(web, wvr) + self.assertEqual(1, len(wvr.begin_chunk)) + self.assertTrue(wvr.begin_chunk[0] is self.theChunk) + self.assertEqual(1, len(wvr.end_chunk)) + self.assertTrue(wvr.end_chunk[0] is self.theChunk) + self.assertEqual("the words & text of this Chunk", "".join( wvr.written)) + + def test_tangle_should_work(self) -> None: 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" ) + self.theChunk.tangle(web, tnglr) + self.assertEqual(1, len(tnglr.begin_chunk)) + self.assertTrue(tnglr.begin_chunk[0] is self.theChunk) + self.assertEqual(1, len(tnglr.end_chunk)) + self.assertTrue(tnglr.end_chunk[0] is self.theChunk) + self.assertEqual("the words & text of this Chunk", "".join( tnglr.written)) + + +class TestNamedChunk_Noindent(unittest.TestCase): + def setUp(self) -> None: + self.theChunk = pyweb.NamedChunk_Noindent("Some 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(1, len(tnglr.begin_chunk)) + self.assertTrue(tnglr.begin_chunk[0] is self.theChunk) + self.assertEqual(1, len(tnglr.end_chunk)) + self.assertTrue(tnglr.end_chunk[0] is self.theChunk) + self.assertEqual("the words & text of this Chunk", "".join( tnglr.written)) + + +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_to_web_should_work( self ): - web= MockWeb() - self.theChunk.webAdd( web ) - self.assertEquals( 1, len(web.chunks) ) + def test_append_to_web_should_work(self) -> None: + web = MockWeb() + self.theChunk.webAdd(web) + self.assertEqual(1, len(web.chunks)) - def test_weave_should_work( self ): + def test_weave_should_work(self) -> None: 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 ): + self.theChunk.weave(web, wvr) + self.assertEqual(1, len(wvr.begin_chunk)) + self.assertTrue(wvr.begin_chunk[0] is self.theChunk) + self.assertEqual(1, len(wvr.end_chunk)) + self.assertTrue(wvr.end_chunk[0] is self.theChunk) + self.assertEqual("the words & text of this Chunk", "".join( wvr.written)) + + def test_tangle_should_work(self) -> None: 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 ) ) + self.theChunk.tangle(web, tnglr) + self.assertEqual(1, len(tnglr.begin_chunk)) + self.assertTrue(tnglr.begin_chunk[0] is self.theChunk) + self.assertEqual(1, len(tnglr.end_chunk)) + self.assertTrue(tnglr.end_chunk[0] is self.theChunk) + self.assertEqual("the words & text of this Chunk", "".join( tnglr.written)) - +# TODO Test This +# No Tests - -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 ): +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.assertEquals( "Some text & words in the document\n ", "".join( wvr.written ) ) - def test_tangle_should_work( self ): + self.cmd.weave(web, wvr) + self.assertEqual("Some text & words in the document\n ", "".join( wvr.written)) + def test_tangle_should_work(self) -> None: tnglr = MockTangler() web = MockWeb() - self.cmd.tangle( web, tnglr ) - self.assertEquals( "Some text & words in the document\n ", "".join( tnglr.written ) ) + self.cmd.tangle(web, tnglr) + self.assertEqual("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 ): +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.assertEquals( "Some text & words in the document\n ", "".join( wvr.written ) ) - def test_tangle_should_work( self ): + self.cmd.weave(web, wvr) + self.assertEqual("Some text & words in the document\n ", "".join( wvr.written)) + def test_tangle_should_work(self) -> None: tnglr = MockTangler() web = MockWeb() - self.cmd.tangle( web, tnglr ) - self.assertEquals( "Some text & words in the document\n ", "".join( tnglr.written ) ) + self.cmd.tangle(web, tnglr) + self.assertEqual("Some text & words in the document\n ", "".join( tnglr.written)) +# No Tests - -class TestFileXRefCommand( unittest.TestCase ): - def setUp( self ): - self.cmd= pyweb.FileXrefCommand( 314 ) - def test_weave_should_work( self ): +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.assertEquals( "file [1, 2, 3]", "".join( wvr.written ) ) - def test_tangle_should_fail( self ): + self.cmd.weave(web, wvr) + self.assertEqual("file [1, 2, 3]", "".join( wvr.written)) + def test_tangle_should_fail(self) -> None: tnglr = MockTangler() web = MockWeb() try: - self.cmd.tangle( web, tnglr ) + 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 ): +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.assertEquals( "chunk [4, 5, 6]", "".join( wvr.written ) ) - def test_tangle_should_fail( self ): + self.cmd.weave(web, wvr) + self.assertEqual("chunk [4, 5, 6]", "".join( wvr.written)) + def test_tangle_should_fail(self) -> None: tnglr = MockTangler() web = MockWeb() try: - self.cmd.tangle( web, tnglr ) + 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 ): +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.assertEquals( "name 7 [8, 9, 10]", "".join( wvr.written ) ) - def test_tangle_should_fail( self ): + self.cmd.weave(web, wvr) + self.assertEqual("name 7 [8, 9, 10]", "".join( wvr.written)) + def test_tangle_should_fail(self) -> None: tnglr = MockTangler() web = MockWeb() try: - self.cmd.tangle( web, tnglr ) + 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 ): +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.assertEquals( "Some Name", "".join( wvr.written ) ) - def test_tangle_should_work( self ): + self.cmd.weave(web, wvr) + self.assertEqual("Some Name", "".join( wvr.written)) + def test_tangle_should_work(self) -> None: tnglr = MockTangler() web = MockWeb() - self.cmd.tangle( web, tnglr ) - self.assertEquals( "Some Name", "".join( tnglr.written ) ) + web.add(self.chunk) + self.cmd.tangle(web, tnglr) + self.assertEqual("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 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 TestWebConstruction( unittest.TestCase ): - def setUp( self ): - self.web= pyweb.Web( "Test" ) +class TestWebConstruction(unittest.TestCase): + def setUp(self) -> None: + self.web = pyweb.Web() - 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_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 ): - 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) ) + 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 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 ) +class TestWebProcessing(unittest.TestCase): + def setUp(self) -> None: + self.web = pyweb.Web() + self.web.webFileName = "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) - 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] ) + 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.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'") ) + except pyweb.Error as e: + self.assertTrue(e.args[0].startswith("Cannot resolve 'A File'")) - def test_valid_web_should_createUsedBy( self ): + 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 ): - 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_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) - 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_tangle(self) -> None: + tangler = MockTangler() + self.web.tangle(tangler) + self.assertEqual(3, len(tangler.written)) + self.assertEqual(['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 ) - + def test_valid_web_should_weave(self) -> None: + weaver = MockWeaver() + self.web.weave(weaver) + self.assertEqual(6, len(weaver.written)) + expected = ['some text', 'some code', None, 'some user2a code', None, 'some user1 code'] + self.assertEqual(expected, weaver.written) + + + +# Tested via functional tests + +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 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 MockAction( object ): - def __init__( self ): - self.count= 0 - def __call__( self ): +class MockAction: + def __init__(self) -> None: + self.count = 0 + def __call__(self) -> None: self.count += 1 -class MockWebReader( object ): - def __init__( self ): - self.count= 0 - self.theWeb= None - def web( self, aWeb ): - self.theWeb= aWeb +class MockWebReader: + def __init__(self) -> None: + self.count = 0 + self.theWeb = None + self.errors = 0 + def web(self, aWeb: "Web") -> None: + """Deprecated""" + warnings.warn("deprecated", DeprecationWarning) + self.theWeb = aWeb return self - def load( self ): + def source(self, filename: str, file: TextIO) -> str: + """Deprecated""" + warnings.warn("deprecated", DeprecationWarning) + self.webFileName = filename + def load(self, aWeb: pyweb.Web, filename: str, source: TextIO | None = None) -> None: + self.theWeb = aWeb + self.webFileName = filename 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 ): +class TestActionSequence(unittest.TestCase): + def setUp(self) -> None: + self.web = MockWeb() + self.a1 = MockAction() + self.a2 = MockAction() + 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() for c in self.action.opSequence: - self.assertEquals( 1, c.count ) - self.assertTrue( self.web is c.web ) + self.assertEqual(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 ): +class TestLoadAction(unittest.TestCase): + def setUp(self) -> None: + self.web = MockWeb() + self.action = pyweb.LoadAction() + self.webReader = MockWebReader() + self.action.web = self.web + self.action.options = argparse.Namespace( + webReader = self.webReader, + webFileName="TestLoadAction.w", + command="@", + permitList = [], ) + with open("TestLoadAction.w","w") as web: + pass + def tearDown(self) -> None: + try: + os.remove("TestLoadAction.w") + except IOError: + pass + def test_should_execute_loading(self) -> None: self.action() - self.assertEquals( 1, self.webReader.count ) + self.assertEqual(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 ): +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, ) + def test_should_execute_tangling(self) -> None: self.action() - self.assertTrue( self.web.tangled is self.tangler ) + 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 ): +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() ) + def test_should_execute_weaving(self) -> None: self.action() - self.assertTrue( self.web.wove is self.weaver ) + self.assertTrue(self.web.wove is self.weaver) - +# TODO Test Application class if __name__ == "__main__": import sys - logging.basicConfig( stream=sys.stdout, level= logging.WARN ) + logging.basicConfig(stream=sys.stdout, level=logging.WARN) unittest.main() diff --git a/test/test_weaver.py b/test/test_weaver.py index ef2b1ea..c8c95b7 100644 --- a/test/test_weaver.py +++ b/test/test_weaver.py @@ -1,42 +1,39 @@ -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 ): +import io + + +class WeaveTestcase(unittest.TestCase): + text = "" + file_name = "" + error = "" + def setUp(self) -> None: + self.source = io.StringIO(self.text) + self.web = pyweb.Web() + self.rdr = pyweb.WebReader() + def tangle_and_check_exception(self, exception_text: str) -> None: try: - self.rdr.load() - self.web.tangle( self.tangler ) + self.rdr.load(self.web, self.file_name, self.source) + 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 ) + self.fail("Should not tangle") + except pyweb.Error as e: + self.assertEqual(exception_text, e.args[0]) + def tearDown(self) -> None: + name, _ = os.path.splitext(self.file_name) try: - os.remove( name + ".html" ) + os.remove(name + ".html") except OSError: pass -test0_w= """ +test0_w = """ @@ -45,11 +42,11 @@ def tearDown( self ): @d some code @{ -def fastExp( n, p ): - r= 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) + return n*n*fastExp(n,p/2) for i in range(24): fastExp(2,i) @@ -59,24 +56,24 @@ def fastExp( n, p ): """ -expected= """ +test0_expected = """ - some code (1) +some code (1) - +

    some code (1) =

    
     
    -def fastExp( n, p ):
    -    r= 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)
    +    return n*n*fastExp(n,p/2)
     
     for i in range(24):
         fastExp(2,i)
    @@ -91,20 +88,21 @@ def fastExp( n, p ):
     """
     
     
    -class Test_RefDefWeave( WeaveTestcase ):
    -    text= test0_w
    +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 )
    +    def test_load_should_createChunks(self) -> None:
    +        self.rdr.load(self.web, self.file_name, self.source)
    +        self.assertEqual(3, len(self.web.chunkSeq))
    +    def test_weave_should_createFile(self) -> None:
    +        self.rdr.load(self.web, self.file_name, self.source)
    +        doc = pyweb.HTML()
    +        doc.reference_style = pyweb.SimpleReference() 
    +        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]) ) )
    +            actual = source.read()
    +        self.maxDiff = None
    +        self.assertEqual(test0_expected, actual)
     
     
     
    @@ -113,30 +111,30 @@ def test_weave_should_createFile( self ):
     Time = @(time.asctime()@)
     File = @(theLocation@)
     Version = @(__version__@)
    -OS = @(os.name@)
    -CWD = @(os.getcwd()@)
    +CWD = @(os.path.realpath('.')@)
     """
     
     
    -class TestEvaluations( WeaveTestcase ):
    -    text= test9_w
    +class TestEvaluations(WeaveTestcase):
    +    text = test9_w
         file_name = "test9.w"
    -    def test_should_evaluate( self ):
    -        doc= pyweb.HTML()
    -        self.web.weave( doc )
    +    def test_should_evaluate(self) -> None:
    +        self.rdr.load(self.web, self.file_name, self.source)
    +        doc = pyweb.HTML( )
    +        doc.reference_style = pyweb.SimpleReference() 
    +        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] )
    +            actual = source.readlines()
    +        #print(actual)
    +        self.assertEqual("An anonymous chunk.\n", actual[0])
    +        self.assertTrue(actual[1].startswith("Time ="))
    +        self.assertEqual("File = ('test9.w', 3)\n", actual[2])
    +        self.assertEqual('Version = 3.1\n', actual[3])
    +        self.assertEqual(f'CWD = {os.getcwd()}\n', actual[4])
     
     
     if __name__ == "__main__":
         import sys
    -    logging.basicConfig( stream=sys.stdout, level= logging.WARN )
    +    logging.basicConfig(stream=sys.stderr, level=logging.WARN)
         unittest.main()
     
    diff --git a/test/testtangler b/test/testtangler
    new file mode 100644
    index 0000000..987558a
    --- /dev/null
    +++ b/test/testtangler
    @@ -0,0 +1 @@
    +*The* `Code`
    diff --git a/test/testweaver.rst b/test/testweaver.rst
    new file mode 100644
    index 0000000..2efd65a
    --- /dev/null
    +++ b/test/testweaver.rst
    @@ -0,0 +1,4 @@
    +
    +:Chunk:
    +    `123`_ [`314`_] `567`_
    +
    diff --git a/test/unit.w b/test/unit.w
    index 8d40ed9..275cce2 100644
    --- a/test/unit.w
    +++ b/test/unit.w
    @@ -1,64 +1,99 @@
    -
    +Unit Testing
    +============
     
    -

    There are several broad areas of unit testing. There are the 34 classes in this application. +.. 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( 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.

    + + +- 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. @o test_unit.py @{@ @@ -73,12 +108,13 @@ We'll decompose these into several hierarchies. @ @} -

    Emitter Tests

    +Emitter Tests +------------- -

    The emitter class hierarchy produces output files; either woven output +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... @{ @ @@ -91,597 +127,655 @@ precisely follows the document structure. @ @} -

    The Emitter superclass is designed to be extended. The test -creates a subclass to exercise a few key features.

    +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. @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 EmitterExtension(pyweb.Emitter): + def doOpen(self, fileName: str) -> None: + self.theFile = io.StringIO() + def doClose(self) -> None: + self.theFile.flush() -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" ) +class TestEmitter(unittest.TestCase): + def setUp(self) -> None: + self.emitter = EmitterExtension() + def test_emitter_should_open_close_write(self) -> None: + self.emitter.open("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("test.tmp") + self.emitter.codeBlock("Some") + self.emitter.codeBlock(" Code") 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.assertEqual("Some Code\n", self.emitter.theFile.getvalue()) + def test_emitter_should_indent(self) -> None: + self.emitter.open("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.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.assertEqual("Begin\n More Code\nEnd\n", self.emitter.theFile.getvalue()) + def test_emitter_should_noindent(self) -> None: + self.emitter.open("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.codeBlock("End") self.emitter.close() - self.assertEquals( "Begin\n More Code\nEnd\n", self.emitter.file.getvalue() ) + self.assertEqual("Begin\nMore Code\nEnd\n", self.emitter.theFile.getvalue()) @} -

    A Mock Chunk is a Chunk-like object that we can use to test Weavers.

    +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= [] +class MockChunk: + def __init__(self, name: str, seq: int, lineNumber: int) -> None: + self.name = name + self.fullName = name + self.seq = seq + self.lineNumber = lineNumber + self.initial = True + self.commands = [] + self.referencedBy = [] + def __repr__(self) -> str: + return f"({self.name!r}, {self.seq!r})" + def references(self, aWeaver: pyweb.Weaver) -> list[str]: + return [(c.name, c.seq) for c in self.referencedBy] + def reference_indent(self, aWeb: "Web", aTangler: "Tangler", amount: int) -> None: + aTangler.addIndent(amount) + def reference_dedent(self, aWeb: "Web", aTangler: "Tangler") -> None: + aTangler.clrIndent() + def tangle(self, aWeb: "Web", aTangler: "Tangler") -> None: + aTangler.write(self.name) @} -

    The default Weaver is an Emitter that uses templates to produce RST markup.

    +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 ): +class TestWeaver(unittest.TestCase): + def setUp(self) -> None: + self.weaver = pyweb.Weaver() + self.weaver.reference_style = pyweb.SimpleReference() + self.filename = "testweaver" + self.aFileChunk = MockChunk("File", 123, 456) + self.aFileChunk.referencedBy = [ ] + self.aChunk = MockChunk("Chunk", 314, 278) + self.aChunk.referencedBy = [ self.aFileChunk ] + def tearDown(self) -> None: import os try: - os.remove( "testweaver.rst" ) + pass #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_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) - 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 ) + def test_weaver_should_codeBegin(self) -> None: + self.weaver.open(self.filename) + 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() - 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 ) + with open("testweaver.rst", "r") as result: + txt = result.read() + 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 ): - self.weaver.open( self.filename ) - self.weaver.fileBegin( self.aFileChunk ) - self.weaver.codeBlock( self.weaver.quote( "*The* `Code`\n" ) ) - self.weaver.fileEnd( self.aFileChunk ) + def test_weaver_should_fileBegin(self) -> None: + 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 ) + with open("testweaver.rst", "r") as result: + txt = result.read() + 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 ): - self.weaver.open( self.filename ) + def test_weaver_should_xref(self) -> None: + self.weaver.open(self.filename) self.weaver.xrefHead( ) - self.weaver.xrefLine( "Chunk", [ ("Container", 123) ] ) + self.weaver.xrefLine("Chunk", [ ("Container", 123) ]) self.weaver.xrefFoot( ) - self.weaver.fileEnd( self.aFileChunk ) + #self.weaver.fileEnd(self.aFileChunk) # Why? 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 ) + with open("testweaver.rst", "r") as result: + txt = result.read() + self.assertEqual("\n:Chunk:\n |srarr|\\ (`('Container', 123)`_)\n\n", txt) - def test_weaver_should_xref_def( self ): - self.weaver.open( self.filename ) + def test_weaver_should_xref_def(self) -> None: + self.weaver.open(self.filename) self.weaver.xrefHead( ) - self.weaver.xrefDefLine( "Chunk", 314, [ ("Container", 123), ("Chunk", 314) ] ) + # 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 ) + #self.weaver.fileEnd(self.aFileChunk) # Why? 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 ) + with open("testweaver.rst", "r") as result: + txt = result.read() + self.assertEqual("\n:Chunk:\n `123`_ [`314`_] `567`_\n\n", txt) @} -

    A significant fraction of the various subclasses of weaver are simply +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.

    +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 ): +class TestLaTeX(unittest.TestCase): + def setUp(self) -> None: + self.weaver = pyweb.LaTeX() + self.weaver.reference_style = pyweb.SimpleReference() + self.filename = "testweaver" + self.aFileChunk = MockChunk("File", 123, 456) + self.aFileChunk.referencedBy = [ ] + self.aChunk = MockChunk("Chunk", 314, 278) + self.aChunk.referencedBy = [ self.aFileChunk, ] + def tearDown(self) -> None: import os try: - os.remove( "testweaver.tex" ) + 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 ) + def test_weaver_functions_latex(self) -> None: + result = self.weaver.quote("\\end{Verbatim}") + self.assertEqual("\\end\\,{Verbatim}", result) + result = self.weaver.references(self.aChunk) + self.assertEqual("\n \\footnotesize\n Used by:\n \\begin{list}{}{}\n \n \\item Code example File (123) (Sect. \\ref{pyweb123}, p. \\pageref{pyweb123})\n\n \\end{list}\n \\normalsize\n", result) + result = self.weaver.referenceTo("Chunk", 314) + self.assertEqual("$\\triangleright$ Code Example Chunk (314)", result) @} -

    We'll examine a few features of the HTML templates.

    +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 ): +class TestHTML(unittest.TestCase): + def setUp(self) -> None: + self.weaver = pyweb.HTML( ) + self.weaver.reference_style = pyweb.SimpleReference() + self.filename = "testweaver" + self.aFileChunk = MockChunk("File", 123, 456) + self.aFileChunk.referencedBy = [] + self.aChunk = MockChunk("Chunk", 314, 278) + self.aChunk.referencedBy = [ self.aFileChunk, ] + def tearDown(self) -> None: import os try: - os.remove( "testweaver.html" ) + 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 ) + 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) @} -

    The unique feature of the HTMLShort class is just a template change. -

    +The unique feature of the ``HTMLShort`` class is just a template change. -

    To Do: Test this.

    + **To Do** Test ``HTMLShort``. -@d Unit Test of HTMLShort subclass... @{ @} +@d Unit Test of HTMLShort subclass... @{# TODO: Finish this@} -

    A Tangler emits the various named source files in proper format for the desired -compiler and language.

    +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 ): +class TestTangler(unittest.TestCase): + def setUp(self) -> None: + self.tangler = pyweb.Tangler() + self.filename = "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: import os try: - os.remove( "testtangler.w" ) + os.remove("testtangler.code") 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 ) + 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.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 ) + with open("testtangler.code", "r") as result: + txt = result.read() + self.assertEqual("*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 +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. -

    -

    In order to be sure that the timestamps really have changed, we -need to wait for a full second to elapse. -

    +In order to be sure that the timestamps really have changed, we either +need to wait for a full second to elapse or we need to mock the various +``os`` and ``filecmp`` features used by ``TanglerMake``. + @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 ) +class TestTanglerMake(unittest.TestCase): + def setUp(self) -> None: + self.tangler = pyweb.TanglerMake() + self.filename = "testtangler.code" + 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 ): + self.time_original = os.path.getmtime(self.filename) + self.original = os.lstat(self.filename) + #time.sleep(0.75) # Alternative to assure timestamps must be different + + def tearDown(self) -> None: import os try: - os.remove( "testtangler.w" ) + os.remove("testtangler.code") 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 ) + def test_same_should_leave(self) -> None: + 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 ) ) + self.assertTrue(os.path.samestat(self.original, os.lstat(self.filename))) + #self.assertEqual(self.time_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 ) + def test_different_should_update(self) -> None: + 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 ) ) + self.assertFalse(os.path.samestat(self.original, os.lstat(self.filename))) + #self.assertNotEqual(self.time_original, os.path.getmtime(self.filename)) @} -

    Chunk Tests

    +Chunk Tests +------------ -

    The Chunk and Command class hierarchies model the input document -- the web +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. +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.

    +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 ): +class MockCommand: + def __init__(self) -> None: + self.lineNumber = 314 + def startswith(self, text: str) -> bool: return False @} -

    A MockWeb can contain a Chunk.

    +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 ): +class MockWeb: + def __init__(self) -> None: + self.chunks = [] + self.wove = None + self.tangled = None + def add(self, aChunk: pyweb.Chunk) -> None: + self.chunks.append(aChunk) + def addNamed(self, aChunk: pyweb.Chunk) -> None: + self.chunks.append(aChunk) + def addOutput(self, aChunk: pyweb.Chunk) -> None: + self.chunks.append(aChunk) + def fullNameFor(self, name: str) -> str: 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 ): + def fileXref(self) -> dict[str, list[int]]: + return {'file': [1,2,3]} + def chunkXref(self) -> dict[str, list[int]]: + return {'chunk': [4,5,6]} + def userNamesXref(self) -> dict[str, list[int]]: + return {'name': (7, [8,9,10])} + def getchunk(self, name: str) -> list[pyweb.Chunk]: + return [MockChunk(name, 1, 314)] + def createUsedBy(self) -> None: 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 + def weaveChunk(self, name, weaver) -> None: + weaver.write(name) + def weave(self, weaver) -> None: + self.wove = weaver + def tangle(self, tangler) -> None: + self.tangled = tangler @} -

    A MockWeaver or MockTangle can process a Chunk.

    +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 ): +class MockWeaver: + def __init__(self) -> None: + self.begin_chunk = [] + self.end_chunk = [] + self.written = [] + self.code_indent = None + def quote(self, text: str) -> str: + return text.replace("&", "&") # token quoting + def docBegin(self, aChunk: pyweb.Chunk) -> None: + self.begin_chunk.append(aChunk) + def write(self, text: str) -> None: + self.written.append(text) + def docEnd(self, aChunk: pyweb.Chunk) -> None: + self.end_chunk.append(aChunk) + def codeBegin(self, aChunk: pyweb.Chunk) -> None: + self.begin_chunk.append(aChunk) + def codeBlock(self, text: str) -> None: + self.written.append(text) + def codeEnd(self, aChunk: pyweb.Chunk) -> None: + self.end_chunk.append(aChunk) + def fileBegin(self, aChunk: pyweb.Chunk) -> None: + self.begin_chunk.append(aChunk) + def fileEnd(self, aChunk: pyweb.Chunk) -> None: + self.end_chunk.append(aChunk) + def addIndent(self, increment=0): pass - def xrefHead( self ): + def setIndent(self, fixed: int | None=None, command: str | None=None) -> None: + self.indent = fixed + def addIndent(self, increment: int = 0) -> None: + self.indent = increment + def clrIndent(self) -> None: 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 ): + def xrefHead(self) -> None: pass - def open( self, aFile ): + def xrefLine(self, name: str, refList: list[int]) -> None: + self.written.append(f"{name} {refList}") + def xrefDefLine(self, name: str, defn: int, refList: list[int]) -> None: + self.written.append(f"{name} {defn} {refList}") + def xrefFoot(self) -> None: pass - def close( self ): + def referenceTo(self, name: str, seq: int) -> None: pass - def referenceTo( self, name, seq ): + def open(self, aFile: str) -> "MockWeaver": + return self + def close(self) -> None: pass + def __enter__(self) -> "MockWeaver": + return self + def __exit__(self, *args: Any) -> bool: + return False -class MockTangler( MockWeaver ): - def __init__( self ): - super( MockTangler, self ).__init__() - self.context= [0] +class MockTangler(MockWeaver): + def __init__(self) -> None: + super().__init__() + self.context = [0] + def addIndent(self, amount: int) -> None: + pass @} -

    A Chunk is built, interrogated and then emitted.

    +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() +class TestChunk(unittest.TestCase): + def setUp(self) -> None: + self.theChunk = pyweb.Chunk() @ @ @ @} -

    Can we build a 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_command_should_work(self) -> None: + cmd1 = MockCommand() + self.theChunk.append(cmd1) + self.assertEqual(1, len(self.theChunk.commands) ) + cmd2 = MockCommand() + self.theChunk.append(cmd2) + self.assertEqual(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_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 ): - cmd1= MockCommand() - self.theChunk.append( cmd1 ) - self.theChunk.appendText( "hi mom" ) - self.assertEquals( 2, len(self.theChunk.commands ) ) +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) ) -def test_append_to_web_should_work( self ): - web= MockWeb() - self.theChunk.webAdd( web ) - self.assertEquals( 1, len(web.chunks) ) +def test_append_to_web_should_work(self) -> None: + web = MockWeb() + self.theChunk.webAdd(web) + self.assertEqual(1, len(web.chunks)) @} -

    Can we interrogate a Chunk?

    +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_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 ): - 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_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 ): - self.assertTrue( self.theChunk.lineNumber is None ) - cmd1= MockCommand() - self.theChunk.append( cmd1 ) - self.assertEqual( 314, self.theChunk.lineNumber ) +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) @} -

    Can we emit a Chunk with a weaver or tangler?

    +Can we emit a Chunk with a weaver or tangler? @d Unit Test of Chunk emission... @{ -def test_weave_should_work( self ): +def test_weave_should_work(self) -> None: 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 ) ) + self.theChunk.appendText("this chunk has very & many words") + self.theChunk.weave(web, wvr) + self.assertEqual(1, len(wvr.begin_chunk)) + self.assertTrue(wvr.begin_chunk[0] is self.theChunk) + self.assertEqual(1, len(wvr.end_chunk)) + self.assertTrue(wvr.end_chunk[0] is self.theChunk) + self.assertEqual("this chunk has very & many words", "".join( wvr.written)) -def test_tangle_should_fail( self ): +def test_tangle_should_fail(self) -> None: tnglr = MockTangler() web = MockWeb() - self.theChunk.appendText( "this chunk has very & many words" ) + self.theChunk.appendText("this chunk has very & many words") try: - self.theChunk.tangle( web, tnglr ) + self.theChunk.tangle(web, tnglr) self.fail() - except pyweb.Error, e: - self.assertEquals( "Cannot tangle an anonymous chunk", e.args[0] ) + except pyweb.Error as e: + self.assertEqual("Cannot tangle an anonymous chunk", e.args[0]) @} -

    The NamedChunk is created by a @@d command. +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" ) +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 ): - self.assertEquals( 2, len(self.theChunk.getUserIDRefs()) ) - self.assertEquals( "index", self.theChunk.getUserIDRefs()[0] ) - self.assertEquals( "terms", self.theChunk.getUserIDRefs()[1] ) + 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_to_web_should_work( self ): - web= MockWeb() - self.theChunk.webAdd( web ) - self.assertEquals( 1, len(web.chunks) ) + def test_append_to_web_should_work(self) -> None: + web = MockWeb() + self.theChunk.webAdd(web) + self.assertEqual(1, len(web.chunks)) - def test_weave_should_work( self ): + def test_weave_should_work(self) -> None: 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 ): + self.theChunk.weave(web, wvr) + self.assertEqual(1, len(wvr.begin_chunk)) + self.assertTrue(wvr.begin_chunk[0] is self.theChunk) + self.assertEqual(1, len(wvr.end_chunk)) + self.assertTrue(wvr.end_chunk[0] is self.theChunk) + self.assertEqual("the words & text of this Chunk", "".join( wvr.written)) + + def test_tangle_should_work(self) -> None: 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 ) ) + self.theChunk.tangle(web, tnglr) + self.assertEqual(1, len(tnglr.begin_chunk)) + self.assertTrue(tnglr.begin_chunk[0] is self.theChunk) + self.assertEqual(1, len(tnglr.end_chunk)) + self.assertTrue(tnglr.end_chunk[0] is self.theChunk) + self.assertEqual("the words & text of this Chunk", "".join( tnglr.written)) @} -

    The OutputChunk is created by a @@o command. +@d Unit Test of NamedChunk_Noindent subclass... +@{ +class TestNamedChunk_Noindent(unittest.TestCase): + def setUp(self) -> None: + self.theChunk = pyweb.NamedChunk_Noindent("Some 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(1, len(tnglr.begin_chunk)) + self.assertTrue(tnglr.begin_chunk[0] is self.theChunk) + self.assertEqual(1, len(tnglr.end_chunk)) + self.assertTrue(tnglr.end_chunk[0] is self.theChunk) + self.assertEqual("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" ) +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_to_web_should_work( self ): - web= MockWeb() - self.theChunk.webAdd( web ) - self.assertEquals( 1, len(web.chunks) ) + def test_append_to_web_should_work(self) -> None: + web = MockWeb() + self.theChunk.webAdd(web) + self.assertEqual(1, len(web.chunks)) - def test_weave_should_work( self ): + def test_weave_should_work(self) -> None: 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 ): + self.theChunk.weave(web, wvr) + self.assertEqual(1, len(wvr.begin_chunk)) + self.assertTrue(wvr.begin_chunk[0] is self.theChunk) + self.assertEqual(1, len(wvr.end_chunk)) + self.assertTrue(wvr.end_chunk[0] is self.theChunk) + self.assertEqual("the words & text of this Chunk", "".join( wvr.written)) + + def test_tangle_should_work(self) -> None: 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 ) ) + self.theChunk.tangle(web, tnglr) + self.assertEqual(1, len(tnglr.begin_chunk)) + self.assertTrue(tnglr.begin_chunk[0] is self.theChunk) + self.assertEqual(1, len(tnglr.end_chunk)) + self.assertTrue(tnglr.end_chunk[0] is self.theChunk) + self.assertEqual("the words & text of this Chunk", "".join( tnglr.written)) @} -

    The NamedDocumentChunk is a little-used feature.

    +The ``NamedDocumentChunk`` is a little-used feature. -@d Unit Test of NamedDocumentChunk subclass... @{ @} + **TODO** Test ``NamedDocumentChunk``. -

    Command Tests

    +@d Unit Test of NamedDocumentChunk subclass... @{# TODO Test This @} + +Command Tests +--------------- @d Unit Test of Command class hierarchy... @{ @ @@ -694,216 +788,219 @@ class TestOutputChunk( unittest.TestCase ): @ @} -

    This Command superclass is essentially an inteface definition, it -has no real testable features.

    -@d Unit Test of Command superclass... @{ @} +This Command superclass is essentially an inteface definition, it +has no real testable features. + +@d Unit Test of Command superclass... @{# No Tests@} -

    A TextCommand object must be constructed, interrogated and emitted.

    +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 ): +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.assertEquals( "Some text & words in the document\n ", "".join( wvr.written ) ) - def test_tangle_should_work( self ): + self.cmd.weave(web, wvr) + self.assertEqual("Some text & words in the document\n ", "".join( wvr.written)) + def test_tangle_should_work(self) -> None: tnglr = MockTangler() web = MockWeb() - self.cmd.tangle( web, tnglr ) - self.assertEquals( "Some text & words in the document\n ", "".join( tnglr.written ) ) + self.cmd.tangle(web, tnglr) + self.assertEqual("Some text & words in the document\n ", "".join( tnglr.written)) @} -

    A CodeCommand object is a TextCommand with different processing for being emitted.

    +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 ): +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.assertEquals( "Some text & words in the document\n ", "".join( wvr.written ) ) - def test_tangle_should_work( self ): + self.cmd.weave(web, wvr) + self.assertEqual("Some text & words in the document\n ", "".join( wvr.written)) + def test_tangle_should_work(self) -> None: tnglr = MockTangler() web = MockWeb() - self.cmd.tangle( web, tnglr ) - self.assertEquals( "Some text & words in the document\n ", "".join( tnglr.written ) ) + self.cmd.tangle(web, tnglr) + self.assertEqual("Some text & words in the document\n ", "".join( tnglr.written)) @} -

    The XrefCommand class is largely abstract.

    +The XrefCommand class is largely abstract. -@d Unit Test of XrefCommand superclass... @{ @} +@d Unit Test of XrefCommand superclass... @{# No Tests @} -

    The FileXrefCommand command is expanded by a weaver to a list of all @@o -locations.

    +The FileXrefCommand command is expanded by a weaver to a list of ``@@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 ): +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.assertEquals( "file [1, 2, 3]", "".join( wvr.written ) ) - def test_tangle_should_fail( self ): + self.cmd.weave(web, wvr) + self.assertEqual("file [1, 2, 3]", "".join( wvr.written)) + def test_tangle_should_fail(self) -> None: tnglr = MockTangler() web = MockWeb() try: - self.cmd.tangle( web, tnglr ) + 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.

    +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 ): +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.assertEquals( "chunk [4, 5, 6]", "".join( wvr.written ) ) - def test_tangle_should_fail( self ): + self.cmd.weave(web, wvr) + self.assertEqual("chunk [4, 5, 6]", "".join( wvr.written)) + def test_tangle_should_fail(self) -> None: tnglr = MockTangler() web = MockWeb() try: - self.cmd.tangle( web, tnglr ) + 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.

    +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 ): +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.assertEquals( "name 7 [8, 9, 10]", "".join( wvr.written ) ) - def test_tangle_should_fail( self ): + self.cmd.weave(web, wvr) + self.assertEqual("name 7 [8, 9, 10]", "".join( wvr.written)) + def test_tangle_should_fail(self) -> None: tnglr = MockTangler() web = MockWeb() try: - self.cmd.tangle( web, tnglr ) + self.cmd.tangle(web, tnglr) self.fail() except pyweb.Error: pass @} -

    Reference commands require a context when tangling. +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 ): +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.assertEquals( "Some Name", "".join( wvr.written ) ) - def test_tangle_should_work( self ): + self.cmd.weave(web, wvr) + self.assertEqual("Some Name", "".join( wvr.written)) + def test_tangle_should_work(self) -> None: tnglr = MockTangler() web = MockWeb() - self.cmd.tangle( web, tnglr ) - self.assertEquals( "Some Name", "".join( tnglr.written ) ) + web.add(self.chunk) + self.cmd.tangle(web, tnglr) + self.assertEqual("Some Name", "".join( tnglr.written)) @} -

    Reference Tests

    +Reference Tests +---------------- -

    The Reference class implements one of two search strategies for +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, +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] ) +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]) @} -

    Web Tests

    +Web Tests +----------- -

    This is more difficult to create mocks for.

    +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 TestWebConstruction(unittest.TestCase): + def setUp(self) -> None: + self.web = pyweb.Web() @ -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 ) +class TestWebProcessing(unittest.TestCase): + def setUp(self) -> None: + self.web = pyweb.Web() + self.web.webFileName = "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) @ @ @ @@ -912,117 +1009,171 @@ class TestWebProcessing( unittest.TestCase ): @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_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 ): - 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) ) +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)) @} @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] ) +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.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'") ) + except pyweb.Error as 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 ): +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 ): - 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_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) @} @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 ) +def test_valid_web_should_tangle(self) -> None: + tangler = MockTangler() + self.web.tangle(tangler) + self.assertEqual(3, len(tangler.written)) + self.assertEqual(['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 ) +def test_valid_web_should_weave(self) -> None: + weaver = MockWeaver() + self.web.weave(weaver) + self.assertEqual(6, len(weaver.written)) + expected = ['some text', 'some code', None, 'some user2a code', None, 'some user1 code'] + self.assertEqual(expected, weaver.written) @} -

    WebReader Tests

    +WebReader Tests +---------------- -

    Generally, this is tested separately through the functional 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... @{ @} +We should test this through some clever mocks that produce the +proper sequence of tokens to parse the various kinds of Commands. + +@d Unit Test of WebReader... @{ +# Tested via functional tests +@} + +Some lower-level units: specifically the tokenizer and the option parser. + +@d Unit Test of WebReader... @{ +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) +@} + +@d Unit Test of WebReader... @{ +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) +@} + -

    Action Tests

    +Action Tests +------------- -

    Each class is tested separately. Sequence of some mocks, +Each class is tested separately. Sequence of some mocks, load, tangle, weave. -

    @d Unit Test of Action class hierarchy... @{ @ @@ -1032,105 +1183,135 @@ load, tangle, weave. @} @d Unit test of Action Sequence class... @{ -class MockAction( object ): - def __init__( self ): - self.count= 0 - def __call__( self ): +class MockAction: + def __init__(self) -> None: + self.count = 0 + def __call__(self) -> None: self.count += 1 -class MockWebReader( object ): - def __init__( self ): - self.count= 0 - self.theWeb= None - def web( self, aWeb ): - self.theWeb= aWeb +class MockWebReader: + def __init__(self) -> None: + self.count = 0 + self.theWeb = None + self.errors = 0 + def web(self, aWeb: "Web") -> None: + """Deprecated""" + warnings.warn("deprecated", DeprecationWarning) + self.theWeb = aWeb return self - def load( self ): + def source(self, filename: str, file: TextIO) -> str: + """Deprecated""" + warnings.warn("deprecated", DeprecationWarning) + self.webFileName = filename + def load(self, aWeb: pyweb.Web, filename: str, source: TextIO | None = None) -> None: + self.theWeb = aWeb + self.webFileName = filename 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 ): +class TestActionSequence(unittest.TestCase): + def setUp(self) -> None: + self.web = MockWeb() + self.a1 = MockAction() + self.a2 = MockAction() + 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() for c in self.action.opSequence: - self.assertEquals( 1, c.count ) - self.assertTrue( self.web is c.web ) + self.assertEqual(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 ): +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() ) + def test_should_execute_weaving(self) -> None: self.action() - self.assertTrue( self.web.wove is self.weaver ) + 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 ): +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, ) + def test_should_execute_tangling(self) -> None: self.action() - self.assertTrue( self.web.tangled is self.tangler ) + 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 ): +class TestLoadAction(unittest.TestCase): + def setUp(self) -> None: + self.web = MockWeb() + self.action = pyweb.LoadAction() + self.webReader = MockWebReader() + self.action.web = self.web + self.action.options = argparse.Namespace( + webReader = self.webReader, + webFileName="TestLoadAction.w", + command="@@", + permitList = [], ) + with open("TestLoadAction.w","w") as web: + pass + def tearDown(self) -> None: + try: + os.remove("TestLoadAction.w") + except IOError: + pass + def test_should_execute_loading(self) -> None: self.action() - self.assertEquals( 1, self.webReader.count ) + self.assertEqual(1, self.webReader.count) @} -

    Application Tests

    +Application Tests +------------------ -

    As with testing WebReader, this requires extensive mocking. +As with testing WebReader, this requires extensive mocking. It's easier to simply run the various use cases. -

    -@d Unit Test of Application... @{ @} +@d Unit Test of Application... @{# TODO Test Application class @} -

    Overheads and Main Script

    +Overheads and Main Script +-------------------------- -

    The boilerplate code for unit testing is the following.

    +The boilerplate code for unit testing is the following. @d Unit Test overheads... -@{from __future__ import print_function -"""Unit tests.""" -import pyweb -import unittest +@{"""Unit tests.""" +import argparse +import io import logging -import StringIO -import string import os -import time import re +import string +import time +from typing import Any, TextIO +import unittest +import warnings + +import pyweb @} @d Unit Test main... @{ if __name__ == "__main__": import sys - logging.basicConfig( stream=sys.stdout, level= logging.WARN ) + logging.basicConfig(stream=sys.stdout, level=logging.WARN) unittest.main() -@} \ No newline at end of file +@} + +We run the default ``unittest.main()`` to execute the entire suite of tests. diff --git a/todo.w b/todo.w index b5992c0..a0694ba 100644 --- a/todo.w +++ b/todo.w @@ -1,20 +1,36 @@ .. pyweb/todo.w +Python 3.10 Migration +===================== + + +1. [x] Add type hints. + +#. [x] Replace all ``.format()`` with f-strings. + +#. [ ] Replace filename strings (and ``os.path``) with ``pathlib.Path``. + +#. [ ] Introduce ``match`` statements for some of the ``elif`` blocks + +#. [ ] Introduce pytest instead of building a test runner. + +#. [ ] ``pyproject.toml``. This requires -o dir to write output to a directory of choice; which requires Pathlib + +#. [ ] Replace various mock classes with ``unittest.mock.Mock`` objects. + + 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. +1. Silence the logging during testing. -#. Add a JSON-based configuration file to configure templates. +#. Add a JSON-based (or TOML) 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. + - An alternative is to get markup templates from some kind of "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 @@ -23,12 +39,15 @@ To Do #. 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 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. +#. Fix name definition order. There's no **good** reason why a full name should + be first and elided names defined later. + #. 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 diff --git a/weave.py b/weave.py index 4ade03f..8dbc263 100644 --- a/weave.py +++ b/weave.py @@ -6,27 +6,27 @@ import string -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""") @@ -35,41 +35,41 @@ class MyHTML( pyweb.HTML ): '${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}') -with pyweb.Logger( pyweb.log_config ): - logger= logging.getLogger(__file__) +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(), + 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() + w = pyweb.Web() for action in LoadAction(), WeaveAction(): - action.web= w - action.options= options + action.web = w + action.options = options action() - logger.info( action.summary() ) + logger.info(action.summary()) From d5afb80edcfaed23b396394680d994ec784730e7 Mon Sep 17 00:00:00 2001 From: "S.Lott" Date: Fri, 10 Jun 2022 17:27:04 -0400 Subject: [PATCH 2/8] More modernization Replace string filenames and os operations with pathlib, Use 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``. --- .gitignore | 1 + Makefile | 16 +- additional.w | 36 +- done.w | 8 +- impl.w | 621 +++-- intro.w | 4 +- jedit.w | 2 +- overview.w | 2 +- pyproject.toml | 4 +- pyweb.html | 4558 +++++++++++++++++---------------- pyweb.py | 582 +++-- pyweb.rst | 2716 ++++++++++---------- requirements-dev.txt | 2 +- setup.py | 12 +- test/func.w | 110 +- test/pyweb_test.html | 230 +- test/pyweb_test.rst | 226 +- test/pyweb_test.w | 2 +- test/{test.py => runner.py} | 3 +- test/{combined.w => runner.w} | 13 +- test/test_latex.w | 8 +- test/test_loader.py | 25 +- test/test_rst.w | 8 +- test/test_tangler.py | 40 +- test/test_unit.py | 87 +- test/test_weaver.py | 43 +- test/unit.w | 87 +- tests.w | 2 +- todo.w | 21 +- 29 files changed, 4970 insertions(+), 4499 deletions(-) rename test/{test.py => runner.py} (95%) rename test/{combined.w => runner.w} (89%) diff --git a/.gitignore b/.gitignore index 2f9d84b..6a4cb7f 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ py_web_tool.egg-info/* *.toc v2_test .tox +pyweb-3.0.py diff --git a/Makefile b/Makefile index 730a0b7..6a33919 100644 --- a/Makefile +++ b/Makefile @@ -2,21 +2,25 @@ # Requires a pyweb-3.0.py (untouched) to bootstrap the current version. SOURCE = pyweb.w intro.w overview.w impl.w tests.w additional.w todo.w done.w \ - test/pyweb_test.w test/intro.w test/unit.w test/func.w test/combined.w + test/pyweb_test.w test/intro.w test/unit.w test/func.w test/runner.w .PHONY : test build # Note the bootstrapping new version from version 3.0 as baseline. +# Handy to keep this *outside* the project's Git repository. +PYWEB_BOOTSTRAP=/Users/slott/Documents/Projects/PyWebTool-3/pyweb/pyweb.py test : $(SOURCE) - python3 pyweb-3.0.py -xw pyweb.w + python3 $(PYWEB_BOOTSTRAP) -xw pyweb.w cd test && python3 ../pyweb.py pyweb_test.w - cd test && PYTHONPATH=.. python3 test.py + PYTHONPATH=${PWD} pytest cd test && rst2html.py pyweb_test.rst pyweb_test.html - mypy --strict pyweb.py + mypy --strict --show-error-codes pyweb.py build : pyweb.py pyweb.html -pyweb.py pyweb.html : $(SOURCE) - python3 pyweb-3.0.py pyweb.w +pyweb.py pyweb.rst : $(SOURCE) + python3 $(PYWEB_BOOTSTRAP) pyweb.w +pyweb.html : pyweb.rst + rst2html.py $< $@ diff --git a/additional.w b/additional.w index 3e24495..e9cc136 100644 --- a/additional.w +++ b/additional.w @@ -1,4 +1,4 @@ -.. pyweb/additional.w +.. py-web-tool/additional.w Additional Files ================ @@ -172,16 +172,16 @@ from distutils.core import setup setup(name='py-web-tool', version='3.1', - description='pyWeb 3.1: Yet Another Literate Programming Tool', + 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'], 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', ] ) @} @@ -204,7 +204,7 @@ In order to install dependencies, the following file is also used. docutils==0.18.1 tox==3.25.0 mypy==0.910 -pytest == 7.1.2 +pytest==7.1.2 @} The ``README`` file @@ -384,24 +384,28 @@ Note that there are tabs in this file. We bootstrap the next version from the 3. # Requires a pyweb-3.0.py (untouched) to bootstrap the current version. SOURCE = pyweb.w intro.w overview.w impl.w tests.w additional.w todo.w done.w \ - test/pyweb_test.w test/intro.w test/unit.w test/func.w test/combined.w + test/pyweb_test.w test/intro.w test/unit.w test/func.w test/runner.w .PHONY : test build # Note the bootstrapping new version from version 3.0 as baseline. +# Handy to keep this *outside* the project's Git repository. +PYWEB_BOOTSTRAP=/Users/slott/Documents/Projects/PyWebTool-3/pyweb/pyweb.py test : $(SOURCE) - python3 pyweb-3.0.py -xw pyweb.w + python3 $(PYWEB_BOOTSTRAP) -xw pyweb.w cd test && python3 ../pyweb.py pyweb_test.w - cd test && PYTHONPATH=.. python3 test.py + PYTHONPATH=${PWD} pytest cd test && rst2html.py pyweb_test.rst pyweb_test.html - mypy --strict pyweb.py + mypy --strict --show-error-codes pyweb.py build : pyweb.py pyweb.html -pyweb.py pyweb.html : $(SOURCE) - python3 pyweb-3.0.py pyweb.w +pyweb.py pyweb.rst : $(SOURCE) + python3 $(PYWEB_BOOTSTRAP) pyweb.w +pyweb.html : pyweb.rst + rst2html.py $< $@ @} **TODO:** Finish ``tox.ini`` or ``pyproject.toml``. @@ -421,8 +425,10 @@ envlist = py310 deps = pytest == 7.1.2 mypy == 0.910 +setenv = + PYWEB_BOOTSTRAP = /Users/slott/Documents/Projects/PyWebTool-3/pyweb/pyweb.py commands_pre = - python3 pyweb-3.0.py pyweb.w + python3 {env:PYWEB_BOOTSTRAP} pyweb.w python3 pyweb.py -o test test/pyweb_test.w commands = python3 test/test.py diff --git a/done.w b/done.w index 3bfc99f..8ff5a64 100644 --- a/done.w +++ b/done.w @@ -1,4 +1,4 @@ -.. pyweb/done.w +.. py-web-tool/done.w Change Log =========== @@ -7,14 +7,16 @@ Changes for 3.1 - Change to Python 3.10. -- Add type hints, f-strings, pathlib. +- Add type hints, f-strings, pathlib, abc.ABC - Replace some complex elif blocks with match statements -- Remove the Jedit configuration file as an output. +- Use pytest as a test runner. - Add a ``Makefile``, ``pyproject.toml``, ``requirements.txt`` and ``requirements-dev.txt``. + + Changes for 3.0 - Move to GitHub diff --git a/impl.w b/impl.w index f1235fa..627c9fa 100644 --- a/impl.w +++ b/impl.w @@ -1,4 +1,4 @@ -.. pyweb/impl.w +.. py-web-tool/impl.w Implementation ============== @@ -85,23 +85,36 @@ fit elsewhere @d Base Class Definitions @{ + @ + @ + @ + @ + @ + @

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:
     """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}"
+
+    →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 ):
+def weave(self, aWeb: "Web", aWeaver: "Weaver") -> None:
     pass
-def tangle( self, aWeb, aTangler ):
+def tangle(self, aWeb: "Web", aTangler: "Tangler") -> None:
     pass
 
-

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 +3684,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 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 +3706,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 +3729,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 +3760,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 +3838,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 +3882,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 +3955,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 +3989,70 @@

    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 ):
    +    def __init__(self) -> None:
    +        self.logger = logging.getLogger(self.__class__.__qualname__)
    +    def chunkReferencedBy(self, aChunk: Chunk) -> list[Chunk]:
             """Return a list of Chunks."""
    -        pass
    +        return []
     
    -

    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 +4064,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 +4116,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 +4142,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__)
     
    -    →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)
    +    def __str__(self) -> str:
    +        return f"Web {self.web_path!r}"
    +
    +    →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 +4183,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 (157)

    -

    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 +4231,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 +4286,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 +4325,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 +4366,23 @@

    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] ) ]
    +        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)) ) )
    +            raise Error(f"Ambiguous abbreviation {name!r}, matches {list(sorted(best))!r}")
             elif len(best) == 1:
                 return best[0]
         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 +4390,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 +4427,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():
         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 +4481,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 +4530,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 +4556,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 +4617,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 +4626,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 +4641,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 +4672,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 %r", 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 +4755,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 +4770,65 @@

    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__ )
    +    # State of reading and parsing.
    +    tokenizer: Tokenizer
    +    aChunk: Chunk
    +
    +    # Configuration
    +    command: str
    +    permitList: list[str]
    +
    +    # State of the reader
    +    _source: TextIO
    +    filePath: Path
    +    theWeb: "Web"
    +
    +    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 (133)
     
    -        →WebReader command literals (130)
    -    def __str__( self ):
    +    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 (130)
    +    →WebReader load the web (132)
    +    →WebReader handle a command string (117), →(129)
     
    -

    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 +4852,41 @@

    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) =

    +

    WebReader handle a command string (117) =

    -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)
    +def handleCommand(self, token: str) -> bool:
    +    self.logger.debug("Reading %r", token)
    +
    +    →major commands segment the input into separate Chunks (118)
    +    →minor commands add Commands to the current Chunk (123)
         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.logger.error("Extra %r (possibly missing chunk name) near %r", token, self.location())
             self.errors += 1
         else:
    -        return None # did not recogize the command
    -    return True # did recognize the command
    +        return False  # did not recogize the command
    +    return True  # did recognize the command
     
    -

    WebReader handle a command string (115). Used by: WebReader class... (114)

    +

    WebReader handle a command string (117). Used by: WebReader class... (116)

    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) =

    +

    major commands segment the input into separate Chunks (118) =

     if token[:2] == self.cmdo:
    -    →start an OutputChunk, adding it to the web (117)
    +    →start an OutputChunk, adding it to the web (119)
     elif token[:2] == self.cmdd:
    -    →start a NamedChunk or NamedDocumentChunk, adding it to the web (118)
    +    →start a NamedChunk or NamedDocumentChunk, adding it to the web (120)
     elif token[:2] == self.cmdi:
    -    →import another file (119)
    +    →include another file (121)
     elif token[:2] in (self.cmdrcurl,self.cmdrbrak):
    -    →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 (122)
     
    -

    major commands segment the input into separate Chunks (116). Used by: WebReader handle a command... (115)

    +

    major commands segment the input into separate Chunks (118). Used by: WebReader handle a command... (117)

    An output chunk has the form @o name @{ content @}. We use the first two tokens to name the OutputChunk. We simply expect @@ -4784,23 +4894,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 (119) =

    +
    +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 (119). Used by: major commands... (118)

    A named chunk has the form @d name @{ content @} for code and @d name @[ content @] for document source. @@ -4815,34 +4926,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 (120) =

    -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 (120). Used by: major commands... (118)

    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 +4971,35 @@

    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 (121) =

    -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 )
    +    self.logger.info("Including %r", incPath)
    +    include = WebReader(parent=self)
    +    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 (121). Used by: major commands... (118)

    When a @} or @] are found, this finishes a named chunk. The next text is therefore part of an anonymous chunk.

    @@ -4905,37 +5009,37 @@

    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 (122) =

    -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 (122). Used by: major commands... (118)

    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) =

    +

    minor commands add Commands to the current Chunk (123) =

     elif token[:2] == self.cmdpipe:
    -    →assign user identifiers to the current chunk (122)
    +    →assign user identifiers to the current chunk (124)
     elif token[:2] == self.cmdf:
    -    self.aChunk.append( FileXrefCommand(self.tokenizer.lineNumber) )
    +    self.aChunk.append(FileXrefCommand(self.tokenizer.lineNumber))
     elif token[:2] == self.cmdm:
    -    self.aChunk.append( MacroXrefCommand(self.tokenizer.lineNumber) )
    +    self.aChunk.append(MacroXrefCommand(self.tokenizer.lineNumber))
     elif token[:2] == self.cmdu:
    -    self.aChunk.append( UserIdXrefCommand(self.tokenizer.lineNumber) )
    +    self.aChunk.append(UserIdXrefCommand(self.tokenizer.lineNumber))
     elif token[:2] == self.cmdlangl:
    -    →add a reference command to the current chunk (123)
    +    →add a reference command to the current chunk (125)
     elif token[:2] == self.cmdlexpr:
    -    →add an expression command to the current chunk (125)
    +    →add an expression command to the current chunk (127)
     elif token[:2] == self.cmdcmd:
    -    →double at-sign replacement, append this character to previous TextCommand (126)
    +    →double at-sign replacement, append this character to previous TextCommand (128)
     
    -

    minor commands add Commands to the current Chunk (121). Used by: WebReader handle a command... (115)

    +

    minor commands add Commands to the current Chunk (123). Used by: WebReader handle a command... (117)

    User identifiers occur after a @| in a NamedChunk.

    Note that no check is made to assure that the previous Chunk was indeed a named @@ -4946,34 +5050,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 (124) =

     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 (124). Used by: minor commands... (123)

    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 (125) =

     # 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 (125). Used by: minor commands... (123)

    An expression command has the form @(Python Expression@). We accept three @@ -4983,15 +5087,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 (126) +=

     import builtins
     import sys
    @@ -4999,40 +5103,43 @@ 

    The WebReader Class

    -

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

    +

    Imports (126). Used by: pyweb.py (157)

    -

    add an expression command to the current chunk (125) =

    +

    add an expression command to the current chunk (127) =

     # 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)
    +    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 ('breakpoint', 'compile', 'eval', 'exec', 'execfile', 'globals', 'help', 'input', 'memoryview', 'open', 'print', 'super', '__import__')
    +    ))
    +    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__,
             )
         # 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 (127). Used by: minor commands... (123)

    A double command sequence ('@@', when the command is an '@') has the usual meaning of '@' in the input stream. We do this via @@ -5042,50 +5149,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 (128) =

    -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 (128). Used by: minor commands... (123)

    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 (129) +=

    -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 (129). 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 (130) =

    -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 (130). 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 +5202,91 @@

    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 (131) +=

    +
    +from typing import TextIO
    +
    + +
    +

    Imports (131). Used by: pyweb.py (157)

    +
    +

    WebReader load the web (132) =

    -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
     
         # 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
    +    # **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.warning('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)
     
    -

    WebReader load the web (129). Used by: WebReader class... (114)

    +

    WebReader load the web (132). 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 (133) =

     # 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 (133). 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 +5314,40 @@

      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 (134) +=

       import re
      +from collections.abc import Iterator, Iterable
       
      -

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

      +

      Imports (134). Used by: pyweb.py (157)

      -

      Tokenizer class - breaks input into tokens (132) =

      +

      Tokenizer class - breaks input into tokens (135) =

      -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 (135). 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 +5365,75 @@

    The Option Parser Class

    To handle this, we have a separate lexical scanner and parser for these two commands.

    -

    Imports (133) +=

    +

    Imports (136) +=

     import shlex
     
    -

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

    +

    Imports (136). Used by: pyweb.py (157)

    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 (137) =

    +
    +class ParseError(Exception): pass
    +
    + +
    +

    Option Parser class - locates optional values on commands (137). Used by: Base Class Definitions (1)

    +
    +

    Option Parser class - locates optional values on commands (138) +=

     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 (138). 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 (139) +=

     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 +5441,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 (139). 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 +5468,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??
             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 @@ -5361,44 +5491,44 @@

    Action Class Hierarchy

    This two pass action might be embedded in the following type of Python program.

     import pyweb, os, runpy, sys
    -pyweb.tangle( "source.w" )
    +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" )
    +    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 +

    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 (140) =

    -→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 (141)
    +→ActionSequence subclass that holds a sequence of other actions (144)
    +→WeaveAction subclass initiates the weave action (148)
    +→TangleAction subclass initiates the tangle action (151)
    +→LoadAction subclass loads the document web (154)
     
    -

    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 (140). 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 +5538,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 +5561,58 @@

    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 (141) =

     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 (142)
    +    →Action final summary of what was done (143)
     
    -

    Action superclass has common features of all actions (137). Used by: Action class hierarchy... (136)

    +

    Action superclass has common features of all actions (141). Used by: Action class hierarchy... (140)

    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 (142) =

    -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 (142). Used by: Action superclass... (141)

    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 (143) =

    -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 (143). Used by: Action superclass... (141)

    -

    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 +5622,68 @@

    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 (144) =

    -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 (145)
    +    →ActionSequence append adds a new action to the sequence (146)
    +    →ActionSequence summary summarizes each step (147)
     
    -

    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 (144). Used by: Action class hierarchy... (140)

    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 (145) =

    -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 (145). Used by: ActionSequence subclass... (144)

    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 (146) =

    -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 (146). Used by: ActionSequence subclass... (144)

    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 (147) =

    -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 (147). Used by: ActionSequence subclass... (144)

    -

    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 +5692,66 @@

    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 (148) =

    -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 (145)
    -    →WeaveAction summary of language choice (146)
    +    →WeaveAction call method to pick the language (149)
    +    →WeaveAction summary of language choice (150)
     
    -

    WeaveAction subclass initiates the weave action (144). Used by: Action class hierarchy... (136)

    +

    WeaveAction subclass initiates the weave action (148). Used by: Action class hierarchy... (140)

    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 (149) =

    -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
         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 (149). Used by: WeaveAction subclass... (148)

    The summary() method returns some basic processing statistics for the weave action.

    -

    WeaveAction summary of language choice (146) =

    +

    WeaveAction summary of language choice (150) =

    -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 (150). Used by: WeaveAction subclass... (148)

    -

    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 +5759,76 @@

    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 (151) =

    -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 (152)
    +    →TangleAction summary method provides total lines tangled (153)
     
    -

    TangleAction subclass initiates the tangle action (147). Used by: Action class hierarchy... (136)

    +

    TangleAction subclass initiates the tangle action (151). Used by: Action class hierarchy... (140)

    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 (152) =

    -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
         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 (152). Used by: TangleAction subclass... (151)

    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 (153) =

    -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 (153). Used by: TangleAction subclass... (151)

    -

    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 (154) =

    -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 (155)
    +    →LoadAction summary provides lines read (156)
     
    -

    LoadAction subclass loads the document web (150). Used by: Action class hierarchy... (136)

    +

    LoadAction subclass loads the document web (154). Used by: Action class hierarchy... (140)

    Trying to load the web involves two steps, either of which can raise exceptions due to incorrect inputs.

    @@ -5709,25 +5845,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 (155) =

    -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 +5872,38 @@ 

    LoadAction Class

    -

    LoadAction call method loads the input files (151). Used by: LoadAction subclass... (150)

    +

    LoadAction call method loads the input files (155). Used by: LoadAction subclass... (154)

    The summary() method returns some basic processing statistics for the load action.

    -

    LoadAction summary provides lines read (152) =

    +

    LoadAction summary provides lines read (156) =

    -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 (156). Used by: LoadAction subclass... (154)

    -

    pyWeb Module File

    +

    pyWeb Module File

    The pyWeb application file is shown below:

    -

    pyweb.py (153) =

    +

    pyweb.py (157) =

    -→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 (159), →(160), →(161)
    +→Imports (3), →(12), →(48), →(58), →(98), →(126), →(131), →(134), →(136), →(158), →(162), →(168)
    +→Base Class Definitions (1)
    +→Application Class (163), →(164)
    +→Logging Setup (169), →(170)
    +→Interface Functions (171)
     
    -

    pyweb.py (153).

    +

    pyweb.py (157).

    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 +5933,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 (158) +=

     import os
     import time
    @@ -5807,34 +5942,34 @@ 

    Python Library Imports

    -

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

    +

    Imports (158). Used by: pyweb.py (157)

    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 (159) =

     #!/usr/bin/env python
     
    -

    Overheads (155). Used by: pyweb.py (153)

    +

    Overheads (159). Used by: pyweb.py (157)

    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 (160) +=

    -"""pyWeb Literate Programming - tangle and weave tool.
    +"""py-web-tool Literate Programming.
     
     Yet another simple literate programming tool derived from nuweb,
     implemented entirely in Python.
    @@ -5864,30 +5999,30 @@ 

    Overheads

    -

    Overheads (156). Used by: pyweb.py (153)

    +

    Overheads (160). Used by: pyweb.py (157)

    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 (161) +=

    -__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'.
    +### From source pyweb.w modified Fri Jun 10 10:48:04 2022.
    +### In working directory '/Users/slott/Documents/Projects/py-web-tool'.
     
    -

    Overheads (157). Used by: pyweb.py (153)

    +

    Overheads (161). Used by: pyweb.py (157)

    -

    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 +6043,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 (162) +=

     import argparse
     
    -

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

    +

    Imports (162). Used by: pyweb.py (157)

    -

    Application Class (159) =

    +

    Application Class (163) =

     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 (165)
    +
    +    →Application parse command line (166)
    +    →Application class process all files (167)
     
    -

    Application Class (159). Used by: pyweb.py (153)

    +

    Application Class (163). Used by: pyweb.py (157)

    The first part of parsing the command line is setting default values that apply when parameters are omitted. @@ -6016,7 +6152,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 (164) +=

     # Global list of available weaver classes.
     weavers = {
    @@ -6028,61 +6164,61 @@ 

    The Application Class

    -

    Application Class (160). Used by: pyweb.py (153)

    +

    Application Class (164). Used by: pyweb.py (157)

    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 (165) =

    +
    +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 )
    +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 (165). Used by: Application Class... (163)

    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 (166) =

    -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("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.
         """
    @@ -6091,33 +6227,33 @@ 

    The Application Class

    elif config.reference == 's': config.reference_style = SimpleReference() else: - raise Error( "Improper configuration" ) + raise Error("Improper configuration") 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() + 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 (166). Used by: Application Class... (163)

    The process() function uses the current Application settings to process each file as follows:

    @@ -6125,7 +6261,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 +6273,122 @@

    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 (167) =

    -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
    +            self.theAction = self.doTangle
             elif config.skip.lower().startswith('t'): # not tangling == weaving
    -            self.theAction= self.doWeave
    +            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 (167). Used by: Application Class... (163)

  • -

    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 (168) +=

     import logging
     import logging.config
     
    -

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

    +

    Imports (168). Used by: pyweb.py (157)

    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 (169) =

     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 (169). Used by: pyweb.py (157)

    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 (170) +=

    -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 (170). Used by: pyweb.py (157)

    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 +6396,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 (171) =

    -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 (171). Used by: pyweb.py (157)

    This can be extended by doing something like the following.

      @@ -6284,19 +6420,19 @@

      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

    +

    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

    @@ -6315,7 +6451,7 @@

    Unit Tests

    -

    Additional Files

    +

    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 @@ -6323,7 +6459,7 @@

    Additional Files

    publishing from GitHub.

    The .CSS file and .conf file for RST production are also provided here.

    -

    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 (173) =

    -→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 (174)
    +→weave.py custom weaver definition to customize the Weaver being used (175)
    +→weaver.py processing: load and weave the document (176)
     
    -

    weave.py (169).

    +

    weave.py (173).

    -

    weave.py overheads for correct operation of a script (170) =

    +

    weave.py overheads for correct operation of a script (174) =

     #!/usr/bin/env python3
     """Sample weave.py script."""
    @@ -6398,31 +6534,31 @@ 

    weave.py
    -

    weave.py overheads for correct operation of a script (170). Used by: weave.py (169)

    +

    weave.py overheads for correct operation of a script (174). Used by: weave.py (173)

    -

    weave.py custom weaver definition to customize the Weaver being used (171) =

    +

    weave.py custom weaver definition to customize the Weaver being used (175) =

    -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,67 +6567,67 @@ 

    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 (175). Used by: weave.py (173)

    -

    weaver.py processing: load and weave the document (172) =

    +

    weaver.py processing: load and weave the document (176) =

    -with pyweb.Logger( pyweb.log_config ):
    -    logger= logging.getLogger(__file__)
    +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(),
    +            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()
    +    w = pyweb.Web()
     
         for action in LoadAction(), WeaveAction():
    -            action.web= w
    -            action.options= options
    +            action.web = w
    +            action.options = options
                 action()
    -            logger.info( action.summary() )
    +            logger.info(action.summary())
     
    -

    weaver.py processing: load and weave the document (172). Used by: weave.py (169)

    +

    weaver.py processing: load and weave the document (176). Used by: weave.py (173)

    -
    -

    The setup.py and MANIFEST.in files

    +
    -

    The README file

    +

    The README file

    Here's the README file.

    -

    README (175) =

    +

    README (180) =

    -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
    @@ -6544,7 +6692,7 @@ 

    The README 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. @@ -6552,16 +6700,24 @@

    The README Installation ------------- +This requires Python 3.10. + +First, downnload the distribution kit from PyPI. + :: python3 setup.py install -This will install the pyweb module. +This will install the ``pyweb`` module, and the ``weave`` and ``tangle`` applications. -Document production --------------------- +Produce Documentation +--------------------- -The supplied documentation uses RST markup and requires docutils. +The supplied documentation uses RST markup; it requires docutils. + +:: + + python3 -m pip install docutils :: @@ -6571,16 +6727,16 @@

    The README 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. 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 :: @@ -6611,34 +6767,35 @@

    The README python3 -m pyweb pyweb_test.w PYTHONPATH=.. python3 test.py rst2html.py pyweb_test.rst pyweb_test.html + mypy --strict pyweb.py

    -

    README (175).

    +

    README (180).

    -

    The HTML Support Files

    +

    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 (181) =

     # 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
     
    -

    docutils.conf (176).

    +

    docutils.conf (181).

    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.css (182) =

     /* Page layout tweaks */
     div.document { width: 7in; }
    @@ -6660,13 +6817,13 @@ 

    The HTML Support Files

    -

    page-layout.css (177).

    +

    page-layout.css (182).

    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) =

    +

    .nojekyll (183) =

    -

    System Message: ERROR/3 (pyweb.rst, line 8444)

    +

    System Message: ERROR/3 (pyweb.rst, line 8633)

    Content block expected for the "parsed-literal" directive; none found.

     ..  parsed-literal::
    @@ -6678,10 +6835,10 @@ 

    The HTML Support Files

    -

    .nojekyll (178).

    +

    .nojekyll (183).

    -

    Finally, an index.html to redirect GitHub to the pyweb.html file.

    -

    index.html (179) =

    +

    Here's an index.html to redirect GitHub to the pyweb.html file.

    +

    index.html (184) =

     <?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">
    @@ -6694,33 +6851,100 @@ 

    The HTML Support Files

    -

    index.html (179).

    +

    index.html (184).

    +
    +
    +
    +

    Tox and Makefile

    +

    It's simpler to have a Makefile to automate testing, particularly when making changes +to py-web-tool.

    +

    Note that there are tabs in this file. We bootstrap the next version from the 3.0 version.

    +

    Makefile (185) =

    +
    +# Makefile for py-web-tool.
    +# Requires a pyweb-3.0.py (untouched) to bootstrap the current version.
    +
    +SOURCE = pyweb.w intro.w overview.w impl.w tests.w additional.w todo.w done.w \
    +    test/pyweb_test.w test/intro.w test/unit.w test/func.w test/combined.w
    +
    +.PHONY : test build
    +
    +# Note the bootstrapping new version from version 3.0 as baseline.
    +# Handy to keep this *outside* the project's Git repository.
    +PYWEB_BOOTSTRAP=/Users/slott/Documents/Projects/PyWebTool-3/pyweb/pyweb.py
    +
    +test : $(SOURCE)
    +    python3 $(PYWEB_BOOTSTRAP) -xw pyweb.w
    +    cd test && python3 ../pyweb.py pyweb_test.w
    +    cd test && PYTHONPATH=.. python3 test.py
    +    cd test && rst2html.py pyweb_test.rst pyweb_test.html
    +    mypy --strict --show-error-codes pyweb.py
    +
    +build : pyweb.py pyweb.html
    +
    +pyweb.py pyweb.rst : $(SOURCE)
    +    python3 $(PYWEB_BOOTSTRAP) pyweb.w
    +
    +pyweb.html : pyweb.rst
    +    rst2html.py $< $@
    +
    + +
    +

    Makefile (185).

    +
    +

    TODO: Finish tox.ini or pyproject.toml.

    +

    pyproject.toml (186) =

    +
    +[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 = """
    +[tox]
    +envlist = py310
    +
    +[testenv]
    +deps =
    +    pytest == 7.1.2
    +    mypy == 0.910
    +commands_pre =
    +    python3 pyweb-3.0.py pyweb.w
    +    python3 pyweb.py -o test test/pyweb_test.w
    +commands =
    +    python3 test/test.py
    +    mypy --strict pyweb.py
    +"""
    +
    + +
    +

    pyproject.toml (186).

    -

    JEdit Configuration

    +

    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) =

    +

    jedit/pyweb.xml (187) =

     <?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)
    +    →props for JEdit mode (188)
    +    →rules for JEdit PyWeb and RST (189)
    +    →rules for JEdit PyWeb XML-Like Constructs (190)
     </MODE>
     
    -

    jedit/pyweb.xml (180).

    +

    jedit/pyweb.xml (187).

    Here are some properties to define RST constructs to JEdit

    -

    props for JEdit mode (181) =

    +

    props for JEdit mode (188) =

     <PROPS>
         <PROPERTY NAME="lineComment" VALUE=".. "/>
    @@ -6734,10 +6958,10 @@ 

    JEdit Configuration

    -

    props for JEdit mode (181). Used by: jedit/pyweb.xml (180)

    +

    props for JEdit mode (188). Used by: jedit/pyweb.xml (187)

    Here are some rules to define PyWeb and RST constructs to JEdit.

    -

    rules for JEdit PyWeb and RST (182) =

    +

    rules for JEdit PyWeb and RST (189) =

     <RULES IGNORE_CASE="FALSE" HIGHLIGHT_DIGITS="FALSE">
     
    @@ -6873,11 +7097,11 @@ 

    JEdit Configuration

    -

    rules for JEdit PyWeb and RST (182). Used by: jedit/pyweb.xml (180)

    +

    rules for JEdit PyWeb and RST (189). Used by: jedit/pyweb.xml (187)

    Here are some additional rules to define PyWeb constructs to JEdit that look like XML.

    -

    rules for JEdit PyWeb XML-Like Constructs (183) =

    +

    rules for JEdit PyWeb XML-Like Constructs (190) =

     <RULES SET="CODE" DEFAULT="KEYWORD1">
         <SPAN TYPE="MARKUP">
    @@ -6888,7 +7112,7 @@ 

    JEdit Configuration

    -

    rules for JEdit PyWeb XML-Like Constructs (183). Used by: jedit/pyweb.xml (180)

    +

    rules for JEdit PyWeb XML-Like Constructs (190). Used by: jedit/pyweb.xml (187)

    Additionally, you'll want to update the JEdit catalog.

    @@ -6904,21 +7128,33 @@ 

    JEdit Configuration

    +
    +

    Python 3.10 Migration

    +
      +
    1. [x] Add type hints.
    2. +
    3. [x] Replace all .format() with f-strings.
    4. +
    5. [x] Replace filename strings (and os.path) with pathlib.Path.
    6. +
    7. [ ] Add abc to formalize Abstract Base Classes.
    8. +
    9. [ ] Introduce match statements for some of the elif blocks.
    10. +
    11. [ ] Introduce pytest instead of building a test runner.
    12. +
    13. [ ] pyproject.toml. This requires `-o dir option to write output to a directory of choice; which requires pathlib.
    14. +
    15. [ ] Replace various mock classes with unittest.mock.Mock objects and appropriate extended testing.
    16. +
    +
    -

    To Do

    +

    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 ERROR-level logging during testing.

    3. -
    4. Silence the logging during testing.

      +
    5. Silence the error when creating an empty file i.e. .nojekyll

    6. -
    7. Add a JSON-based configuration file to configure templates.

      +
    8. Add a JSON-based (or TOML) 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.

        +
      • An alternative is to get markup templates from some kind of "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 @@ -6934,6 +7170,9 @@

        To Do

      • We might want to interleave code and test into a document that presents both side-by-side. They get routed to different output files.

      • +
      • Fix name definition order. There's no good reason why a full name should +be first and elided names defined later.

        +
      • 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 @@ -6960,7 +7199,7 @@

        To Do

    -

    Other Thoughts

    +

    Other Thoughts

    There are two possible projects that might prove useful.

    -

    Change Log

    +

    Change Log

    +

    Changes for 3.1

    +
      +
    • Change to Python 3.10.
    • +
    • Add type hints, f-strings, pathlib.
    • +
    • Replace some complex elif blocks with match statements
    • +
    • Remove the Jedit configuration file as an output.
    • +
    • Add a Makefile, pyproject.toml, requirements.txt and requirements-dev.txt.
    • +

    Changes for 3.0

    • Move to GitHub
    • @@ -7071,747 +7318,762 @@

      Change Log

    -

    Indices

    +

    Indices

    -

    Files

    +

    Files

    - + + + - + - + - + - + - + - + - + - + - + + - + + + + +
    .nojekyll:→(178)
    .nojekyll:→(183)
    MANIFEST.in:→(178)
    MANIFEST.in:→(174)
    Makefile:→(185)
    README:→(175)
    README:→(180)
    docutils.conf:→(176)
    docutils.conf:→(181)
    index.html:→(179)
    index.html:→(184)
    jedit/pyweb.xml:
     →(180)
     →(187)
    page-layout.css:
     →(177)
     →(182)
    pyweb.py:→(153)
    pyproject.toml:→(186)
    setup.py:→(173)
    pyweb.py:→(157)
    tangle.py:→(168)
    requirements-dev.txt:
     →(179)
    weave.py:→(169)
    setup.py:→(177)
    tangle.py:→(172)
    weave.py:→(173)
    -

    Macros

    +

    Macros

    - + - - + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + +→(59) - + - + - + - + - + - + - + +→(80) - + - + - + - + - + - + - + +→(11) - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - - + + - + - + - + - + - + - + - + - + - + +→(176)
    Action call method actually does the real work:
     →(138)
     →(142)
    Action class hierarchy - used to describe basic actions of the application:
     →(136)
    Action class hierarchy - used to describe actions of the application:
     →(140)
    Action final summary of what was done:
     →(139)
     →(143)
    Action superclass has common features of all actions:
     →(137)
     →(141)
    ActionSequence append adds a new action to the sequence:
     →(142)
     →(146)
    ActionSequence call method delegates the sequence of ations:
     →(141)
     →(145)
    ActionSequence subclass that holds a sequence of other actions:
     →(140)
     →(144)
    ActionSequence summary summarizes each step:
     →(143)
     →(147)
    Application Class:
     →(159) →(160)
     →(163) →(164)
    Application class process all files:
     →(163)
     →(167)
    Application default options:
     →(161)
     →(165)
    Application parse command line:
     →(162)
     →(166)
    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 class:→(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) →(126) →(131) →(134) →(136) →(158) →(162) →(168)
    Interface Functions:
     →(167)
     →(171)
    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)
     →(155)
    LoadAction subclass loads the document web:
     →(150)
     →(154)
    LoadAction summary provides lines read:
     →(152)
     →(156)
    Logging Setup:→(165) →(166)
    Logging Setup:→(169) →(170)
    MacroXrefCommand class for a named chunk cross-reference:
     →(84)
     →(86)
    NamedChunk add to the web:
     →(65)
     →(67)
    NamedChunk class:
     →(63) →(68)
     →(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)
     →(137) →(138) →(139)
    OutputChunk add to the web:
     →(70)
     →(72)
    OutputChunk class:
     →(69)
     →(71)
    OutputChunk tangle:
     →(72)
     →(74)
    OutputChunk weave:
     →(71)
     →(73)
    Overheads:→(155) →(156) →(157)
    Overheads:→(159) →(160) →(161)
    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)
     →(152)
    TangleAction subclass initiates the tangle action:
     →(147)
     →(151)
    TangleAction summary method provides total lines tangled:
     →(149)
     →(153)
    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)
     →(135)
    UserIdXrefCommand class for a user identifier cross-reference:
     →(85)
     →(87)
    WeaveAction call method to pick the language:
     →(145)
     →(149)
    WeaveAction subclass initiates the weave action:
     →(144)
     →(148)
    WeaveAction summary of language choice:
     →(146)
     →(150)
    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)
     →(133)
    WebReader handle a command string:
     →(115) →(127)
     →(117) →(129)
    WebReader load the web:
     →(129)
     →(132)
    WebReader location in the input stream:
     →(128)
     →(130)
    XrefCommand superclass for all cross-reference commands:
     →(82)
     →(84)
    add a reference command to the current chunk:
     →(123)
     →(125)
    add an expression command to the current chunk:
     →(125)
     →(127)
    assign user identifiers to the current chunk:
     →(122)
     →(124)
    collect all user identifiers from a given map into ux:
     →(109)
     →(111)
    double at-sign replacement, append this character to previous TextCommand:
     →(126)
     →(128)
    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)
     →(122)
    import another file:
     →(119)
    include another file:
     →(121)
    major commands segment the input into separate Chunks:
     →(116)
     →(118)
    minor commands add Commands to the current Chunk:
     →(121)
     →(123)
    props for JEdit mode:
     →(181)
     →(188)
    rules for JEdit PyWeb XML-Like Constructs:
     →(183)
     →(190)
    rules for JEdit PyWeb and RST:
     →(182)
     →(189)
    start a NamedChunk or NamedDocumentChunk, adding it to the web:
     →(118)
     →(120)
    start an OutputChunk, adding it to the web:
     →(117)
     →(119)
    weave.py custom weaver definition to customize the Weaver being used:
     →(171)
     →(175)
    weave.py overheads for correct operation of a script:
     →(170)
     →(174)
    weaver.py processing:
     load and weave the document: -→(172)
    -

    User Identifiers

    +

    User Identifiers

    - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + + + + + - + + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - +
    Action:[137] 140 144 147 150
    Action:[141] 144 146 148 151 154
    ActionSequence:[140] 161
    ActionSequence:[144] 165
    Application:[159] 167
    Application:[163] 171
    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 121 122 125 132
    CodeCommand:63 [81]
    CodeCommand:65 [83]
    Command:53 [77] 80 82 86 163
    Command:53 54 57 65 75 [79] 82 84 88 167
    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 120 121 127 139 149 152 155 166
    FileXrefCommand:
     [83] 121
     [85] 123
    HTML:31 [32] 111 160 171
    HTML:32 [33] 113 164 175
    LaTeX:[23] 111 160
    LaTeX:[24] 113 164
    LoadAction:[150] 161 168 172
    LoadAction:[154] 165 172 176
    MacroXrefCommand:
     [84] 121
     [86] 123
    NamedChunk:[63] 68 69 73 118
    NamedChunk:59 [65] 70 71 75 120
    NamedDocumentChunk:
     [73] 118
     [75] 120
    OutputChunk:[71] 119
    OutputChunk:[69] 117
    Path:[3] 4 5 6 14 45 50 53 97 114 116 121 132 166
    ReferenceCommand:
     [86] 123
     [88] 125
    TangleAction:[147] 161 168
    TangleAction:[151] 165 172
    Tangler:3 [43] 48 162
    Tangler:4 [44] 49 63 64 69 70 74 77 81 82 83 84 92 114 166
    TanglerMake:[48] 162 166 168 172
    TanglerMake:[49] 166 170 172 176
    TextCommand:54 56 67 73 [80] 81
    TextCommand:55 57 69 75 [82] 83
    Tokenizer:129 [132]
    Tokenizer:116 132 [135]
    UserIdXrefCommand:
     [85] 121
     [87] 123
    WeaveAction:[144] 161 172
    WeaveAction:[148] 165 176
    Weaver:[12] 22 23 31 162 163
    Weaver:[13] 23 24 32 61 62 68 73 76 81 82 83 84 85 86 87 91 113 115 166 167
    Web:45 55 65 70 [95] 151 163 168 172 175
    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 132 141 155 167 172 176 180
    WebReader:[114] 119 162 166 168 172
    WebReader:[116] 121 132 166 170 172 176
    XrefCommand:[82] 83 84 85
    XrefCommand:[84] 85 86 87
    __version__:125 [157]
    __enter__:[5] 169
    _gatherUserId:[108]
    __exit__:[5] 169
    _updateUserId:[108]
    __version__:127 [161]
    add:55 [99]
    _gatherUserId:[110]
    addDefName:[98] 100 123
    _updateUserId:[110]
    addIndent:10 [13] 62 66
    add:56 [101]
    addNamed:65 [100]
    addDefName:[100] 102 125
    addOutput:70 [101]
    addIndent:11 [14] 64 68
    append:10 13 53 54 93 99 100 101 104 110 121 123 135 [142]
    addNamed:67 [102]
    appendText:[54] 123 125 126 129
    addOutput:72 [103]
    argparse:[158] 161 162 168 170 172
    append:11 14 54 55 95 101 102 103 106 112 123 125 139 [146]
    builtins:[124] 125
    appendText:[55] 125 127 128 132
    chunkXref:84 [107]
    argparse:141 [162] 165 166 167 172 174 176
    close:[4] 13 44 50
    builtins:[126] 127
    clrIndent:[10] 62 66 68
    chunkXref:86 [109]
    codeBegin:17 [45] 66 67
    close:[5] 14 45 51
    codeBlock:[7] 66 81
    clrIndent:[11] 64 68 70
    codeEnd:17 [46] 66 67
    codeBegin:18 [46] 68 69
    codeFinish:4 9 [13]
    codeBlock:[8] 68 83
    createUsedBy:[104] 151
    codeEnd:18 [47] 68 69
    datetime:125 [154]
    codeFinish:5 10 [14]
    doClose:4 6 13 44 [50]
    createUsedBy:[106] 155
    doOpen:4 5 13 44 [49]
    datetime:127 [158]
    docBegin:[15] 60
    doClose:5 7 14 45 [51]
    docEnd:[15] 60
    doOpen:5 6 14 45 [50]
    duration:[139] 146 149 152
    docBegin:[16] 62
    expand:74 123 161 [162]
    docEnd:[16] 62
    expect:117 118 123 125 [127]
    duration:[143] 150 153 156
    fileBegin:18 [35] 71
    expand:76 125 165 [166]
    fileEnd:18 [36] 71
    expect:119 120 125 127 [129]
    fileXref:83 [107]
    fileBegin:19 [36] 73
    filecmp:[47] 50
    fileEnd:19 [37] 73
    formatXref:[82] 83 84
    fileXref:85 [109]
    fullNameFor:66 71 87 98 [102] 103 104
    filecmp:[48] 51
    genReferences:[58] 104
    formatXref:[84] 85 86
    getUserIDRefs:57 [64] 109
    fullNameFor:68 73 89 100 [104] 105 106
    getchunk:87 [103] 104 113
    genReferences:[60] 106
    handleCommand:[115] 129
    getUserIDRefs:59 [66] 111
    language:[111] 145 156 175
    getchunk:89 [105] 106 115
    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
    handleCommand:[117] 132
    load:119 [129] 151 161 163
    language:[113] 149 160 180
    location:115 122 125 127 [128]
    lineNumber:18 19 34 36 46 55 57 [59] 65 69 75 79 82 84 88 121 123 125 127 128 130 132 135 175
    logging:3 77 91 95 114 137 159 161 162 163 [164] 165 166 168 170 172
    load:121 [132] 155 165 167
    logging.config:[164] 165
    location:117 124 127 129 [130]
    main:[167]
    logging:4 53 79 93 97 116 141 163 165 166 167 [168] 169 170 172 174 176
    makeContent:54 [56] 63 73
    logging.config:[168] 169
    main:[171]
    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 127 132
    open:[4] 13 44 112 113 125 129
    os:50 51 127 [158]
    os:44 49 50 113 125 [154]
    parse:119 120 [132] 139
    parse:117 118 [129] 135
    parseArgs:[166] 171
    parseArgs:[162] 167
    perform:[155]
    perform:[151]
    platform:[126] 127
    platform:[124] 125
    process:127 [167] 171
    process:125 [163] 167
    quote:[9] 83
    quote:[8] 81
    quoted_chars:9 15 30 [39]
    quoted_chars:8 14 29 [38]
    re:112 [134] 135 180
    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 124 160 165 175
    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] 124
    shlex:[133] 135
    shlex:[136] 139
    startswith:57 [78] 80 102 111 129 135 163
    startswith:59 [80] 82 104 113 132 139 167
    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 174 175
    summary:139 143 146 149 [152] 163 168 172
    summary:143 147 150 153 [156] 167 172 176
    sys:[124] 125 166
    sys:[126] 127 170 171
    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] 152 165 172 180
    tempfile:[47] 49
    tempfile:[48] 50
    time:138 139 [154]
    time:127 142 143 [158]
    types:12 125 [154]
    types:13 127 [158]
    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] 149 165 174 180
    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] 119 120 121 122 132
    write:[4] 7 9 17 18 20 21 45 80 113
    write:[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/PyWebTool-3/pyweb/pyweb.py at Fri Jun 10 13:42:20 2022. +

    Source pyweb.w modified Fri Jun 10 10:48:04 2022.

    pyweb.__version__ '3.0'.

    -

    Working directory '/Users/slott/Documents/Projects/PyWebTool-3/pyweb'.

    +

    Working directory '/Users/slott/Documents/Projects/py-web-tool'.

    diff --git a/pyweb.py b/pyweb.py index fffa5ae..5d77dde 100644 --- a/pyweb.py +++ b/pyweb.py @@ -29,17 +29,20 @@ __version__ = """3.1""" ### DO NOT EDIT THIS FILE! -### It was created by pyweb-3.0.py, __version__='3.0'. -### From source pyweb.w modified Wed Jun 8 14:04:44 2022. +### It was created by /Users/slott/Documents/Projects/PyWebTool-3/pyweb/pyweb.py, __version__='3.0'. +### From source pyweb.w modified Fri Jun 10 10:48:04 2022. ### In working directory '/Users/slott/Documents/Projects/py-web-tool'. +from pathlib import Path +import abc + import string +from textwrap import dedent import tempfile import filecmp from typing import Pattern, Match, Optional, Any, Literal - import weakref @@ -70,11 +73,13 @@ + class Error(Exception): pass -class Command: + +class Command(abc.ABC): """A Command is the lowest level of granularity in the input stream.""" chunk : "Chunk" text : str @@ -97,15 +102,20 @@ def indent(self) -> int: def ref(self, aWeb: "Web") -> str | None: return None + + @abc.abstractmethod def weave(self, aWeb: "Web", aWeaver: "Weaver") -> None: - pass + ... + + @abc.abstractmethod def tangle(self, aWeb: "Web", aTangler: "Tangler") -> None: - pass + ... + class TextCommand(Command): """A piece of document source text.""" def __init__(self, text: str, fromLine: int = 0) -> None: @@ -132,6 +142,7 @@ def tangle(self, aWeb: "Web", aTangler: "Tangler") -> None: + class CodeCommand(TextCommand): """A piece of program source code.""" def weave(self, aWeb: "Web", aWeaver: "Weaver") -> None: @@ -141,6 +152,7 @@ def tangle(self, aWeb: "Web", aTangler: "Tangler") -> None: + class XrefCommand(Command): """Any of the Xref-goes-here commands in the input.""" def __str__(self) -> str: @@ -157,6 +169,7 @@ def tangle(self, aWeb: "Web", aTangler: "Tangler") -> None: + class FileXrefCommand(XrefCommand): """A FileXref command.""" def weave(self, aWeb: "Web", aWeaver: "Weaver") -> None: @@ -165,6 +178,7 @@ def weave(self, aWeb: "Web", aWeaver: "Weaver") -> None: + class MacroXrefCommand(XrefCommand): """A MacroXref command.""" def weave(self, aWeb: "Web", aWeaver: "Weaver") -> None: @@ -173,6 +187,7 @@ def weave(self, aWeb: "Web", aWeaver: "Weaver") -> None: + class UserIdXrefCommand(XrefCommand): """A UserIdXref command.""" def weave(self, aWeb: "Web", aWeaver: "Weaver") -> None: @@ -189,6 +204,7 @@ def weave(self, aWeb: "Web", aWeaver: "Weaver") -> None: + class ReferenceCommand(Command): """A reference to a named chunk, via @.""" def __init__(self, refTo: str, fromLine: int = 0) -> None: @@ -245,11 +261,13 @@ def tangle(self, aWeb: "Web", aTangler: "Tangler") -> None: + 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 @@ -257,7 +275,6 @@ def __init__(self) -> None: self.name: str = '' self.fullName: str = "" self.seq: int = 0 - self.fileName = '' 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 @@ -378,6 +395,7 @@ def reference_dedent(self, aWeb: "Web", aTangler: "Tangler") -> None: + class NamedChunk(Chunk): """Named piece of input file: will be output as both tangler and weaver.""" def __init__(self, name: str) -> None: @@ -455,6 +473,7 @@ 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: @@ -486,6 +505,7 @@ def tangle(self, aWeb: "Web", aTangler: "Tangler") -> None: + class NamedDocumentChunk(NamedChunk): """Named piece of input file with document source, defines an output tangle.""" @@ -515,10 +535,11 @@ def tangle(self, aWeb: "Web", aTangler: "Tangler") -> None: + class Web: """The overall Web of chunks.""" - def __init__(self, filename: str | None = None) -> None: - self.webFileName = filename + 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 @@ -527,7 +548,7 @@ def __init__(self, filename: str | None = None) -> None: self.logger = logging.getLogger(self.__class__.__qualname__) def __str__(self) -> str: - return f"Web {self.webFileName!r}" + return f"Web {self.web_path!r}" @@ -588,15 +609,22 @@ def addOutput(self, chunk: Chunk) -> None: 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(f"Ambiguous abbreviation {name!r}, matches {list(sorted(best))!r}") - 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 def getchunk(self, name: str) -> list[Chunk]: @@ -618,6 +646,7 @@ def createUsedBy(self) -> None: 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) @@ -694,18 +723,17 @@ def language(self, preferredWeaverClass: type["Weaver"] | None = None) -> "Weave 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) def weave(self, aWeaver: "Weaver") -> None: - self.logger.debug("Weaving file from %r", self.webFileName) - if not self.webFileName: + self.logger.debug("Weaving file from %r", self.web_path) + if not self.web_path: raise Error("No filename supplied for weaving.") - basename, _ = os.path.splitext(self.webFileName) - with aWeaver.open(basename): + with aWeaver.open(self.web_path): for c in self.chunkSeq: c.weave(self, aWeaver) @@ -723,21 +751,25 @@ def weaveChunk(self, name: str, aWeaver: "Weaver") -> None: + 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: @@ -770,7 +802,7 @@ def _group(self, word_iter: Iterator[str]) -> Iterator[tuple[str, list[str]]]: try: 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: @@ -796,6 +828,7 @@ def _group(self, word_iter: Iterator[str]) -> Iterator[tuple[str, list[str]]]: yield self.trailers[0], final + class WebReader: """Parse an input file, creating Chunks and Commands.""" @@ -821,7 +854,7 @@ class WebReader: # State of the reader _source: TextIO - fileName: str + filePath: Path theWeb: "Web" def __init__(self, parent: Optional["WebReader"] = None) -> None: @@ -870,24 +903,24 @@ def __str__(self) -> str: def location(self) -> tuple[str, int]: - return (self.fileName, self.tokenizer.lineNumber+1) + return (str(self.filePath), self.tokenizer.lineNumber+1) - def load(self, web: "Web", filename: str, source: TextIO | None = None) -> "WebReader": + def load(self, web: "Web", filepath: Path, source: TextIO | None = None) -> "WebReader": self.theWeb = web - self.fileName = filename + self.filePath = filepath # 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 + # **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 open(self.fileName, "r") as self._source: + with self.filePath.open() as self._source: self.parse_source() return self @@ -903,152 +936,153 @@ def parse_source(self) -> None: if self.handleCommand(token): continue else: - self.logger.warning('Unknown @-command in input: %r', token) + 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 token[:2] == 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.fileName = self.fileName - self.aChunk.webAdd(self.theWeb) - # capture an OutputChunk up to @} - - elif token[:2] == 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) + 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: - 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 @] - - elif token[:2] == self.cmdi: - - incFile = next(self.tokenizer).strip() - try: - self.logger.info("Including %r", 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 %r, output is incomplete.", incFile) - except Error as e: - self.logger.error("Problems with included file %r, output is incomplete.", incFile) - self.errors += 1 - except IOError as e: - self.logger.error("Problems finding included file %r, output is incomplete.", incFile) - # 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) - - elif token[:2] in (self.cmdrcurl,self.cmdrbrak): - - self.aChunk = Chunk() - self.aChunk.webAdd(self.theWeb) - - + raise Error("Design Error") - elif token[:2] == 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 - - 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: - - # 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) - - elif token[:2] == self.cmdlexpr: - - # 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 ('breakpoint', 'compile', 'eval', 'exec', 'execfile', 'globals', 'help', 'input', 'memoryview', 'open', 'print', 'super', '__import__') - )) - globals = dict( - __builtins__=safe, - os=types.SimpleNamespace(path=os.path, getcwd=os.getcwd, name=os.name), - time=time, - 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 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) - - elif token[:2] == self.cmdcmd: - - self.aChunk.appendText(self.command, self.tokenizer.lineNumber) - + self.aChunk.filePath = self.filePath + self.aChunk.webAdd(self.theWeb) + # capture a NamedChunk up to @} or @] - 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", token, self.location()) - self.errors += 1 - else: - return False # did not recogize the command + case self.cmdi: + + incPath = Path(next(self.tokenizer).strip()) + try: + self.logger.info("Including %r", incPath) + include = WebReader(parent=self) + 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 + safe = types.SimpleNamespace(**dict( + (name, obj) + for name,obj in builtins.__dict__.items() + if name not in ('breakpoint', 'compile', 'eval', 'exec', 'execfile', 'globals', 'help', 'input', 'memoryview', 'open', 'print', 'super', '__import__') + )) + 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__, + ) + # 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 @@ -1058,11 +1092,11 @@ def expect(self, tokens: Iterable[str]) -> str | None: while t == '\n': t = next(self.tokenizer) except StopIteration: - self.logger.error("At %r: end of input, %r not found", self.location(),tokens) + 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.logger.error("At %r: expected %r, found %r", self.location(), tokens, t) self.errors += 1 return None return t @@ -1071,13 +1105,14 @@ def expect(self, tokens: Iterable[str]) -> str | None: + class Emitter: """Emit an output file; handling indentation context.""" code_indent = 0 # Used by a Tangler + filePath : Path theFile: TextIO def __init__(self) -> None: - self.fileName = "" self.logger = logging.getLogger(self.__class__.__qualname__) self.log_indent = logging.getLogger("indent." + self.__class__.__qualname__) # Summary @@ -1088,22 +1123,22 @@ def __init__(self) -> None: self.lastIndent = 0 self.fragment = False self.context: list[int] = [] - self.readdIndent(self.code_indent) # Create context and initial lastIndent values + self.readdIndent(self.code_indent) # Create context and initial lastIndent values def __str__(self) -> str: return self.__class__.__name__ - def open(self, aFile: str) -> "Emitter": + def open(self, aPath: Path) -> "Emitter": """Open a file.""" - self.fileName = aFile + self.filePath = aPath self.linesWritten = 0 - self.doOpen(aFile) + self.doOpen(aPath) return self - def doOpen(self, aFile: str) -> None: - self.logger.debug("creating %r", self.fileName) + def doOpen(self, aFile: Path) -> None: + self.logger.debug("Creating %r", self.filePath) @@ -1115,30 +1150,28 @@ def close(self) -> None: def doClose(self) -> None: - self.logger.debug( - "wrote %d lines to %r", self.linesWritten, self.fileName - ) + self.logger.debug("Wrote %d lines to %r", self.linesWritten, self.filePath) def write(self, text: str) -> None: if text is None: return - self.linesWritten += text.count('\n') 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. We buffer - The spaces from the last line to act as the indent for the next line. + """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') @@ -1165,7 +1198,7 @@ def codeBlock(self, text: str) -> None: else: # Last line was empty, a trailing newline. self.logger.debug("Last (Empty) Line: indent is %d", len(rest[-1]) + indent) - # Buffer a next indent + # Buffer the next indent self.lastIndent = len(rest[-1]) + indent self.fragment = False @@ -1192,15 +1225,18 @@ 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 @@ -1211,11 +1247,15 @@ def readdIndent(self, indent: int = 0) -> None: + 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 @@ -1227,18 +1267,21 @@ def __init__(self) -> None: super().__init__() - def doOpen(self, basename: str) -> None: - self.fileName = basename + self.extension - self.logger.info("Weaving %r", self.fileName) - self.theFile = open(self.fileName, "w") + def doOpen(self, basename: Path) -> None: + self.filePath = basename.with_suffix(self.extension) + self.logger.info("Weaving %r", 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.fileName) + 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 @@ -1247,18 +1290,19 @@ def codeFinish(self) -> None: # Template Expansions. + # Prevent some RST markup from being recognized (and processed) in code. quoted_chars: list[tuple[str, str]] = [ - # prevent some RST markup from being recognized - ('\\',r'\\'), # Must be first. - ('`',r'\`'), - ('_',r'\_'), - ('*',r'\*'), - ('|',r'\|'), + ('\\', r'\\'), # Must be first. + ('`', r'\`'), + ('_', r'\_'), + ('*', r'\*'), + ('|', r'\|'), ] def docBegin(self, aChunk: Chunk) -> None: pass + def docEnd(self, aChunk: Chunk) -> None: pass @@ -1267,6 +1311,7 @@ def docEnd(self, aChunk: Chunk) -> None: 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: @@ -1283,10 +1328,10 @@ def references(self, aChunk: Chunk) -> str: 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) @@ -1307,10 +1352,10 @@ def codeEnd(self, aChunk: Chunk) -> None: 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 "+=", # RST Separator + seq=aChunk.seq, + lineNumber=aChunk.lineNumber, + fullName=aChunk.fullName, + concat="=" if aChunk.initial else "+=", ) self.write(txt) @@ -1319,10 +1364,10 @@ def fileBegin(self, aChunk: Chunk) -> None: 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 = [] ) + seq=aChunk.seq, + lineNumber=aChunk.lineNumber, + fullName=aChunk.fullName, + references=[]) self.write(txt) @@ -1391,24 +1436,26 @@ class RST(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""" - cb_template = string.Template( """\\label{pyweb${seq}} + 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 + \\begin{Verbatim}[commandchars=\\\\\\{\\},codes={\\catcode`$$=3\\catcode`^=7},frame=single]\n""" + ) ce_template = string.Template(""" \\end{Verbatim} ${references} - \\end{flushleft}\n""") # Prevent indentation + \\end{flushleft}\n""") @@ -1420,9 +1467,10 @@ class LaTeX(Weaver): - ref_item_template = string.Template( """ + ref_item_template = string.Template(""" \\item Code example ${fullName} (${seq}) (Sect. \\ref{pyweb${seq}}, p. \\pageref{pyweb${seq}})\n""") - ref_template = string.Template( """ + + ref_template = string.Template(""" \\footnotesize Used by: \\begin{list}{}{} @@ -1433,14 +1481,15 @@ class LaTeX(Weaver): quoted_chars: list[tuple[str, str]] = [ - ("\\end{Verbatim}", "\\end\,{Verbatim}"), # Allow \end{Verbatim} - ("\\{","\\\,{"), # Prevent unexpected commands in Verbatim - ("$","\\$"), # Prevent unexpected math in Verbatim + ("\\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})""") @@ -1452,6 +1501,7 @@ class HTML(Weaver): extension = ".html" code_indent = 0 header = "" + cb_template = string.Template(""" @@ -1484,12 +1534,13 @@ class HTML(Weaver): ref_item_template = string.Template('${fullName} (${seq})') - ref_template = string.Template(' Used by ${refList}.' ) + + ref_template = string.Template(' Used by ${refList}.') quoted_chars: list[tuple[str, str]] = [ - ("&", "&"), # Must be first + ("&", "&"), # Must be first ("<", "<"), (">", ">"), ('"', """), @@ -1498,6 +1549,7 @@ class HTML(Weaver): refto_name_template = string.Template('${fullName} (${seq})') + refto_seq_template = string.Template('(${seq})') @@ -1506,7 +1558,9 @@ class HTML(Weaver): 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}') @@ -1523,6 +1577,7 @@ class HTMLShort(HTML): + class Tangler(Emitter): """Tangle output files.""" def __init__(self) -> None: @@ -1531,23 +1586,18 @@ def __init__(self) -> None: self.comment_end: str = "" self.include_line_numbers = False + def checkPath(self) -> None: - if "/" in self.fileName: - dirname, _, _ = self.fileName.rpartition("/") - try: - os.makedirs(dirname) - self.logger.info("Creating %r", dirname) - except OSError as exc: - # Already exists. Could check for errno.EEXIST. - self.logger.debug("Exception %r creating %r", exc, dirname) - def doOpen(self, aFile: str) -> None: - self.fileName = aFile + self.filePath.parent.mkdir(parents=True, exist_ok=True) + + def doOpen(self, aFile: Path) -> None: + self.filePath = aFile self.checkPath() - self.theFile = open(aFile, "w") + self.theFile = self.filePath.open("w") self.logger.info("Tangling %r", aFile) def doClose(self) -> None: self.theFile.close() - self.logger.info( "Wrote %d lines to %r", self.linesWritten, self.fileName) + self.logger.info("Wrote %d lines to %r", self.linesWritten, self.filePath) @@ -1556,7 +1606,7 @@ def codeBegin(self, aChunk: Chunk) -> None: if self.include_line_numbers: self.write( f"\n{self.comment_start!s} Web: " - f"{aChunk.fileName!s}:{aChunk.lineNumber!r} " + f"{aChunk.filePath.name!s}:{aChunk.lineNumber!r} " f"{aChunk.fullName!s}({aChunk.seq:d}) {self.comment_end!s}\n" ) @@ -1576,7 +1626,7 @@ def __init__(self, *args: Any) -> None: super().__init__(*args) - def doOpen(self, aFile: str) -> None: + def doOpen(self, aFile: Path) -> None: fd, self.tempname = tempfile.mkstemp(dir=os.curdir) self.theFile = os.fdopen(fd, "w") self.logger.info("Tangling %r", aFile) @@ -1587,33 +1637,37 @@ def doOpen(self, aFile: str) -> None: 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", self.fileName) + self.logger.info("No change to %r", 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 %e lines to %r", self.linesWritten, self.fileName) + pass # Doesn't exist. (Could check for errno.ENOENT) + self.checkPath() + self.filePath.hardlink_to(self.tempname) # type: ignore [attr-defined] + os.remove(self.tempname) + self.logger.info("Wrote %e lines to %s", self.linesWritten, self.filePath) -class Reference: + +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.""" - return [] + ... class SimpleReference(Reference): def chunkReferencedBy(self, aChunk: Chunk) -> list[Chunk]: @@ -1720,7 +1774,7 @@ def __call__(self) -> None: 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.webFileName) + self.logger.error("Problems weaving document from %r (weave file is faulty).", self.web.web_path) #raise @@ -1748,7 +1802,7 @@ def __call__(self) -> None: 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.webFileName) + self.logger.error("Problems tangling outputs from %r (tangle files are faulty).", self.web.web_path) #raise @@ -1776,10 +1830,10 @@ def __call__(self) -> None: self.webReader = self.options.webReader self.webReader.command = self.options.command self.webReader.permitList = self.options.permitList - self.web.webFileName = self.options.webFileName - error = f"Problems with source file {self.options.webFileName!r}, no output produced." + 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") @@ -1815,9 +1869,9 @@ def __init__(self) -> None: verbosity=logging.INFO, command='@', weaver='rst', - skip='', # Don't skip any steps - permit='', # Don't tolerate missing includes - reference='s', # Simple references + skip='', # Don't skip any steps + permit='', # Don't tolerate missing includes + reference='s', # Simple references tangler_line_numbers=False, ) self.expand(self.defaults) @@ -1845,7 +1899,7 @@ def parseArgs(self, argv: list[str]) -> argparse.Namespace: 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='+') + p.add_argument("files", nargs='+', type=Path) config = p.parse_args(argv, namespace=self.defaults) self.expand(config) return config @@ -1854,12 +1908,13 @@ 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") try: weaver_class = weavers[config.weaver.lower()] @@ -1883,7 +1938,6 @@ def expand(self, config: argparse.Namespace) -> argparse.Namespace: return config - def process(self, config: argparse.Namespace) -> None: @@ -1895,9 +1949,9 @@ def process(self, config: argparse.Namespace) -> None: self.logger.debug("Command character %r", config.command) if config.skip: - if config.skip.lower().startswith('w'): # not weaving == tangling + if config.skip.lower().startswith('w'): # not weaving == tangling self.theAction = self.doTangle - elif config.skip.lower().startswith('t'): # not tangling == weaving + elif config.skip.lower().startswith('t'): # not tangling == weaving self.theAction = self.doWeave else: raise Exception(f"Unknown -x option {config.skip!r}") @@ -1907,7 +1961,7 @@ def process(self, config: argparse.Namespace) -> None: for f in config.files: w = Web() # New, empty web to load and process. self.logger.info("%s %r", self.theAction.name, f) - config.webFileName = f + config.source_path = f self.theAction.web = w self.theAction.options = config self.theAction() @@ -1929,12 +1983,14 @@ 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 diff --git a/pyweb.rst b/pyweb.rst index d8a9650..4bf4aff 100644 --- a/pyweb.rst +++ b/pyweb.rst @@ -795,7 +795,7 @@ A global context is created with the following variables defined. Running **py-web-tool** to Tangle and Weave --------------------------------------- +------------------------------------------- Assuming that you have marked ``pyweb.py`` as executable, you do the following. @@ -1124,29 +1124,42 @@ fit elsewhere :class: code - |srarr|\ Error class - defines the errors raised (`95`_) - |srarr|\ Command class hierarchy - used to describe individual commands (`77`_) - |srarr|\ Chunk class hierarchy - used to describe input chunks (`51`_) - |srarr|\ Web class - describes the overall "web" of chunks (`96`_) - |srarr|\ Tokenizer class - breaks input into tokens (`134`_) - |srarr|\ Option Parser class - locates optional values on commands (`136`_), |srarr|\ (`137`_), |srarr|\ (`138`_) - |srarr|\ WebReader class - parses the input file, building the Web structure (`115`_) + + |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 (`92`_), |srarr|\ (`93`_), |srarr|\ (`94`_) - |srarr|\ Action class hierarchy - used to describe basic actions of the application (`139`_) + |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 (`156`_) + |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. @@ -1160,13 +1173,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`_) .. @@ -1178,31 +1193,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 **py-web-tool** 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. @@ -1221,19 +1234,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 @@ -1247,10 +1260,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()``. @@ -1258,39 +1271,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) -> "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: @@ -1298,7 +1311,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'``. @@ -1309,7 +1323,24 @@ 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 @@ -1317,10 +1348,10 @@ The ``codeBlock()`` method to indent each line written. class Emitter: """Emit an output file; handling indentation context.""" code\_indent = 0 # Used by a Tangler + filePath : Path theFile: TextIO def \_\_init\_\_(self) -> None: - self.fileName = "" self.logger = logging.getLogger(self.\_\_class\_\_.\_\_qualname\_\_) self.log\_indent = logging.getLogger("indent." + self.\_\_class\_\_.\_\_qualname\_\_) # Summary @@ -1331,21 +1362,21 @@ The ``codeBlock()`` method to indent each line written. self.lastIndent = 0 self.fragment = False self.context: list[int] = [] - self.readdIndent(self.code\_indent) # Create context and initial lastIndent values + self.readdIndent(self.code\_indent) # Create context and initial lastIndent values 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. @@ -1363,20 +1394,20 @@ 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: str) -> "Emitter": + def open(self, aPath: Path) -> "Emitter": """Open a file.""" - self.fileName = aFile + self.filePath = aPath self.linesWritten = 0 - self.doOpen(aFile) + self.doOpen(aPath) return self - |srarr|\ Emitter doOpen, to be overridden by subclasses (`5`_) + |srarr|\ Emitter doOpen, to be overridden by subclasses (`6`_) def close(self) -> None: self.codeFinish() # Trailing newline for tangler only. @@ -1384,27 +1415,27 @@ characters to the file. self.totalFiles += 1 self.totalLines += self.linesWritten - |srarr|\ Emitter doClose, to be overridden by subclasses (`6`_) + |srarr|\ Emitter doClose, to be overridden by subclasses (`7`_) def write(self, text: str) -> None: if text is None: return - self.linesWritten += text.count('\\n') 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 - .. .. 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()`` @@ -1412,39 +1443,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: str) -> None: - self.logger.debug("creating {!r}".format(self.fileName)) + def doOpen(self, aFile: Path) -> 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) -> None: - self.logger.debug( "wrote {:d} lines to {!s}".format( self.linesWritten, self.fileName)) + 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 @@ -1495,42 +1526,42 @@ 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: str) -> None: - """Indented write of a block of code. We buffer - The spaces from the last line to act as the indent for the next line. + """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(f"Fragment: {self.lastIndent}, {lines[0]!r}") - self.write('{!s}{!s}'.format(self.lastIndent\*' ', lines[0])) + 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(f"First Line: {self.lastIndent}, {first[0]!r}") - self.write('{!s}{!s}\\n'.format(self.lastIndent\*' ', first[0])) + 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(f"Next Line: {indent}, {l!r}") - 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]: # Last line is non-empty. - self.logger.debug(f"Last (Partial) Line: {indent}, {rest[-1]!r}") - self.write('{!s}{!s}'.format(indent\*' ', rest[-1])) + 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(f"Last (Empty) Line: indent is {len(rest[-1]) + indent}") - # Buffer a next indent + 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 @@ -1539,7 +1570,7 @@ a NamedChunk. It's not really a general feature of emitters or even tanglers. .. 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. @@ -1553,8 +1584,8 @@ 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 @@ -1575,14 +1606,14 @@ 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 @@ -1596,7 +1627,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 @@ -1627,8 +1658,8 @@ 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 @@ -1636,28 +1667,31 @@ requires this. ``readdIndent()`` uses this initial offset for weaving. def addIndent(self, increment: int) -> None: self.lastIndent = self.context[-1]+increment self.context.append(self.lastIndent) - self.log\_indent.debug("addIndent {!s}: {!r}".format(increment, self.context)) + 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 {!s}: {!r}".format(indent, self.context)) + 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)) + 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 {!s}: {!r}".format(indent, self.context)) + 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 @@ -1691,7 +1725,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 @@ -1707,33 +1741,37 @@ 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 .. .. class:: small - |loz| *Imports (11)*. Used by: pyweb.py (`156`_) + |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): """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 @@ -1744,24 +1782,24 @@ Instance-level configuration values: def \_\_init\_\_(self) -> None: super().\_\_init\_\_() - |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 @@ -1775,24 +1813,27 @@ 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: str) -> None: - self.fileName = basename + self.extension - self.logger.info("Weaving {!r}".format(self.fileName)) - self.theFile = open(self.fileName, "w") + def doOpen(self, basename: Path) -> None: + self.filePath = basename.with\_suffix(self.extension) + self.logger.info("Weaving %r", 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)) + 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)) + self.log\_indent.debug("addIndent %d: %r", self.lastIndent, self.context) + def codeFinish(self) -> None: pass # Not needed when weaving @@ -1801,38 +1842,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 + # Prevent some RST markup from being recognized (and processed) in code. quoted\_chars: list[tuple[str, str]] = [ - # prevent some RST markup from being recognized - ('\\\\',r'\\\\'), # Must be first. - ('\`',r'\\\`'), - ('\_',r'\\\_'), - ('\*',r'\\\*'), - ('\|',r'\\\|'), + ('\\\\', 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. @@ -1845,14 +1887,15 @@ 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: Chunk) -> None: pass + def docEnd(self, aChunk: Chunk) -> None: pass @@ -1861,7 +1904,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. @@ -1873,8 +1916,8 @@ 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 @@ -1882,6 +1925,7 @@ Each code chunk includes the places where the chunk is referenced. 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: @@ -1897,7 +1941,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`_) @@ -1911,8 +1955,8 @@ 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 @@ -1921,10 +1965,10 @@ refer to this chunk can be emitted. 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) @@ -1944,7 +1988,7 @@ 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`_) The ``fileBegin()`` method emits the necessary material prior to @@ -1959,8 +2003,8 @@ 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 @@ -1969,10 +2013,10 @@ list is always empty. 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 "+=", # RST Separator + seq=aChunk.seq, + lineNumber=aChunk.lineNumber, + fullName=aChunk.fullName, + concat="=" if aChunk.initial else "+=", ) self.write(txt) @@ -1981,10 +2025,10 @@ list is always empty. 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 = [] ) + seq=aChunk.seq, + lineNumber=aChunk.lineNumber, + fullName=aChunk.fullName, + references=[]) self.write(txt) @@ -1992,7 +2036,7 @@ list is always empty. .. 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 @@ -2008,8 +2052,8 @@ 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 @@ -2036,7 +2080,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 @@ -2063,8 +2107,8 @@ 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) = +.. _`21`: +.. rubric:: Weaver cross reference output methods (21) = .. parsed-literal:: :class: code @@ -2094,14 +2138,14 @@ to change the look of the final woven document. .. 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 @@ -2125,18 +2169,19 @@ Cross-reference definition line .. 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 @@ -2148,7 +2193,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`_) @@ -2164,7 +2209,7 @@ given to the ``weave()`` method of the Web. .. parsed-literal:: w = Web() - WebReader().load(w,"somefile.w") + WebReader().load(w, "somefile.w") weave_latex = LaTeX() w.weave(weave_latex) @@ -2180,34 +2225,34 @@ 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): """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""" - |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 @@ -2218,25 +2263,28 @@ 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`` -- we're trying to avoid an indent when weaving. -.. _`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}} + 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 + \\\\begin{Verbatim}[commandchars=\\\\\\\\\\\\{\\\\},codes={\\\\catcode\`$$=3\\\\catcode\`^=7},frame=single]\\n""" + ) .. .. class:: small - |loz| *LaTeX code chunk begin (24)*. Used by: LaTeX subclass... (`23`_) + |loz| *LaTeX code chunk begin (25)*. Used by: LaTeX subclass... (`24`_) @@ -2247,8 +2295,8 @@ 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 @@ -2256,14 +2304,14 @@ indentation. ce\_template = string.Template(""" \\\\end{Verbatim} ${references} - \\\\end{flushleft}\\n""") # Prevent indentation + \\\\end{flushleft}\\n""") .. .. class:: small - |loz| *LaTeX code chunk end (25)*. Used by: LaTeX subclass... (`23`_) + |loz| *LaTeX code chunk end (26)*. Used by: LaTeX subclass... (`24`_) @@ -2273,8 +2321,8 @@ start of a code chunk. -.. _`26`: -.. rubric:: LaTeX file output begin (26) = +.. _`27`: +.. rubric:: LaTeX file output begin (27) = .. parsed-literal:: :class: code @@ -2286,7 +2334,7 @@ start of a code chunk. .. 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 @@ -2295,8 +2343,8 @@ 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 @@ -2308,7 +2356,7 @@ invokes this chunk, and restores normal indentation. .. 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 @@ -2317,15 +2365,16 @@ 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) = +.. _`29`: +.. rubric:: LaTeX references summary at the end of a chunk (29) = .. parsed-literal:: :class: code - ref\_item\_template = string.Template( """ + ref\_item\_template = string.Template(""" \\\\item Code example ${fullName} (${seq}) (Sect. \\\\ref{pyweb${seq}}, p. \\\\pageref{pyweb${seq}})\\n""") - ref\_template = string.Template( """ + + ref\_template = string.Template(""" \\\\footnotesize Used by: \\\\begin{list}{}{} @@ -2338,7 +2387,7 @@ referring block appears. .. 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 @@ -2349,16 +2398,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: list[tuple[str, str]] = [ - ("\\\\end{Verbatim}", "\\\\end\\,{Verbatim}"), # Allow \\end{Verbatim} - ("\\\\{","\\\\\\,{"), # Prevent unexpected commands in Verbatim - ("$","\\\\$"), # Prevent unexpected math in Verbatim + ("\\\\end{Verbatim}", "\\\\end\\,{Verbatim}"), # Allow \\end{Verbatim} in a Verbatim context + ("\\\\{", "\\\\\\,{"), # Prevent unexpected commands in Verbatim + ("$", "\\\\$"), # Prevent unexpected math in Verbatim ] @@ -2366,7 +2415,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 @@ -2375,13 +2424,14 @@ 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})""") @@ -2389,14 +2439,12 @@ the current line of code. .. 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. @@ -2415,7 +2463,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. @@ -2426,8 +2473,8 @@ 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 @@ -2437,40 +2484,41 @@ with abbreviated (no name) cross references at the end of the chunk. 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`_) + + |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): """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 @@ -2478,8 +2526,8 @@ 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 @@ -2495,7 +2543,7 @@ and HTML tags necessary to set the code off visually. .. 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 @@ -2503,8 +2551,8 @@ 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 @@ -2520,15 +2568,15 @@ write the list of chunks that reference this chunk. .. 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 @@ -2543,7 +2591,7 @@ and HTML tags necessary to set the code off visually. .. 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 @@ -2551,8 +2599,8 @@ 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 @@ -2567,7 +2615,7 @@ write the list of chunks that reference this chunk. .. 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. @@ -2575,21 +2623,22 @@ 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})') - 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. @@ -2597,14 +2646,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: list[tuple[str, str]] = [ - ("&", "&"), # Must be first + ("&", "&"), # Must be first ("<", "<"), (">", ">"), ('"', """), @@ -2615,7 +2664,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 @@ -2623,13 +2672,14 @@ 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})') + refto\_seq\_template = string.Template('(${seq})') @@ -2637,7 +2687,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 @@ -2650,8 +2700,8 @@ 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 @@ -2659,14 +2709,15 @@ The ``xrefLine()`` method writes a line for the file or macro cross reference bl 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`_) + + |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 @@ -2675,13 +2726,14 @@ 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}') @@ -2689,7 +2741,7 @@ is included in the correct order with the other instances, but is bold and marke .. 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 @@ -2699,8 +2751,8 @@ 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 @@ -2712,7 +2764,7 @@ transitive references. .. 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 @@ -2753,8 +2805,8 @@ 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 @@ -2766,16 +2818,17 @@ There are three configurable values: self.comment\_start: str = "#" self.comment\_end: str = "" self.include\_line\_numbers = False - |srarr|\ Tangler doOpen, and doClose overrides (`44`_) - |srarr|\ Tangler code chunk begin (`45`_) - |srarr|\ Tangler code chunk end (`46`_) + + |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. @@ -2788,36 +2841,30 @@ 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) -> None: - 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: str) -> None: - self.fileName = aFile + self.filePath.parent.mkdir(parents=True, exist\_ok=True) + + def doOpen(self, aFile: Path) -> None: + self.filePath = aFile self.checkPath() - self.theFile = open(aFile, "w") - self.logger.info("Tangling {!r}".format(aFile)) + self.theFile = self.filePath.open("w") + self.logger.info("Tangling %r", aFile) 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. @@ -2825,26 +2872,19 @@ 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: Chunk) -> None: - self.log\_indent.debug(" None: - self.log\_indent.debug(">{!s}".format(aChunk.fullName)) + 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 @@ -2903,8 +2943,8 @@ This subclass of ``Tangler`` changes how files are opened and closed. -.. _`47`: -.. rubric:: Imports (47) += +.. _`48`: +.. rubric:: Imports (48) += .. parsed-literal:: :class: code @@ -2916,12 +2956,12 @@ are opened and closed. .. class:: small - |loz| *Imports (47)*. Used by: pyweb.py (`156`_) + |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 @@ -2932,16 +2972,16 @@ are opened and closed. def \_\_init\_\_(self, \*args: Any) -> None: super().\_\_init\_\_(\*args) - |srarr|\ TanglerMake doOpen override, using a temporary file (`49`_) + |srarr|\ TanglerMake doOpen override, using a temporary file (`50`_) - |srarr|\ TanglerMake doClose override, comparing temporary to original (`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 @@ -2951,23 +2991,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: str) -> None: + def doOpen(self, aFile: Path) -> None: fd, self.tempname = tempfile.mkstemp(dir=os.curdir) self.theFile = os.fdopen(fd, "w") - self.logger.info("Tangling {!r}".format(aFile)) + self.logger.info("Tangling %r", aFile) .. .. 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. @@ -2980,8 +3020,8 @@ 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 @@ -2989,28 +3029,29 @@ and time) if nothing has changed. 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)) + self.logger.info("No change to %r", 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) # type: ignore [attr-defined] + os.remove(self.tempname) + self.logger.info("Wrote %e 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 @@ -3031,22 +3072,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 (`64`_), |srarr|\ (`69`_) - |srarr|\ OutputChunk class (`70`_) - |srarr|\ NamedDocumentChunk class (`74`_) + |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 @@ -3104,7 +3148,7 @@ the identifier. 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) ) + pattern = re.compile(f'\\W{i!s}\\W' ) for c in *the Web's named chunk list*: c.searchForRE(pattern) @@ -3174,7 +3218,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: @@ -3191,8 +3235,8 @@ 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 @@ -3202,6 +3246,7 @@ The ``Chunk`` constructor initializes the following instance variables: 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 @@ -3209,7 +3254,6 @@ The ``Chunk`` constructor initializes the following instance variables: self.name: str = '' self.fullName: str = "" self.seq: int = 0 - self.fileName = '' 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 @@ -3217,34 +3261,34 @@ The ``Chunk`` constructor initializes the following instance variables: def \_\_str\_\_(self) -> str: return "\\n".join(map(str, self.commands)) def \_\_repr\_\_(self) -> str: - return "{!s}('{!s}')".format(self.\_\_class\_\_.\_\_name\_\_, self.name) + return f"{self.\_\_class\_\_.\_\_name\_\_!s}({self.name!r})" - |srarr|\ Chunk append a command (`53`_) - |srarr|\ Chunk append text (`54`_) - |srarr|\ Chunk add to the web (`55`_) + |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 (`59`_) - |srarr|\ Chunk superclass make Content definition (`56`_) - |srarr|\ Chunk examination: starts with, matches pattern (`58`_) - |srarr|\ Chunk references to this Chunk (`60`_) + |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 (`61`_) - |srarr|\ Chunk tangle this Chunk into a code file (`62`_) - |srarr|\ Chunk indent adjustments (`63`_) + |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 @@ -3259,7 +3303,7 @@ The ``append()`` method simply appends a ``Command`` instance to this chunk. .. 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, @@ -3273,8 +3317,8 @@ 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 @@ -3296,7 +3340,7 @@ be a separate ``TextCommand`` because it will wind up indented. .. 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. @@ -3306,8 +3350,8 @@ 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 @@ -3321,7 +3365,7 @@ of the ``Web`` class to append an anonymous, unindexed chunk. .. 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. @@ -3332,8 +3376,8 @@ 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 @@ -3346,7 +3390,7 @@ A Named Chunk using ``@[`` and ``@]`` creates text. .. 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 @@ -3368,8 +3412,8 @@ 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:: Imports (57) += +.. _`58`: +.. rubric:: Imports (58) += .. parsed-literal:: :class: code @@ -3379,12 +3423,12 @@ and accumulated as part of a cross reference for this ``Chunk``. .. class:: small - |loz| *Imports (57)*. Used by: pyweb.py (`156`_) + |loz| *Imports (58)*. Used by: pyweb.py (`155`_) -.. _`58`: -.. rubric:: Chunk examination: starts with, matches pattern (58) = +.. _`59`: +.. rubric:: Chunk examination: starts with, matches pattern (59) = .. parsed-literal:: :class: code @@ -3418,7 +3462,7 @@ and accumulated as part of a cross reference for this ``Chunk``. .. class:: small - |loz| *Chunk examination: starts with, matches pattern (58)*. 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``. @@ -3434,8 +3478,8 @@ context information. -.. _`59`: -.. rubric:: Chunk generate references from this Chunk (59) = +.. _`60`: +.. rubric:: Chunk generate references from this Chunk (60) = .. parsed-literal:: :class: code @@ -3455,7 +3499,7 @@ context information. .. class:: small - |loz| *Chunk generate references from this Chunk (59)*. 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 @@ -3466,8 +3510,8 @@ 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``. -.. _`60`: -.. rubric:: Chunk references to this Chunk (60) = +.. _`61`: +.. rubric:: Chunk references to this Chunk (61) = .. parsed-literal:: :class: code @@ -3483,7 +3527,7 @@ The Weaver pushed it into the Web so that it is available for each ``Chunk``. .. class:: small - |loz| *Chunk references to this Chunk (60)*. 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: @@ -3502,8 +3546,8 @@ context information. -.. _`61`: -.. rubric:: Chunk weave this Chunk into the documentation (61) = +.. _`62`: +.. rubric:: Chunk weave this Chunk into the documentation (62) = .. parsed-literal:: :class: code @@ -3526,15 +3570,15 @@ context information. .. class:: small - |loz| *Chunk weave this Chunk into the documentation (61)*. 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. -.. _`62`: -.. rubric:: Chunk tangle this Chunk into a code file (62) = +.. _`63`: +.. rubric:: Chunk tangle this Chunk into a code file (63) = .. parsed-literal:: :class: code @@ -3548,7 +3592,7 @@ problem with this program or the input file. .. class:: small - |loz| *Chunk tangle this Chunk into a code file (62)*. 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 @@ -3557,8 +3601,8 @@ a subclass may not indent when tangling and may -- instead -- put stuff flush at left margin by forcing the local indent to zero. -.. _`63`: -.. rubric:: Chunk indent adjustments (63) = +.. _`64`: +.. rubric:: Chunk indent adjustments (64) = .. parsed-literal:: :class: code @@ -3573,7 +3617,7 @@ left margin by forcing the local indent to zero. .. class:: small - |loz| *Chunk indent adjustments (63)*. Used by: Chunk class (`52`_) + |loz| *Chunk indent adjustments (64)*. Used by: Chunk base class... (`53`_) NamedChunk class @@ -3626,8 +3670,8 @@ This class introduces some additional attributes. -.. _`64`: -.. rubric:: NamedChunk class (64) = +.. _`65`: +.. rubric:: NamedChunk class for defined names (65) = .. parsed-literal:: :class: code @@ -3641,22 +3685,22 @@ This class introduces some additional attributes. self.refCount = 0 def \_\_str\_\_(self) -> str: - return "{!r}: {!s}".format(self.name, Chunk.\_\_str\_\_(self)) + 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 (`65`_) - |srarr|\ NamedChunk add to the web (`66`_) - |srarr|\ NamedChunk weave into the documentation (`67`_) - |srarr|\ NamedChunk tangle into the source file (`68`_) + |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 (64)*. 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 @@ -3664,8 +3708,8 @@ associated with this chunk. These are provided after the ``@|`` separator in a ``@d`` named chunk. These are used by the ``@u`` cross reference generator. -.. _`65`: -.. rubric:: NamedChunk user identifiers set and get (65) = +.. _`66`: +.. rubric:: NamedChunk user identifiers set and get (66) = .. parsed-literal:: :class: code @@ -3681,7 +3725,7 @@ in a ``@d`` named chunk. These are used by the ``@u`` cross reference generator .. class:: small - |loz| *NamedChunk user identifiers set and get (65)*. Used by: NamedChunk class (`64`_) + |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. @@ -3690,8 +3734,8 @@ Each class of ``Chunk`` must override this to be sure that the various of the ``Web`` class to append a named chunk. -.. _`66`: -.. rubric:: NamedChunk add to the web (66) = +.. _`67`: +.. rubric:: NamedChunk add to the web (67) = .. parsed-literal:: :class: code @@ -3705,7 +3749,7 @@ of the ``Web`` class to append a named chunk. .. class:: small - |loz| *NamedChunk add to the web (66)*. Used by: NamedChunk class (`64`_) + |loz| *NamedChunk add to the web (67)*. Used by: NamedChunk class... (`65`_) The ``weave()`` method weaves this chunk into the final document as follows: @@ -3732,8 +3776,8 @@ The woven references simply follow whatever preceded them on the line; the inden -.. _`67`: -.. rubric:: NamedChunk weave into the documentation (67) = +.. _`68`: +.. rubric:: NamedChunk weave into the documentation (68) = .. parsed-literal:: :class: code @@ -3762,7 +3806,7 @@ The woven references simply follow whatever preceded them on the line; the inden .. class:: small - |loz| *NamedChunk weave into the documentation (67)*. Used by: NamedChunk class (`64`_) + |loz| *NamedChunk weave into the documentation (68)*. Used by: NamedChunk class... (`65`_) The ``tangle()`` method tangles this chunk into the final document as follows: @@ -3779,8 +3823,8 @@ context information. -.. _`68`: -.. rubric:: NamedChunk tangle into the source file (68) = +.. _`69`: +.. rubric:: NamedChunk tangle into the source file (69) = .. parsed-literal:: :class: code @@ -3805,15 +3849,15 @@ context information. .. class:: small - |loz| *NamedChunk tangle into the source file (68)*. Used by: NamedChunk class (`64`_) + |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. -.. _`69`: -.. rubric:: NamedChunk class (69) += +.. _`70`: +.. rubric:: NamedChunk class for defined names (70) += .. parsed-literal:: :class: code @@ -3830,7 +3874,7 @@ context. It simply sets an indent at the left margin. .. class:: small - |loz| *NamedChunk class (69)*. Used by: Chunk class hierarchy... (`51`_) + |loz| *NamedChunk class for defined names (70)*. Used by: Chunk class hierarchy... (`52`_) OutputChunk class @@ -3852,8 +3896,8 @@ use different ``Weaver`` methods for different kinds of text. All other methods, including the tangle method are identical to ``NamedChunk``. -.. _`70`: -.. rubric:: OutputChunk class (70) = +.. _`71`: +.. rubric:: OutputChunk class (71) = .. parsed-literal:: :class: code @@ -3864,16 +3908,16 @@ All other methods, including the tangle method are identical to ``NamedChunk``. super().\_\_init\_\_(name) self.comment\_start = comment\_start self.comment\_end = comment\_end - |srarr|\ OutputChunk add to the web (`71`_) - |srarr|\ OutputChunk weave (`72`_) - |srarr|\ OutputChunk tangle (`73`_) + |srarr|\ OutputChunk add to the web (`72`_) + |srarr|\ OutputChunk weave (`73`_) + |srarr|\ OutputChunk tangle (`74`_) .. .. class:: small - |loz| *OutputChunk class (70)*. 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``. @@ -3882,8 +3926,8 @@ Each class of ``Chunk`` must override this to be sure that the various of the ``Web`` class to append a file output chunk. -.. _`71`: -.. rubric:: OutputChunk add to the web (71) = +.. _`72`: +.. rubric:: OutputChunk add to the web (72) = .. parsed-literal:: :class: code @@ -3897,7 +3941,7 @@ of the ``Web`` class to append a file output chunk. .. class:: small - |loz| *OutputChunk add to the web (71)*. Used by: OutputChunk class (`70`_) + |loz| *OutputChunk add to the web (72)*. Used by: OutputChunk class... (`71`_) The ``weave()`` method weaves this chunk into the final document as follows: @@ -3917,8 +3961,8 @@ context information. -.. _`72`: -.. rubric:: OutputChunk weave (72) = +.. _`73`: +.. rubric:: OutputChunk weave (73) = .. parsed-literal:: :class: code @@ -3936,15 +3980,15 @@ context information. .. class:: small - |loz| *OutputChunk weave (72)*. Used by: OutputChunk class (`70`_) + |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. -.. _`73`: -.. rubric:: OutputChunk tangle (73) = +.. _`74`: +.. rubric:: OutputChunk tangle (74) = .. parsed-literal:: :class: code @@ -3958,7 +4002,7 @@ to be sure that -- if line numbers were requested -- they can be included proper .. class:: small - |loz| *OutputChunk tangle (73)*. Used by: OutputChunk class (`70`_) + |loz| *OutputChunk tangle (74)*. Used by: OutputChunk class... (`71`_) NamedDocumentChunk class @@ -3982,8 +4026,8 @@ All other methods, including the tangle method are identical to ``NamedChunk``. -.. _`74`: -.. rubric:: NamedDocumentChunk class (74) = +.. _`75`: +.. rubric:: NamedDocumentChunk class (75) = .. parsed-literal:: :class: code @@ -3994,15 +4038,15 @@ All other methods, including the tangle method are identical to ``NamedChunk``. def makeContent(self, text: str, lineNumber: int = 0) -> Command: return TextCommand(text, lineNumber) - |srarr|\ NamedDocumentChunk weave (`75`_) - |srarr|\ NamedDocumentChunk tangle (`76`_) + |srarr|\ NamedDocumentChunk weave (`76`_) + |srarr|\ NamedDocumentChunk tangle (`77`_) .. .. class:: small - |loz| *NamedDocumentChunk class (74)*. 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. @@ -4018,8 +4062,8 @@ to insert the entire chunk. -.. _`75`: -.. rubric:: NamedDocumentChunk weave (75) = +.. _`76`: +.. rubric:: NamedDocumentChunk weave (76) = .. parsed-literal:: :class: code @@ -4040,12 +4084,12 @@ to insert the entire chunk. .. class:: small - |loz| *NamedDocumentChunk weave (75)*. Used by: NamedDocumentChunk class (`74`_) + |loz| *NamedDocumentChunk weave (76)*. Used by: NamedDocumentChunk class... (`75`_) -.. _`76`: -.. rubric:: NamedDocumentChunk tangle (76) = +.. _`77`: +.. rubric:: NamedDocumentChunk tangle (77) = .. parsed-literal:: :class: code @@ -4059,7 +4103,7 @@ to insert the entire chunk. .. class:: small - |loz| *NamedDocumentChunk tangle (76)*. Used by: NamedDocumentChunk class (`74`_) + |loz| *NamedDocumentChunk tangle (77)*. Used by: NamedDocumentChunk class... (`75`_) Commands @@ -4081,26 +4125,33 @@ cross reference information and tangle a file or weave the final document. -.. _`77`: -.. rubric:: Command class hierarchy - used to describe individual commands (77) = +.. _`78`: +.. rubric:: Command class hierarchy - used to describe individual commands (78) = .. parsed-literal:: :class: code - |srarr|\ Command superclass (`78`_) - |srarr|\ TextCommand class to contain a document text block (`81`_) - |srarr|\ CodeCommand class to contain a program source code block (`82`_) - |srarr|\ XrefCommand superclass for all cross-reference commands (`83`_) - |srarr|\ FileXrefCommand class for an output file cross-reference (`84`_) - |srarr|\ MacroXrefCommand class for a named chunk cross-reference (`85`_) - |srarr|\ UserIdXrefCommand class for a user identifier cross-reference (`86`_) - |srarr|\ ReferenceCommand class for chunk references (`87`_) + |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 (77)*. Used by: Base Class Definitions (`1`_) + |loz| *Command class hierarchy - used to describe individual commands (78)*. Used by: Base Class Definitions (`1`_) Command Superclass @@ -4164,13 +4215,13 @@ The attributes of a ``Command`` instance includes the line number on which the command began, in ``lineNumber``. -.. _`78`: -.. rubric:: Command superclass (78) = +.. _`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.""" chunk : "Chunk" text : str @@ -4179,22 +4230,22 @@ the command began, in ``lineNumber``. self.logger = logging.getLogger(self.\_\_class\_\_.\_\_qualname\_\_) def \_\_str\_\_(self) -> str: - return "at {!r}".format(self.lineNumber) + return f"at {self.lineNumber!r}" - |srarr|\ Command analysis features: starts-with and Regular Expression search (`79`_) - |srarr|\ Command tangle and weave functions (`80`_) + |srarr|\ Command analysis features: starts-with and Regular Expression search (`80`_) + |srarr|\ Command tangle and weave functions (`81`_) .. .. class:: small - |loz| *Command superclass (78)*. Used by: Command class hierarchy... (`77`_) + |loz| *Command superclass (79)*. Used by: Command class hierarchy... (`78`_) -.. _`79`: -.. rubric:: Command analysis features: starts-with and Regular Expression search (79) = +.. _`80`: +.. rubric:: Command analysis features: starts-with and Regular Expression search (80) = .. parsed-literal:: :class: code @@ -4211,29 +4262,33 @@ the command began, in ``lineNumber``. .. class:: small - |loz| *Command analysis features: starts-with and Regular Expression search (79)*. Used by: Command superclass (`78`_) + |loz| *Command analysis features: starts-with and Regular Expression search (80)*. Used by: Command superclass (`79`_) -.. _`80`: -.. rubric:: Command tangle and weave functions (80) = +.. _`81`: +.. rubric:: Command tangle and weave functions (81) = .. parsed-literal:: :class: code def ref(self, aWeb: "Web") -> str \| None: return None + + @abc.abstractmethod def weave(self, aWeb: "Web", aWeaver: "Weaver") -> None: - pass + ... + + @abc.abstractmethod def tangle(self, aWeb: "Web", aTangler: "Tangler") -> None: - pass + ... .. .. class:: small - |loz| *Command tangle and weave functions (80)*. Used by: Command superclass (`78`_) + |loz| *Command tangle and weave functions (81)*. Used by: Command superclass (`79`_) TextCommand class @@ -4253,10 +4308,11 @@ 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 to snip off first 32 chars of the text. -.. _`81`: -.. rubric:: TextCommand class to contain a document text block (81) = +.. _`82`: +.. rubric:: TextCommand class to contain a document text block (82) = .. parsed-literal:: :class: code @@ -4267,7 +4323,7 @@ or tangler. super().\_\_init\_\_(fromLine) self.text = text def \_\_str\_\_(self) -> str: - return "at {!r}: {!r}...".format(self.lineNumber,self.text[:32]) + 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: @@ -4290,7 +4346,7 @@ or tangler. .. class:: small - |loz| *TextCommand class to contain a document text block (81)*. Used by: Command class hierarchy... (`77`_) + |loz| *TextCommand class to contain a document text block (82)*. Used by: Command class hierarchy... (`78`_) CodeCommand class @@ -4314,8 +4370,8 @@ indentation is maintained. -.. _`82`: -.. rubric:: CodeCommand class to contain a program source code block (82) = +.. _`83`: +.. rubric:: CodeCommand class to contain a program source code block (83) = .. parsed-literal:: :class: code @@ -4332,7 +4388,7 @@ indentation is maintained. .. class:: small - |loz| *CodeCommand class to contain a program source code block (82)*. Used by: Command class hierarchy... (`77`_) + |loz| *CodeCommand class to contain a program source code block (83)*. Used by: Command class hierarchy... (`78`_) XrefCommand superclass @@ -4361,8 +4417,8 @@ is illegal. An exception is raised and processing stops. -.. _`83`: -.. rubric:: XrefCommand superclass for all cross-reference commands (83) = +.. _`84`: +.. rubric:: XrefCommand superclass for all cross-reference commands (84) = .. parsed-literal:: :class: code @@ -4370,7 +4426,7 @@ is illegal. An exception is raised and processing stops. class XrefCommand(Command): """Any of the Xref-goes-here commands in the input.""" def \_\_str\_\_(self) -> str: - return "at {!r}: cross reference".format(self.lineNumber) + return f"at {self.lineNumber!r}: cross reference" def formatXref(self, xref: dict[str, list[int]], aWeaver: "Weaver") -> None: aWeaver.xrefHead() @@ -4386,7 +4442,7 @@ is illegal. An exception is raised and processing stops. .. class:: small - |loz| *XrefCommand superclass for all cross-reference commands (83)*. Used by: Command class hierarchy... (`77`_) + |loz| *XrefCommand superclass for all cross-reference commands (84)*. Used by: Command class hierarchy... (`78`_) FileXrefCommand class @@ -4402,8 +4458,8 @@ the ``formatXref()`` method of the ``XrefCommand`` superclass for format this r -.. _`84`: -.. rubric:: FileXrefCommand class for an output file cross-reference (84) = +.. _`85`: +.. rubric:: FileXrefCommand class for an output file cross-reference (85) = .. parsed-literal:: :class: code @@ -4419,7 +4475,7 @@ the ``formatXref()`` method of the ``XrefCommand`` superclass for format this r .. class:: small - |loz| *FileXrefCommand class for an output file cross-reference (84)*. Used by: Command class hierarchy... (`77`_) + |loz| *FileXrefCommand class for an output file cross-reference (85)*. Used by: Command class hierarchy... (`78`_) MacroXrefCommand class @@ -4435,8 +4491,8 @@ the ``formatXref()`` method of the ``XrefCommand`` superclass method for format -.. _`85`: -.. rubric:: MacroXrefCommand class for a named chunk cross-reference (85) = +.. _`86`: +.. rubric:: MacroXrefCommand class for a named chunk cross-reference (86) = .. parsed-literal:: :class: code @@ -4452,7 +4508,7 @@ the ``formatXref()`` method of the ``XrefCommand`` superclass method for format .. class:: small - |loz| *MacroXrefCommand class for a named chunk cross-reference (85)*. Used by: Command class hierarchy... (`77`_) + |loz| *MacroXrefCommand class for a named chunk cross-reference (86)*. Used by: Command class hierarchy... (`78`_) UserIdXrefCommand class @@ -4477,8 +4533,8 @@ algorithm, which is similar to the algorithm in the ``XrefCommand`` superclass. -.. _`86`: -.. rubric:: UserIdXrefCommand class for a user identifier cross-reference (86) = +.. _`87`: +.. rubric:: UserIdXrefCommand class for a user identifier cross-reference (87) = .. parsed-literal:: :class: code @@ -4502,7 +4558,7 @@ algorithm, which is similar to the algorithm in the ``XrefCommand`` superclass. .. class:: small - |loz| *UserIdXrefCommand class for a user identifier cross-reference (86)*. Used by: Command class hierarchy... (`77`_) + |loz| *UserIdXrefCommand class for a user identifier cross-reference (87)*. Used by: Command class hierarchy... (`78`_) ReferenceCommand class @@ -4534,8 +4590,8 @@ of a ``ReferenceCommand``. -.. _`87`: -.. rubric:: ReferenceCommand class for chunk references (87) = +.. _`88`: +.. rubric:: ReferenceCommand class for chunk references (88) = .. parsed-literal:: :class: code @@ -4550,19 +4606,19 @@ of a ``ReferenceCommand``. self.chunkList: list[Chunk] = [] def \_\_str\_\_(self) -> str: - return "at {!r}: reference to chunk {!r}".format(self.lineNumber,self.refTo) + return "at {self.lineNumber!r}: reference to chunk {self.refTo!r}" - |srarr|\ ReferenceCommand resolve a referenced chunk name (`88`_) - |srarr|\ ReferenceCommand refers to a chunk (`89`_) - |srarr|\ ReferenceCommand weave a reference to a chunk (`90`_) - |srarr|\ ReferenceCommand tangle a referenced chunk (`91`_) + |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 (87)*. Used by: Command class hierarchy... (`77`_) + |loz| *ReferenceCommand class for chunk references (88)*. Used by: Command class hierarchy... (`78`_) The ``resolve()`` method queries the overall ``Web`` instance for the full @@ -4572,8 +4628,8 @@ to the chunk. -.. _`88`: -.. rubric:: ReferenceCommand resolve a referenced chunk name (88) = +.. _`89`: +.. rubric:: ReferenceCommand resolve a referenced chunk name (89) = .. parsed-literal:: :class: code @@ -4588,7 +4644,7 @@ to the chunk. .. class:: small - |loz| *ReferenceCommand resolve a referenced chunk name (88)*. Used by: ReferenceCommand class... (`87`_) + |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``; @@ -4598,8 +4654,8 @@ Chinks to which it refers. -.. _`89`: -.. rubric:: ReferenceCommand refers to a chunk (89) = +.. _`90`: +.. rubric:: ReferenceCommand refers to a chunk (90) = .. parsed-literal:: :class: code @@ -4614,7 +4670,7 @@ Chinks to which it refers. .. class:: small - |loz| *ReferenceCommand refers to a chunk (89)*. Used by: ReferenceCommand class... (`87`_) + |loz| *ReferenceCommand refers to a chunk (90)*. Used by: ReferenceCommand class... (`88`_) The ``weave()`` method inserts a markup reference to a named @@ -4623,8 +4679,8 @@ this appropriately for the document type being woven. -.. _`90`: -.. rubric:: ReferenceCommand weave a reference to a chunk (90) = +.. _`91`: +.. rubric:: ReferenceCommand weave a reference to a chunk (91) = .. parsed-literal:: :class: code @@ -4639,7 +4695,7 @@ this appropriately for the document type being woven. .. class:: small - |loz| *ReferenceCommand weave a reference to a chunk (90)*. Used by: ReferenceCommand class... (`87`_) + |loz| *ReferenceCommand weave a reference to a chunk (91)*. Used by: ReferenceCommand class... (`88`_) The ``tangle()`` method inserts the resolved chunk in this @@ -4651,8 +4707,8 @@ Or where indentation is set to a local zero because the included Chunk is a no-indent Chunk. -.. _`91`: -.. rubric:: ReferenceCommand tangle a referenced chunk (91) = +.. _`92`: +.. rubric:: ReferenceCommand tangle a referenced chunk (92) = .. parsed-literal:: :class: code @@ -4661,15 +4717,15 @@ Chunk is a no-indent Chunk. """Create source code.""" self.resolve(aWeb) - self.logger.debug("Indent {!r} + {!r}".format(aTangler.context, 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(f"Tangling {self.fullName!r} with chunks {self.chunkList!r}") + 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("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) @@ -4678,7 +4734,7 @@ Chunk is a no-indent Chunk. .. class:: small - |loz| *ReferenceCommand tangle a referenced chunk (91)*. Used by: ReferenceCommand class... (`87`_) + |loz| *ReferenceCommand tangle a referenced chunk (92)*. Used by: ReferenceCommand class... (`88`_) Reference Strategy @@ -4701,24 +4757,26 @@ this object. -.. _`92`: -.. rubric:: Reference class hierarchy - strategies for references to a chunk (92) = +.. _`93`: +.. rubric:: Reference class hierarchy - strategies for references to a chunk (93) = .. parsed-literal:: :class: code - class Reference: + 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.""" - return [] + ... .. .. 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 (93)*. Used by: Base Class Definitions (`1`_) SimpleReference Class @@ -4728,8 +4786,8 @@ The SimpleReference subclass does the simplest version of resolution. It returns the ``Chunks`` referenced. -.. _`93`: -.. rubric:: Reference class hierarchy - strategies for references to a chunk (93) += +.. _`94`: +.. rubric:: Reference class hierarchy - strategies for references to a chunk (94) += .. parsed-literal:: :class: code @@ -4743,7 +4801,7 @@ the ``Chunks`` referenced. .. 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 (94)*. Used by: Base Class Definitions (`1`_) TransitiveReference Class @@ -4756,8 +4814,8 @@ This requires walking through the ``Web`` to locate "parents" of each referenced ``Chunk``. -.. _`94`: -.. rubric:: Reference class hierarchy - strategies for references to a chunk (94) += +.. _`95`: +.. rubric:: Reference class hierarchy - strategies for references to a chunk (95) += .. parsed-literal:: :class: code @@ -4765,7 +4823,7 @@ This requires walking through the ``Web`` to locate "parents" of each referenced class TransitiveReference(Reference): def chunkReferencedBy(self, aChunk: Chunk) -> list[Chunk]: refBy = aChunk.referencedBy - self.logger.debug("References: {!s}({:d}) {!r}".format(aChunk.name, aChunk.seq, refBy)) + 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. @@ -4774,14 +4832,14 @@ This requires walking through the ``Web`` to locate "parents" of each referenced 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)) + self.logger.debug(f"References: {'--':>{2\*depth}s} {final!s}") return final .. .. class:: small - |loz| *Reference class hierarchy - strategies for references to a chunk (94)*. Used by: Base Class Definitions (`1`_) + |loz| *Reference class hierarchy - strategies for references to a chunk (95)*. Used by: Base Class Definitions (`1`_) @@ -4803,7 +4861,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: @@ -4823,8 +4881,8 @@ but merely creates a distinct class to facilitate writing ``except`` statements. -.. _`95`: -.. rubric:: Error class - defines the errors raised (95) = +.. _`96`: +.. rubric:: Error class - defines the errors raised (96) = .. parsed-literal:: :class: code @@ -4835,7 +4893,7 @@ but merely creates a distinct class to facilitate writing ``except`` statements. .. class:: small - |loz| *Error class - defines the errors raised (95)*. 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 @@ -4870,8 +4928,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. @@ -4899,16 +4957,16 @@ A web instance has a number of attributes. named chunk. -.. _`96`: -.. rubric:: Web class - describes the overall "web" of chunks (96) = +.. _`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, filename: str \| None = None) -> None: - self.webFileName = filename + 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 @@ -4917,21 +4975,21 @@ A web instance has a number of attributes. self.logger = logging.getLogger(self.\_\_class\_\_.\_\_qualname\_\_) def \_\_str\_\_(self) -> str: - return "Web {!r}".format(self.webFileName,) + return f"Web {self.web\_path!r}" - |srarr|\ Web construction methods used by Chunks and WebReader (`98`_) - |srarr|\ Web Chunk name resolution methods (`103`_), |srarr|\ (`104`_) - |srarr|\ Web Chunk cross reference methods (`105`_), |srarr|\ (`107`_), |srarr|\ (`108`_), |srarr|\ (`109`_) - |srarr|\ Web determination of the language from the first chunk (`112`_) - |srarr|\ Web tangle the output files (`113`_) - |srarr|\ Web weave the output document (`114`_) + |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 (96)*. Used by: Base Class Definitions (`1`_) + |loz| *Web class - describes the overall "web" of chunks (97)*. Used by: Base Class Definitions (`1`_) Web Construction @@ -4953,12 +5011,11 @@ to contain a more complete description of the chunk. We include a weakref to the ``Web`` to each ``Chunk``. -.. _`97`: -.. rubric:: Imports (97) += +.. _`98`: +.. rubric:: Imports (98) += .. parsed-literal:: :class: code - import weakref @@ -4966,26 +5023,26 @@ We include a weakref to the ``Web`` to each ``Chunk``. .. class:: small - |loz| *Imports (97)*. Used by: pyweb.py (`156`_) + |loz| *Imports (98)*. Used by: pyweb.py (`155`_) -.. _`98`: -.. rubric:: Web construction methods used by Chunks and WebReader (98) = +.. _`99`: +.. rubric:: Web construction methods used by Chunks and WebReader (99) = .. parsed-literal:: :class: code - |srarr|\ Web add full chunk names, ignoring abbreviated names (`99`_) - |srarr|\ Web add an anonymous chunk (`100`_) - |srarr|\ Web add a named macro chunk (`101`_) - |srarr|\ Web add an output file definition chunk (`102`_) + |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 (98)*. Used by: Web class... (`96`_) + |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 @@ -5028,8 +5085,8 @@ uses an abbreviated name. We would no longer need to return a value from this function, either. -.. _`99`: -.. rubric:: Web add full chunk names, ignoring abbreviated names (99) = +.. _`100`: +.. rubric:: Web add full chunk names, ignoring abbreviated names (100) = .. parsed-literal:: :class: code @@ -5039,11 +5096,11 @@ uses an abbreviated 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.logger.debug("Adding empty chunk %r", name) return nm @@ -5051,7 +5108,7 @@ uses an abbreviated name. .. class:: small - |loz| *Web add full chunk names, ignoring abbreviated names (99)*. Used by: Web construction... (`98`_) + |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 @@ -5059,8 +5116,8 @@ tangling. -.. _`100`: -.. rubric:: Web add an anonymous chunk (100) = +.. _`101`: +.. rubric:: Web add an anonymous chunk (101) = .. parsed-literal:: :class: code @@ -5075,7 +5132,7 @@ tangling. .. class:: small - |loz| *Web add an anonymous chunk (100)*. Used by: Web construction... (`98`_) + |loz| *Web add an anonymous chunk (101)*. Used by: Web construction... (`99`_) A named ``Chunk`` is defined with a ``@d`` command. @@ -5109,8 +5166,8 @@ in the list. Otherwise, it's False. The ``addDefName()`` no longer needs to return a value. -.. _`101`: -.. rubric:: Web add a named macro chunk (101) = +.. _`102`: +.. rubric:: Web add a named macro chunk (102) = .. parsed-literal:: :class: code @@ -5127,16 +5184,16 @@ in the list. Otherwise, it's False. 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)) + 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 (101)*. Used by: Web construction... (`98`_) + |loz| *Web add a named macro chunk (102)*. Used by: Web construction... (`99`_) An output file definition ``Chunk`` is defined with an ``@o`` @@ -5167,8 +5224,8 @@ If the chunk list was empty, this is the first chunk, the -.. _`102`: -.. rubric:: Web add an output file definition chunk (102) = +.. _`103`: +.. rubric:: Web add an output file definition chunk (103) = .. parsed-literal:: :class: code @@ -5179,7 +5236,7 @@ If the chunk list was empty, this is the first chunk, the 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 @@ -5191,7 +5248,7 @@ If the chunk list was empty, this is the first chunk, the .. class:: small - |loz| *Web add an output file definition chunk (102)*. Used by: Web construction... (`98`_) + |loz| *Web add an output file definition chunk (103)*. Used by: Web construction... (`99`_) Web Chunk Name Resolution @@ -5222,30 +5279,37 @@ The ``fullNameFor()`` method resolves full name for a chunk as follows: -.. _`103`: -.. rubric:: Web Chunk name resolution methods (103) = +.. _`104`: +.. rubric:: Web Chunk name resolution methods (104) = .. parsed-literal:: :class: code 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 (103)*. Used by: Web class... (`96`_) + |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 @@ -5259,8 +5323,8 @@ weave and tangle results and keep processing. This would allow an author to catch multiple errors in a single run of **py-web-tool** . -.. _`104`: -.. rubric:: Web Chunk name resolution methods (104) += +.. _`105`: +.. rubric:: Web Chunk name resolution methods (105) += .. parsed-literal:: :class: code @@ -5270,26 +5334,24 @@ catch multiple errors in a single run of **py-web-tool** . 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 (104)*. Used by: Web class... (`96`_) + |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 @@ -5297,7 +5359,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 @@ -5307,13 +5368,12 @@ 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. -.. _`105`: -.. rubric:: Web Chunk cross reference methods (105) = +.. _`106`: +.. rubric:: Web Chunk cross reference methods (106) = .. parsed-literal:: :class: code @@ -5328,14 +5388,15 @@ reference, it also assures that all chunks are used exactly once. for c in self.getchunk(aRefName): c.referencedBy.append(aChunk) c.refCount += 1 - |srarr|\ Web Chunk check reference counts are all one (`106`_) + + |srarr|\ Web Chunk check reference counts are all one (`107`_) .. .. class:: small - |loz| *Web Chunk cross reference methods (105)*. Used by: Web class... (`96`_) + |loz| *Web Chunk cross reference methods (106)*. Used by: Web class... (`97`_) We verify that the reference count for a @@ -5343,39 +5404,39 @@ Chunk is exactly one. We don't gracefully tolerate multiple references to a Chunk or unreferenced chunks. -.. _`106`: -.. rubric:: Web Chunk check reference counts are all one (106) = +.. _`107`: +.. rubric:: Web Chunk check reference counts are all one (107) = .. parsed-literal:: :class: code for nm in self.no\_reference(): - self.logger.warning("No reference to {!r}".format(nm)) + self.logger.warning("No reference to %r", nm) for nm in self.multi\_reference(): - self.logger.warning("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 (106)*. Used by: Web Chunk cross reference methods... (`105`_) + |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.warning("No reference to {!r}".format(nm)) + self.logger.warning("No reference to %r", nm) elif cl[0].refCount > 1: - self.logger.warning("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 @@ -5388,8 +5449,8 @@ is a list of chunks referenced but not defined. -.. _`107`: -.. rubric:: Web Chunk cross reference methods (107) += +.. _`108`: +.. rubric:: Web Chunk cross reference methods (108) += .. parsed-literal:: :class: code @@ -5406,20 +5467,18 @@ is a list of chunks referenced but not defined. .. class:: small - |loz| *Web Chunk cross reference methods (107)*. Used by: Web class... (`96`_) + |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. - -.. _`108`: -.. rubric:: Web Chunk cross reference methods (108) += +.. _`109`: +.. rubric:: Web Chunk cross reference methods (109) += .. parsed-literal:: :class: code @@ -5440,7 +5499,7 @@ but applies it to the ``named`` mapping. .. class:: small - |loz| *Web Chunk cross reference methods (108)*. Used by: Web class... (`96`_) + |loz| *Web Chunk cross reference methods (109)*. Used by: Web class... (`97`_) The ``userNamesXref()`` method creates a mapping for each @@ -5448,7 +5507,6 @@ 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 @@ -5466,8 +5524,8 @@ This works in two passes: -.. _`109`: -.. rubric:: Web Chunk cross reference methods (109) += +.. _`110`: +.. rubric:: Web Chunk cross reference methods (110) += .. parsed-literal:: :class: code @@ -5481,17 +5539,17 @@ This works in two passes: return ux 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 (`110`_) + |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 (`111`_) + |srarr|\ find user identifier usage and update ux from the given map (`112`_) .. .. class:: small - |loz| *Web Chunk cross reference methods (109)*. Used by: Web class... (`96`_) + |loz| *Web Chunk cross reference methods (110)*. Used by: Web class... (`97`_) User identifiers are collected by visiting each of the sequence of @@ -5503,8 +5561,8 @@ list as a default action. -.. _`110`: -.. rubric:: collect all user identifiers from a given map into ux (110) = +.. _`111`: +.. rubric:: collect all user identifiers from a given map into ux (111) = .. parsed-literal:: :class: code @@ -5518,7 +5576,7 @@ list as a default action. .. class:: small - |loz| *collect all user identifiers from a given map into ux (110)*. Used by: Web Chunk cross reference methods... (`109`_) + |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 @@ -5529,16 +5587,16 @@ this is appended to the sequence of chunks that reference the original user iden -.. _`111`: -.. rubric:: find user identifier usage and update ux from the given map (111) = +.. _`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): @@ -5548,7 +5606,7 @@ this is appended to the sequence of chunks that reference the original user iden .. class:: small - |loz| *find user identifier usage and update ux from the given map (111)*. Used by: Web Chunk cross reference methods... (`109`_) + |loz| *find user identifier usage and update ux from the given map (112)*. Used by: Web Chunk cross reference methods... (`110`_) Loop Detection @@ -5601,8 +5659,8 @@ LaTeX files typically begin with '%' or '\'. Everything else is probably RST. -.. _`112`: -.. rubric:: Web determination of the language from the first chunk (112) = +.. _`113`: +.. rubric:: Web determination of the language from the first chunk (113) = .. parsed-literal:: :class: code @@ -5611,7 +5669,7 @@ Everything else is probably RST. """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(str(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('\\\\'): @@ -5623,7 +5681,7 @@ Everything else is probably RST. .. class:: small - |loz| *Web determination of the language from the first chunk (112)*. Used by: Web class... (`96`_) + |loz| *Web determination of the language from the first chunk (113)*. Used by: Web class... (`97`_) The ``tangle()`` method of the ``Web`` class performs @@ -5632,15 +5690,15 @@ named output file. Note that several ``Chunks`` may share the file name, requir the file be composed of material from each ``Chunk``, in order. -.. _`113`: -.. rubric:: Web tangle the output files (113) = +.. _`114`: +.. rubric:: Web tangle the output files (114) = .. parsed-literal:: :class: code 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) @@ -5649,7 +5707,7 @@ the file be composed of material from each ``Chunk``, in order. .. class:: small - |loz| *Web tangle the output files (113)*. Used by: Web class... (`96`_) + |loz| *Web tangle the output files (114)*. Used by: Web class... (`97`_) The ``weave()`` method of the ``Web`` class creates the final documentation. @@ -5667,26 +5725,25 @@ The decision is delegated to the referenced chunk. Should it go in ``ReferenceCommand weave...``? -.. _`114`: -.. rubric:: Web weave the output document (114) = +.. _`115`: +.. rubric:: Web weave the output document (115) = .. parsed-literal:: :class: code def weave(self, aWeaver: "Weaver") -> None: - self.logger.debug("Weaving file from {!r}".format(self.webFileName)) - if not self.webFileName: + self.logger.debug("Weaving file from %r", self.web\_path) + if not self.web\_path: raise Error("No filename supplied for weaving.") - basename, \_ = os.path.splitext(self.webFileName) - with aWeaver.open(basename): + 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}".format(name)) + self.logger.debug("Weaving chunk %r", name) chunkList = self.getchunk(name) if not chunkList: - raise Error("No Definition for {!r}".format(name)) + raise Error(f"No Definition for {name!r}") chunkList[0].weaveReferenceTo(self, aWeaver) for p in chunkList[1:]: aWeaver.write(aWeaver.referenceSep()) @@ -5697,7 +5754,7 @@ The decision is delegated to the referenced chunk. .. class:: small - |loz| *Web weave the output document (114)*. Used by: Web class... (`96`_) + |loz| *Web weave the output document (115)*. Used by: Web class... (`97`_) The WebReader Class @@ -5772,7 +5829,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: @@ -5790,8 +5847,8 @@ The class has the following attributes: Summaries -.. _`115`: -.. rubric:: WebReader class - parses the input file, building the Web structure (115) = +.. _`116`: +.. rubric:: WebReader class - parses the input file, building the Web structure (116) = .. parsed-literal:: :class: code @@ -5821,7 +5878,7 @@ The class has the following attributes: # State of the reader \_source: TextIO - fileName: str + filePath: Path theWeb: "Web" def \_\_init\_\_(self, parent: Optional["WebReader"] = None) -> None: @@ -5841,20 +5898,20 @@ The class has the following attributes: self.totalFiles = 0 self.errors = 0 - |srarr|\ WebReader command literals (`132`_) + |srarr|\ WebReader command literals (`131`_) def \_\_str\_\_(self) -> str: return self.\_\_class\_\_.\_\_name\_\_ - |srarr|\ WebReader location in the input stream (`129`_) - |srarr|\ WebReader load the web (`131`_) - |srarr|\ WebReader handle a command string (`116`_), |srarr|\ (`128`_) + |srarr|\ WebReader location in the input stream (`128`_) + |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 (115)*. 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. @@ -5883,22 +5940,44 @@ A subclass can override ``handleCommand()`` to by ``load()`` is to treat the command a text, but also issue a warning. -.. _`116`: -.. rubric:: WebReader handle a command string (116) = +.. _`117`: +.. rubric:: WebReader handle a command string (117) = .. parsed-literal:: :class: code def handleCommand(self, token: str) -> bool: - self.logger.debug("Reading {!r}".format(token)) - |srarr|\ major commands segment the input into separate Chunks (`117`_) - |srarr|\ minor commands add Commands to the current Chunk (`122`_) - 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 False # did not recogize the command + 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 @@ -5906,34 +5985,25 @@ A subclass can override ``handleCommand()`` to .. class:: small - |loz| *WebReader handle a command string (116)*. Used by: WebReader class... (`115`_) + |loz| *WebReader handle a command string (117)*. Used by: WebReader class... (`116`_) The following sequence of ``if``-``elif`` statements identifies the structural commands that partition the input into separate ``Chunks``. +:: -.. _`117`: -.. rubric:: major commands segment the input into separate Chunks (117) = -.. parsed-literal:: - :class: code - - + @d OLD major commands... + @{ if token[:2] == self.cmdo: - |srarr|\ start an OutputChunk, adding it to the web (`118`_) + @ elif token[:2] == self.cmdd: - |srarr|\ start a NamedChunk or NamedDocumentChunk, adding it to the web (`119`_) + @ elif token[:2] == self.cmdi: - |srarr|\ import another file (`120`_) + @ elif token[:2] in (self.cmdrcurl,self.cmdrbrak): - |srarr|\ finish a chunk, start a new Chunk adding it to the web (`121`_) - -.. - - .. class:: small - - |loz| *major commands segment the input into separate Chunks (117)*. Used by: WebReader handle a command... (`116`_) - + @ + @} An output chunk has the form ``@o`` *name* ``@{`` *content* ``@}``. We use the first two tokens to name the ``OutputChunk``. We simply expect @@ -5960,7 +6030,7 @@ With some small additional changes, we could use ``OutputChunk(**options)``. comment\_start=''.join(options.get('start', "# ")), comment\_end=''.join(options.get('end', "")), ) - self.aChunk.fileName = self.fileName + self.aChunk.filePath = self.filePath self.aChunk.webAdd(self.theWeb) # capture an OutputChunk up to @} @@ -5968,7 +6038,7 @@ With some small additional changes, we could use ``OutputChunk(**options)``. .. class:: small - |loz| *start an OutputChunk, adding it to the web (118)*. Used by: major commands... (`117`_) + |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 @@ -5989,7 +6059,7 @@ 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. .. _`119`: @@ -6015,7 +6085,7 @@ If both are in the options, we can provide a warning, I guess. else: raise Error("Design Error") - self.aChunk.fileName = self.fileName + self.aChunk.filePath = self.filePath self.aChunk.webAdd(self.theWeb) # capture a NamedChunk up to @} or @] @@ -6023,7 +6093,7 @@ If both are in the options, we can provide a warning, I guess. .. class:: small - |loz| *start a NamedChunk or NamedDocumentChunk, adding it to the web (119)*. Used by: major commands... (`117`_) + |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 @@ -6053,32 +6123,26 @@ test output into the final document via the ``@i`` command. .. _`120`: -.. rubric:: import another file (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)) + self.logger.info("Including %r", incPath) include = WebReader(parent=self) - include.load(self.theWeb, incFile) + 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 # Seems heavy-handed, but, the file wasn't found! @@ -6089,7 +6153,7 @@ test output into the final document via the ``@i`` command. .. class:: small - |loz| *import another file (120)*. Used by: major commands... (`117`_) + |loz| *include another file (120)*. Used by: WebReader handle a command... (`117`_) When a ``@}`` or ``@]`` are found, this finishes a named chunk. The next @@ -6118,22 +6182,18 @@ For the base ``Chunk`` class, this would be false, but for all other subclasses .. class:: small - |loz| *finish a chunk, start a new Chunk adding it to the web (121)*. Used by: major commands... (`117`_) + |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``. +:: - -.. _`122`: -.. rubric:: minor commands add Commands to the current Chunk (122) = -.. parsed-literal:: - :class: code - - + @d OLD minor commands... + @{ elif token[:2] == self.cmdpipe: - |srarr|\ assign user identifiers to the current chunk (`123`_) + @ elif token[:2] == self.cmdf: self.aChunk.append(FileXrefCommand(self.tokenizer.lineNumber)) elif token[:2] == self.cmdm: @@ -6141,17 +6201,12 @@ the minor commands that add ``Command`` instances to the current open ``Chunk``. 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 (`124`_) + @ elif token[:2] == self.cmdlexpr: - |srarr|\ add an expression command to the current chunk (`126`_) + @ elif token[:2] == self.cmdcmd: - |srarr|\ double at-sign replacement, append this character to previous TextCommand (`127`_) - -.. - - .. class:: small - - |loz| *minor commands add Commands to the current Chunk (122)*. Used by: WebReader handle a command... (`116`_) + @ + @} User identifiers occur after a ``@|`` in a ``NamedChunk``. @@ -6167,8 +6222,8 @@ User identifiers are name references at the end of a NamedChunk These are accumulated and expanded by ``@u`` reference -.. _`123`: -.. rubric:: assign user identifiers to the current chunk (123) = +.. _`122`: +.. rubric:: assign user identifiers to the current chunk (122) = .. parsed-literal:: :class: code @@ -6177,14 +6232,14 @@ These are accumulated and expanded by ``@u`` reference 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 (123)*. Used by: minor commands... (`122`_) + |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 @@ -6192,8 +6247,8 @@ tokens from the input, the middle token is the referenced name. -.. _`124`: -.. rubric:: add a reference command to the current chunk (124) = +.. _`123`: +.. rubric:: add a reference command to the current chunk (123) = .. parsed-literal:: :class: code @@ -6204,13 +6259,13 @@ tokens from the input, the middle token is the referenced name. 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)) + self.logger.debug("Reading %r %r", expand, closing) .. .. class:: small - |loz| *add a reference command to the current chunk (124)*. Used by: minor commands... (`122`_) + |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*\ ``@)``. @@ -6234,8 +6289,8 @@ Note that we've removed the blanket ``os``. We provide ``os.path`` library. An ``os.getcwd()`` could be changed to ``os.path.realpath('.')``. -.. _`125`: -.. rubric:: Imports (125) += +.. _`124`: +.. rubric:: Imports (124) += .. parsed-literal:: :class: code @@ -6249,12 +6304,12 @@ An ``os.getcwd()`` could be changed to ``os.path.realpath('.')``. .. class:: small - |loz| *Imports (125)*. Used by: pyweb.py (`156`_) + |loz| *Imports (124)*. Used by: pyweb.py (`155`_) -.. _`126`: -.. rubric:: add an expression command to the current chunk (126) = +.. _`125`: +.. rubric:: add an expression command to the current chunk (125) = .. parsed-literal:: :class: code @@ -6275,25 +6330,25 @@ An ``os.getcwd()`` could be changed to ``os.path.realpath('.')``. time=time, datetime=datetime, platform=platform, - theLocation=self.location(), + theLocation=str(self.location()), theWebReader=self, - theFile=self.theWeb.webFileName, + theFile=self.theWeb.web\_path, 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)) + 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) + 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 (126)*. Used by: minor commands... (`122`_) + |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 @@ -6307,8 +6362,8 @@ And we make sure the next chunk will be appended to this so that it's largely seamless. -.. _`127`: -.. rubric:: double at-sign replacement, append this character to previous TextCommand (127) = +.. _`126`: +.. rubric:: double at-sign replacement, append this character to previous TextCommand (126) = .. parsed-literal:: :class: code @@ -6319,7 +6374,7 @@ largely seamless. .. class:: small - |loz| *double at-sign replacement, append this character to previous TextCommand (127)*. Used by: minor commands... (`122`_) + |loz| *double at-sign replacement, append this character to previous TextCommand (126)*. Used by: WebReader handle a command... (`117`_) The ``expect()`` method examines the @@ -6328,8 +6383,8 @@ If this is not found, a standard type of error message is raised. This is used by ``handleCommand()``. -.. _`128`: -.. rubric:: WebReader handle a command string (128) += +.. _`127`: +.. rubric:: WebReader handle a command string (127) += .. parsed-literal:: :class: code @@ -6340,11 +6395,11 @@ This is used by ``handleCommand()``. 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.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}".format(self.location(),tokens,t)) + self.logger.error("At %r: expected %r, found %r", self.location(), tokens, t) self.errors += 1 return None return t @@ -6354,7 +6409,7 @@ This is used by ``handleCommand()``. .. class:: small - |loz| *WebReader handle a command string (128)*. Used by: WebReader class... (`115`_) + |loz| *WebReader handle a command string (127)*. Used by: WebReader class... (`116`_) The ``location()`` provides the file name and line number. @@ -6362,21 +6417,21 @@ This allows error messages as well as tangled or woven output to correctly reference the original input files. -.. _`129`: -.. rubric:: WebReader location in the input stream (129) = +.. _`128`: +.. rubric:: WebReader location in the input stream (128) = .. parsed-literal:: :class: code def location(self) -> tuple[str, int]: - return (self.fileName, self.tokenizer.lineNumber+1) + return (str(self.filePath), self.tokenizer.lineNumber+1) .. .. class:: small - |loz| *WebReader location in the input stream (129)*. Used by: WebReader class... (`115`_) + |loz| *WebReader location in the input stream (128)*. Used by: WebReader class... (`116`_) The ``load()`` method reads the entire input file as a sequence @@ -6390,8 +6445,8 @@ The ``load()`` method is used recursively to handle the ``@i`` command. The issu is that it's always loading a single top-level web. -.. _`130`: -.. rubric:: Imports (130) += +.. _`129`: +.. rubric:: Imports (129) += .. parsed-literal:: :class: code @@ -6401,30 +6456,30 @@ is that it's always loading a single top-level web. .. class:: small - |loz| *Imports (130)*. Used by: pyweb.py (`156`_) + |loz| *Imports (129)*. Used by: pyweb.py (`155`_) -.. _`131`: -.. rubric:: WebReader load the web (131) = +.. _`130`: +.. rubric:: WebReader load the web (130) = .. parsed-literal:: :class: code - def load(self, web: "Web", filename: str, source: TextIO \| None = None) -> "WebReader": + def load(self, web: "Web", filepath: Path, source: TextIO \| None = None) -> "WebReader": self.theWeb = web - self.fileName = filename + self.filePath = filepath # 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 + # \*\*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 open(self.fileName, "r") as self.\_source: + with self.filePath.open() as self.\_source: self.parse\_source() return self @@ -6440,18 +6495,21 @@ is that it's always loading a single top-level web. if self.handleCommand(token): continue else: - self.logger.warning('Unknown @-command in input: {!r}'.format(token)) + 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 (131)*. Used by: WebReader class... (`115`_) + |loz| *WebReader load the web (130)*. Used by: WebReader class... (`116`_) The command character can be changed to permit @@ -6462,8 +6520,8 @@ command character. -.. _`132`: -.. rubric:: WebReader command literals (132) = +.. _`131`: +.. rubric:: WebReader command literals (131) = .. parsed-literal:: :class: code @@ -6494,7 +6552,7 @@ command character. .. class:: small - |loz| *WebReader command literals (132)*. Used by: WebReader class... (`115`_) + |loz| *WebReader command literals (131)*. Used by: WebReader class... (`116`_) @@ -6541,8 +6599,8 @@ and ``next(tokens)`` to step through the sequence of tokens until we raise a ``S exception. -.. _`133`: -.. rubric:: Imports (133) += +.. _`132`: +.. rubric:: Imports (132) += .. parsed-literal:: :class: code @@ -6555,12 +6613,12 @@ exception. .. class:: small - |loz| *Imports (133)*. Used by: pyweb.py (`156`_) + |loz| *Imports (132)*. Used by: pyweb.py (`155`_) -.. _`134`: -.. rubric:: Tokenizer class - breaks input into tokens (134) = +.. _`133`: +.. rubric:: Tokenizer class - breaks input into tokens (133) = .. parsed-literal:: :class: code @@ -6568,13 +6626,15 @@ exception. class Tokenizer(Iterator[str]): def \_\_init\_\_(self, stream: TextIO, command\_char: str='@') -> None: self.command = command\_char - self.parsePat = re.compile(r'({!s}.\|\\n)'.format(self.command)) + 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 @@ -6583,7 +6643,7 @@ exception. .. class:: small - |loz| *Tokenizer class - breaks input into tokens (134)*. Used by: Base Class Definitions (`1`_) + |loz| *Tokenizer class - breaks input into tokens (133)*. Used by: Base Class Definitions (`1`_) The Option Parser Class @@ -6610,8 +6670,8 @@ To handle this, we have a separate lexical scanner and parser for these two commands. -.. _`135`: -.. rubric:: Imports (135) += +.. _`134`: +.. rubric:: Imports (134) += .. parsed-literal:: :class: code @@ -6623,7 +6683,7 @@ two commands. .. class:: small - |loz| *Imports (135)*. Used by: pyweb.py (`156`_) + |loz| *Imports (134)*. Used by: pyweb.py (`155`_) Here's how we can define an option. @@ -6641,8 +6701,8 @@ Here's how we can define an option. The idea is to parallel ``argparse.add_argument()`` syntax. -.. _`136`: -.. rubric:: Option Parser class - locates optional values on commands (136) = +.. _`135`: +.. rubric:: Option Parser class - locates optional values on commands (135) = .. parsed-literal:: :class: code @@ -6653,12 +6713,12 @@ The idea is to parallel ``argparse.add_argument()`` syntax. .. class:: small - |loz| *Option Parser class - locates optional values on commands (136)*. Used by: Base Class Definitions (`1`_) + |loz| *Option Parser class - locates optional values on commands (135)*. Used by: Base Class Definitions (`1`_) -.. _`137`: -.. rubric:: Option Parser class - locates optional values on commands (137) += +.. _`136`: +.. rubric:: Option Parser class - locates optional values on commands (136) += .. parsed-literal:: :class: code @@ -6672,7 +6732,7 @@ The idea is to parallel ``argparse.add_argument()`` syntax. .. class:: small - |loz| *Option Parser class - locates optional values on commands (137)*. 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. @@ -6680,8 +6740,8 @@ It then steps through the words, accumulating the options and the final argument value. -.. _`138`: -.. rubric:: Option Parser class - locates optional values on commands (138) += +.. _`137`: +.. rubric:: Option Parser class - locates optional values on commands (137) += .. parsed-literal:: :class: code @@ -6695,7 +6755,7 @@ final argument value. try: word\_iter = iter(shlex.split(text)) except ValueError as e: - raise Error("Error parsing options in {!r}".format(text)) + raise Error(f"Error parsing options in {text!r}") options = dict(self.\_group(word\_iter)) return options @@ -6711,7 +6771,7 @@ final argument value. try: 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: @@ -6719,7 +6779,7 @@ 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): @@ -6740,7 +6800,7 @@ final argument value. .. class:: small - |loz| *Option Parser class - locates optional values on commands (138)*. 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. @@ -6755,7 +6815,7 @@ Then we'd have a loop something like this. (Untested, incomplete, just hand-wavi 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 trailers.pop(0) @@ -6805,23 +6865,23 @@ application. A partner with this command hierarchy is the Application class that defines the application options, inputs and results. -.. _`139`: -.. rubric:: Action class hierarchy - used to describe basic actions of the application (139) = +.. _`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 (`140`_) - |srarr|\ ActionSequence subclass that holds a sequence of other actions (`143`_) - |srarr|\ WeaveAction subclass initiates the weave action (`147`_) - |srarr|\ TangleAction subclass initiates the tangle action (`150`_) - |srarr|\ LoadAction subclass loads the document web (`153`_) + |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 (139)*. 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 @@ -6868,8 +6928,8 @@ An ``Action`` has a number of common attributes. -.. _`140`: -.. rubric:: Action superclass has common features of all actions (140) = +.. _`139`: +.. rubric:: Action superclass has common features of all actions (139) = .. parsed-literal:: :class: code @@ -6884,17 +6944,17 @@ An ``Action`` has a number of common attributes. self.logger = logging.getLogger(self.\_\_class\_\_.\_\_qualname\_\_) def \_\_str\_\_(self) -> str: - return "{!s} [{!s}]".format(self.name, self.web) + return f"{self.name!s} [{self.web!s}]" - |srarr|\ Action call method actually does the real work (`141`_) - |srarr|\ Action final summary of what was done (`142`_) + |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 (140)*. Used by: Action class hierarchy... (`139`_) + |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. @@ -6902,14 +6962,14 @@ For the superclass, it merely logs a message. This is overridden by a subclass. -.. _`141`: -.. rubric:: Action call method actually does the real work (141) = +.. _`140`: +.. rubric:: Action call method actually does the real work (140) = .. parsed-literal:: :class: code def \_\_call\_\_(self) -> None: - self.logger.info("Starting {!s}".format(self.name)) + self.logger.info("Starting %s", self.name) self.start = time.process\_time() @@ -6917,16 +6977,15 @@ by a subclass. .. class:: small - |loz| *Action call method actually does the real work (141)*. Used by: Action superclass... (`140`_) + |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. - -.. _`142`: -.. rubric:: Action final summary of what was done (142) = +.. _`141`: +.. rubric:: Action final summary of what was done (141) = .. parsed-literal:: :class: code @@ -6936,14 +6995,14 @@ statistics for this action. return (self.start and time.process\_time()-self.start) or 0 def summary(self) -> str: - return "{!s} in {:0.3f} sec.".format(self.name, self.duration()) + return f"{self.name!s} in {self.duration():0.3f} sec." .. .. class:: small - |loz| *Action final summary of what was done (142)*. Used by: Action superclass... (`140`_) + |loz| *Action final summary of what was done (141)*. Used by: Action superclass... (`139`_) ActionSequence Class @@ -6963,8 +7022,8 @@ an ``append()`` method that is used to construct the sequence of actions. -.. _`143`: -.. rubric:: ActionSequence subclass that holds a sequence of other actions (143) = +.. _`142`: +.. rubric:: ActionSequence subclass that holds a sequence of other actions (142) = .. parsed-literal:: :class: code @@ -6979,16 +7038,16 @@ an ``append()`` method that is used to construct the sequence of actions. def \_\_str\_\_(self) -> str: return "; ".join([str(x) for x in self.opSequence]) - |srarr|\ ActionSequence call method delegates the sequence of ations (`144`_) - |srarr|\ ActionSequence append adds a new action to the sequence (`145`_) - |srarr|\ ActionSequence summary summarizes each step (`146`_) + |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 (143)*. Used by: Action class hierarchy... (`139`_) + |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, @@ -6997,13 +7056,14 @@ it is possible to short-cut argument processing by using the Python sub-action. -.. _`144`: -.. rubric:: ActionSequence call method delegates the sequence of ations (144) = +.. _`143`: +.. rubric:: ActionSequence call method delegates the sequence of ations (143) = .. parsed-literal:: :class: code def \_\_call\_\_(self) -> None: + super().\_\_call\_\_() for o in self.opSequence: o.web = self.web o.options = self.options @@ -7014,15 +7074,15 @@ sub-action. .. class:: small - |loz| *ActionSequence call method delegates the sequence of ations (144)*. Used by: ActionSequence subclass... (`143`_) + |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. -.. _`145`: -.. rubric:: ActionSequence append adds a new action to the sequence (145) = +.. _`144`: +.. rubric:: ActionSequence append adds a new action to the sequence (144) = .. parsed-literal:: :class: code @@ -7035,15 +7095,15 @@ we delegate sequence related actions directly to the underlying sequence. .. class:: small - |loz| *ActionSequence append adds a new action to the sequence (145)*. Used by: ActionSequence subclass... (`143`_) + |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. -.. _`146`: -.. rubric:: ActionSequence summary summarizes each step (146) = +.. _`145`: +.. rubric:: ActionSequence summary summarizes each step (145) = .. parsed-literal:: :class: code @@ -7056,7 +7116,7 @@ statistics for each step of this action. .. class:: small - |loz| *ActionSequence summary summarizes each step (146)*. Used by: ActionSequence subclass... (`143`_) + |loz| *ActionSequence summary summarizes each step (145)*. Used by: ActionSequence subclass... (`142`_) WeaveAction Class @@ -7074,8 +7134,8 @@ 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. -.. _`147`: -.. rubric:: WeaveAction subclass initiates the weave action (147) = +.. _`146`: +.. rubric:: WeaveAction subclass initiates the weave action (146) = .. parsed-literal:: :class: code @@ -7086,17 +7146,17 @@ Otherwise, the ``web.language()`` method function is used to guess what weaver t super().\_\_init\_\_("Weave") def \_\_str\_\_(self) -> str: - return "{!s} [{!s}, {!s}]".format(self.name, self.web, self.options.theWeaver) + return f"{self.name!s} [{self.web!s}, {self.options.theWeaver!s}]" - |srarr|\ WeaveAction call method to pick the language (`148`_) - |srarr|\ WeaveAction summary of language choice (`149`_) + |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 (147)*. Used by: Action class hierarchy... (`139`_) + |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 @@ -7107,8 +7167,8 @@ Weaving can only raise an exception when there is a reference to a chunk that is never defined. -.. _`148`: -.. rubric:: WeaveAction call method to pick the language (148) = +.. _`147`: +.. rubric:: WeaveAction call method to pick the language (147) = .. parsed-literal:: :class: code @@ -7118,14 +7178,13 @@ is never defined. 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.logger.info("Using %s", 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)) + self.logger.error("Problems weaving document from %r (weave file is faulty).", self.web.web\_path) #raise @@ -7133,7 +7192,7 @@ is never defined. .. class:: small - |loz| *WeaveAction call method to pick the language (148)*. Used by: WeaveAction subclass... (`147`_) + |loz| *WeaveAction call method to pick the language (147)*. Used by: WeaveAction subclass... (`146`_) The ``summary()`` method returns some basic processing @@ -7141,24 +7200,25 @@ statistics for the weave action. -.. _`149`: -.. rubric:: WeaveAction summary of language choice (149) = +.. _`148`: +.. rubric:: WeaveAction summary of language choice (148) = .. parsed-literal:: :class: code def summary(self) -> str: if self.options.theWeaver and self.options.theWeaver.linesWritten > 0: - return "{!s} {:d} lines in {:0.3f} 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 (149)*. Used by: WeaveAction subclass... (`147`_) + |loz| *WeaveAction summary of language choice (148)*. Used by: WeaveAction subclass... (`146`_) TangleAction Class @@ -7175,8 +7235,8 @@ This class overrides the ``__call__()`` method of the superclass. The options **must** include ``theTangler``, with the ``Tangler`` instance to be used. -.. _`150`: -.. rubric:: TangleAction subclass initiates the tangle action (150) = +.. _`149`: +.. rubric:: TangleAction subclass initiates the tangle action (149) = .. parsed-literal:: :class: code @@ -7186,15 +7246,15 @@ The options **must** include ``theTangler``, with the ``Tangler`` instance to be def \_\_init\_\_(self) -> None: super().\_\_init\_\_("Tangle") - |srarr|\ TangleAction call method does tangling of the output files (`151`_) - |srarr|\ TangleAction summary method provides total lines tangled (`152`_) + |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 (150)*. Used by: Action class hierarchy... (`139`_) + |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``) @@ -7203,8 +7263,8 @@ with any of ``@d`` or ``@o`` and use ``@{`` ``@}`` brackets. -.. _`151`: -.. rubric:: TangleAction call method does tangling of the output files (151) = +.. _`150`: +.. rubric:: TangleAction call method does tangling of the output files (150) = .. parsed-literal:: :class: code @@ -7215,8 +7275,7 @@ with any of ``@d`` or ``@o`` and use ``@{`` ``@}`` brackets. 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)) + self.logger.error("Problems tangling outputs from %r (tangle files are faulty).", self.web.web\_path) #raise @@ -7224,31 +7283,32 @@ with any of ``@d`` or ``@o`` and use ``@{`` ``@}`` brackets. .. class:: small - |loz| *TangleAction call method does tangling of the output files (151)*. Used by: TangleAction subclass... (`150`_) + |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. -.. _`152`: -.. rubric:: TangleAction summary method provides total lines tangled (152) = +.. _`151`: +.. rubric:: TangleAction summary method provides total lines tangled (151) = .. parsed-literal:: :class: code def summary(self) -> str: if self.options.theTangler and self.options.theTangler.linesWritten > 0: - return "{!s} {:d} lines in {:0.3f} 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 (152)*. Used by: TangleAction subclass... (`150`_) + |loz| *TangleAction summary method provides total lines tangled (151)*. Used by: TangleAction subclass... (`149`_) @@ -7267,8 +7327,8 @@ The options **must** include ``webReader``, with the ``WebReader`` instance to b -.. _`153`: -.. rubric:: LoadAction subclass loads the document web (153) = +.. _`152`: +.. rubric:: LoadAction subclass loads the document web (152) = .. parsed-literal:: :class: code @@ -7278,16 +7338,16 @@ The options **must** include ``webReader``, with the ``WebReader`` instance to b def \_\_init\_\_(self) -> None: super().\_\_init\_\_("Load") def \_\_str\_\_(self) -> str: - return "Load [{!s}, {!s}]".format(self.webReader, self.web) - |srarr|\ LoadAction call method loads the input files (`154`_) - |srarr|\ LoadAction summary provides lines read (`155`_) + 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 (153)*. Used by: Action class hierarchy... (`139`_) + |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 @@ -7310,8 +7370,8 @@ exceptions due to incorrect inputs. chunk reference cannot be resolved to a named chunk. -.. _`154`: -.. rubric:: LoadAction call method loads the input files (154) = +.. _`153`: +.. rubric:: LoadAction call method loads the input files (153) = .. parsed-literal:: :class: code @@ -7321,11 +7381,10 @@ exceptions due to incorrect inputs. 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.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") @@ -7345,30 +7404,30 @@ exceptions due to incorrect inputs. .. class:: small - |loz| *LoadAction call method loads the input files (154)*. Used by: LoadAction subclass... (`153`_) + |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. -.. _`155`: -.. rubric:: LoadAction summary provides lines read (155) = +.. _`154`: +.. rubric:: LoadAction summary provides lines read (154) = .. parsed-literal:: :class: code def summary(self) -> str: - return "{!s} {:d} lines from {:d} files in {:0.3f} sec.".format( - self.name, self.webReader.totalLines, - self.webReader.totalFiles, self.duration() ) + 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 (155)*. Used by: LoadAction subclass... (`153`_) + |loz| *LoadAction summary provides lines read (154)*. Used by: LoadAction subclass... (`152`_) @@ -7378,23 +7437,23 @@ statistics for the load action. The **pyWeb** application file is shown below: -.. _`156`: -.. rubric:: pyweb.py (156) = +.. _`155`: +.. rubric:: pyweb.py (155) = .. parsed-literal:: :class: code - |srarr|\ Overheads (`158`_), |srarr|\ (`159`_), |srarr|\ (`160`_) - |srarr|\ Imports (`11`_), |srarr|\ (`47`_), |srarr|\ (`57`_), |srarr|\ (`97`_), |srarr|\ (`125`_), |srarr|\ (`130`_), |srarr|\ (`133`_), |srarr|\ (`135`_), |srarr|\ (`157`_), |srarr|\ (`161`_), |srarr|\ (`167`_) + |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 (`162`_), |srarr|\ (`163`_) - |srarr|\ Logging Setup (`168`_), |srarr|\ (`169`_) - |srarr|\ Interface Functions (`170`_) + |srarr|\ Application Class (`161`_), |srarr|\ (`162`_) + |srarr|\ Logging Setup (`167`_), |srarr|\ (`168`_) + |srarr|\ Interface Functions (`169`_) .. .. class:: small - |loz| *pyweb.py (156)*. + |loz| *pyweb.py (155)*. The `Overheads`_ are described below, they include things like: @@ -7440,8 +7499,8 @@ closer to where they're referenced. -.. _`157`: -.. rubric:: Imports (157) += +.. _`156`: +.. rubric:: Imports (156) += .. parsed-literal:: :class: code @@ -7456,7 +7515,7 @@ closer to where they're referenced. .. class:: small - |loz| *Imports (157)*. Used by: pyweb.py (`156`_) + |loz| *Imports (156)*. Used by: pyweb.py (`155`_) Note that ``os.path``, ``time``, ``datetime`` and ``platform``` @@ -7474,8 +7533,8 @@ file as standard input. -.. _`158`: -.. rubric:: Overheads (158) = +.. _`157`: +.. rubric:: Overheads (157) = .. parsed-literal:: :class: code @@ -7485,7 +7544,7 @@ file as standard input. .. class:: small - |loz| *Overheads (158)*. Used by: pyweb.py (`156`_) + |loz| *Overheads (157)*. Used by: pyweb.py (`155`_) A Python ``__doc__`` string provides a standard vehicle for documenting @@ -7495,8 +7554,8 @@ detailed usage information. -.. _`159`: -.. rubric:: Overheads (159) += +.. _`158`: +.. rubric:: Overheads (158) += .. parsed-literal:: :class: code @@ -7532,7 +7591,7 @@ detailed usage information. .. class:: small - |loz| *Overheads (159)*. Used by: pyweb.py (`156`_) + |loz| *Overheads (158)*. Used by: pyweb.py (`155`_) The keyword cruft is a standard way of placing version control information into @@ -7544,23 +7603,23 @@ We also sneak in a "DO NOT EDIT" warning that belongs in all generated applicati source files. -.. _`160`: -.. rubric:: Overheads (160) += +.. _`159`: +.. rubric:: Overheads (159) += .. parsed-literal:: :class: code \_\_version\_\_ = """3.1""" ### DO NOT EDIT THIS FILE! - ### It was created by pyweb-3.0.py, \_\_version\_\_='3.0'. - ### From source pyweb.w modified Wed Jun 8 14:04:44 2022. + ### It was created by /Users/slott/Documents/Projects/PyWebTool-3/pyweb/pyweb.py, \_\_version\_\_='3.0'. + ### From source pyweb.w modified Fri Jun 10 10:48:04 2022. ### In working directory '/Users/slott/Documents/Projects/py-web-tool'. .. .. class:: small - |loz| *Overheads (160)*. Used by: pyweb.py (`156`_) + |loz| *Overheads (159)*. Used by: pyweb.py (`155`_) @@ -7614,8 +7673,8 @@ The configuration can be either a ``types.SimpleNamespace`` or an -.. _`161`: -.. rubric:: Imports (161) += +.. _`160`: +.. rubric:: Imports (160) += .. parsed-literal:: :class: code @@ -7626,12 +7685,12 @@ The configuration can be either a ``types.SimpleNamespace`` or an .. class:: small - |loz| *Imports (161)*. Used by: pyweb.py (`156`_) + |loz| *Imports (160)*. Used by: pyweb.py (`155`_) -.. _`162`: -.. rubric:: Application Class (162) = +.. _`161`: +.. rubric:: Application Class (161) = .. parsed-literal:: :class: code @@ -7639,17 +7698,17 @@ The configuration can be either a ``types.SimpleNamespace`` or an class Application: def \_\_init\_\_(self) -> None: self.logger = logging.getLogger(self.\_\_class\_\_.\_\_qualname\_\_) - |srarr|\ Application default options (`164`_) + |srarr|\ Application default options (`163`_) - |srarr|\ Application parse command line (`165`_) - |srarr|\ Application class process all files (`166`_) + |srarr|\ Application parse command line (`164`_) + |srarr|\ Application class process all files (`165`_) .. .. class:: small - |loz| *Application Class (162)*. Used by: pyweb.py (`156`_) + |loz| *Application Class (161)*. Used by: pyweb.py (`155`_) The first part of parsing the command line is @@ -7727,8 +7786,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. -.. _`163`: -.. rubric:: Application Class (163) += +.. _`162`: +.. rubric:: Application Class (162) += .. parsed-literal:: :class: code @@ -7745,15 +7804,15 @@ that aren't really meant to be used, we provide a manually-developed list. .. class:: small - |loz| *Application Class (163)*. Used by: pyweb.py (`156`_) + |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. -.. _`164`: -.. rubric:: Application default options (164) = +.. _`163`: +.. rubric:: Application default options (163) = .. parsed-literal:: :class: code @@ -7762,9 +7821,9 @@ on these simple text values to create more useful objects. verbosity=logging.INFO, command='@', weaver='rst', - skip='', # Don't skip any steps - permit='', # Don't tolerate missing includes - reference='s', # Simple references + skip='', # Don't skip any steps + permit='', # Don't tolerate missing includes + reference='s', # Simple references tangler\_line\_numbers=False, ) self.expand(self.defaults) @@ -7783,7 +7842,7 @@ on these simple text values to create more useful objects. .. class:: small - |loz| *Application default options (164)*. Used by: Application Class... (`162`_) + |loz| *Application default options (163)*. Used by: Application Class... (`161`_) The algorithm for parsing the command line parameters uses the built in @@ -7795,8 +7854,8 @@ instances. -.. _`165`: -.. rubric:: Application parse command line (165) = +.. _`164`: +.. rubric:: Application parse command line (164) = .. parsed-literal:: :class: code @@ -7812,7 +7871,7 @@ instances. 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='+') + p.add\_argument("files", nargs='+', type=Path) config = p.parse\_args(argv, namespace=self.defaults) self.expand(config) return config @@ -7821,12 +7880,13 @@ instances. """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") try: weaver\_class = weavers[config.weaver.lower()] @@ -7835,14 +7895,14 @@ instances. 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)) + raise TypeError(f"{weaver\_class!r} not a subclass of Weaver") 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] + config.permitList = [f'{config.command!s}{c!s}' for c in config.permit] else: config.permitList = [] @@ -7850,13 +7910,12 @@ instances. return config - .. .. class:: small - |loz| *Application parse command line (165)*. Used by: Application Class... (`162`_) + |loz| *Application parse command line (164)*. Used by: Application Class... (`161`_) The ``process()`` function uses the current ``Application`` settings @@ -7866,7 +7925,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 @@ -7882,8 +7941,8 @@ The re-raising is done so that all exceptions are handled by the outermost main program. -.. _`166`: -.. rubric:: Application class process all files (166) = +.. _`165`: +.. rubric:: Application class process all files (165) = .. parsed-literal:: :class: code @@ -7891,26 +7950,25 @@ outermost main program. def process(self, config: argparse.Namespace) -> None: root = logging.getLogger() root.setLevel(config.verbosity) - self.logger.debug( "Setting root log level to {!r}".format( - logging.getLevelName(root.getEffectiveLevel()) ) ) + 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 + if config.skip.lower().startswith('w'): # not weaving == tangling self.theAction = self.doTangle - elif config.skip.lower().startswith('t'): # not tangling == weaving + 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.logger.info("%s %r", self.theAction.name, f) + config.source\_path = f self.theAction.web = w self.theAction.options = config self.theAction() @@ -7921,7 +7979,7 @@ outermost main program. .. class:: small - |loz| *Application class process all files (166)*. Used by: Application Class... (`162`_) + |loz| *Application class process all files (165)*. Used by: Application Class... (`161`_) Logging Setup @@ -7932,8 +7990,8 @@ function in an explicit ``with`` statement that assures that logging is configured and cleaned up politely. -.. _`167`: -.. rubric:: Imports (167) += +.. _`166`: +.. rubric:: Imports (166) += .. parsed-literal:: :class: code @@ -7946,7 +8004,7 @@ configured and cleaned up politely. .. class:: small - |loz| *Imports (167)*. Used by: pyweb.py (`156`_) + |loz| *Imports (166)*. Used by: pyweb.py (`155`_) This has two configuration approaches. If a positional argument is given, @@ -7957,8 +8015,8 @@ A subclass might properly load a dictionary encoded in YAML and use that with ``logging.config.dictConfig``. -.. _`168`: -.. rubric:: Logging Setup (168) = +.. _`167`: +.. rubric:: Logging Setup (167) = .. parsed-literal:: :class: code @@ -7967,12 +8025,14 @@ encoded in YAML and use that with ``logging.config.dictConfig``. 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 @@ -7981,7 +8041,7 @@ encoded in YAML and use that with ``logging.config.dictConfig``. .. class:: small - |loz| *Logging Setup (168)*. Used by: pyweb.py (`156`_) + |loz| *Logging Setup (167)*. Used by: pyweb.py (`155`_) Here's a sample logging setup. This creates a simple console handler and @@ -7991,8 +8051,8 @@ It defines the root logger plus two overrides for class loggers that might be used to gather additional information. -.. _`169`: -.. rubric:: Logging Setup (169) += +.. _`168`: +.. rubric:: Logging Setup (168) += .. parsed-literal:: :class: code @@ -8000,6 +8060,7 @@ used to gather additional information. log\_config = { 'version': 1, 'disable\_existing\_loggers': False, # Allow pre-existing loggers to work. + 'style': '{', 'handlers': { 'console': { 'class': 'logging.StreamHandler', @@ -8028,7 +8089,7 @@ used to gather additional information. .. class:: small - |loz| *Logging Setup (169)*. Used by: pyweb.py (`156`_) + |loz| *Logging Setup (168)*. Used by: pyweb.py (`155`_) This seems a bit verbose; a separate configuration file might be better. @@ -8050,8 +8111,8 @@ We might also want to parse a logging configuration file, as well as a weaver template configuration file. -.. _`170`: -.. rubric:: Interface Functions (170) = +.. _`169`: +.. rubric:: Interface Functions (169) = .. parsed-literal:: :class: code @@ -8069,7 +8130,7 @@ as a weaver template configuration file. .. class:: small - |loz| *Interface Functions (170)*. Used by: pyweb.py (`156`_) + |loz| *Interface Functions (169)*. Used by: pyweb.py (`155`_) This can be extended by doing something like the following. @@ -8158,8 +8219,8 @@ Note the general flow of this top-level script. a summary. -.. _`171`: -.. rubric:: tangle.py (171) = +.. _`170`: +.. rubric:: tangle.py (170) = .. parsed-literal:: :class: code @@ -8196,7 +8257,7 @@ Note the general flow of this top-level script. .. class:: small - |loz| *tangle.py (171)*. + |loz| *tangle.py (170)*. ``weave.py`` Script @@ -8209,25 +8270,25 @@ to define a customized set of templates for a different markup language. A customized weaver generally has three parts. -.. _`172`: -.. rubric:: weave.py (172) = +.. _`171`: +.. rubric:: weave.py (171) = .. parsed-literal:: :class: code - |srarr|\ weave.py overheads for correct operation of a script (`173`_) - |srarr|\ weave.py custom weaver definition to customize the Weaver being used (`174`_) - |srarr|\ weaver.py processing: load and weave the document (`175`_) + |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 (172)*. + |loz| *weave.py (171)*. -.. _`173`: -.. rubric:: weave.py overheads for correct operation of a script (173) = +.. _`172`: +.. rubric:: weave.py overheads for correct operation of a script (172) = .. parsed-literal:: :class: code @@ -8242,12 +8303,12 @@ A customized weaver generally has three parts. .. class:: small - |loz| *weave.py overheads for correct operation of a script (173)*. Used by: weave.py (`172`_) + |loz| *weave.py overheads for correct operation of a script (172)*. Used by: weave.py (`171`_) -.. _`174`: -.. rubric:: weave.py custom weaver definition to customize the Weaver being used (174) = +.. _`173`: +.. rubric:: weave.py custom weaver definition to customize the Weaver being used (173) = .. parsed-literal:: :class: code @@ -8299,12 +8360,12 @@ A customized weaver generally has three parts. .. class:: small - |loz| *weave.py custom weaver definition to customize the Weaver being used (174)*. Used by: weave.py (`172`_) + |loz| *weave.py custom weaver definition to customize the Weaver being used (173)*. Used by: weave.py (`171`_) -.. _`175`: -.. rubric:: weaver.py processing: load and weave the document (175) = +.. _`174`: +.. rubric:: weaver.py processing: load and weave the document (174) = .. parsed-literal:: :class: code @@ -8336,7 +8397,7 @@ A customized weaver generally has three parts. .. class:: small - |loz| *weaver.py processing: load and weave the document (175)*. Used by: weave.py (`172`_) + |loz| *weaver.py processing: load and weave the document (174)*. Used by: weave.py (`171`_) The ``setup.py``, ``requirements-dev.txt`` and ``MANIFEST.in`` files @@ -8345,8 +8406,8 @@ The ``setup.py``, ``requirements-dev.txt`` and ``MANIFEST.in`` files In order to support a pleasant installation, the ``setup.py`` file is helpful. -.. _`176`: -.. rubric:: setup.py (176) = +.. _`175`: +.. rubric:: setup.py (175) = .. parsed-literal:: :class: code @@ -8374,7 +8435,7 @@ In order to support a pleasant installation, the ``setup.py`` file is helpful. .. class:: small - |loz| *setup.py (176)*. + |loz| *setup.py (175)*. In order build a source distribution kit the ``python3 setup.py sdist`` requires a @@ -8383,8 +8444,8 @@ that specifies additional rules. We use a simple inclusion to augment the default manifest rules. -.. _`177`: -.. rubric:: MANIFEST.in (177) = +.. _`176`: +.. rubric:: MANIFEST.in (176) = .. parsed-literal:: :class: code @@ -8396,14 +8457,14 @@ We use a simple inclusion to augment the default manifest rules. .. class:: small - |loz| *MANIFEST.in (177)*. + |loz| *MANIFEST.in (176)*. In order to install dependencies, the following file is also used. -.. _`178`: -.. rubric:: requirements-dev.txt (178) = +.. _`177`: +.. rubric:: requirements-dev.txt (177) = .. parsed-literal:: :class: code @@ -8411,12 +8472,13 @@ In order to install dependencies, the following file is also used. docutils==0.18.1 tox==3.25.0 mypy==0.910 + pytest==7.1.2 .. .. class:: small - |loz| *requirements-dev.txt (178)*. + |loz| *requirements-dev.txt (177)*. The ``README`` file @@ -8425,8 +8487,8 @@ The ``README`` file Here's the README file. -.. _`179`: -.. rubric:: README (179) = +.. _`178`: +.. rubric:: README (178) = .. parsed-literal:: :class: code @@ -8521,6 +8583,7 @@ Here's the README file. python3 -m pyweb pyweb\_test.w PYTHONPATH=.. python3 test.py rst2html.py pyweb\_test.rst pyweb\_test.html + mypy --strict pyweb.py @@ -8528,7 +8591,7 @@ Here's the README file. .. class:: small - |loz| *README (179)*. + |loz| *README (178)*. The HTML Support Files @@ -8541,8 +8604,8 @@ The default CSS file (stylesheet-path) may need to be customized for your installation of docutils. -.. _`180`: -.. rubric:: docutils.conf (180) = +.. _`179`: +.. rubric:: docutils.conf (179) = .. parsed-literal:: :class: code @@ -8557,15 +8620,15 @@ installation of docutils. .. class:: small - |loz| *docutils.conf (180)*. + |loz| *docutils.conf (179)*. ``page-layout.css`` This tweaks one CSS to be sure that the resulting HTML pages are easier to read. -.. _`181`: -.. rubric:: page-layout.css (181) = +.. _`180`: +.. rubric:: page-layout.css (180) = .. parsed-literal:: :class: code @@ -8591,15 +8654,15 @@ the resulting HTML pages are easier to read. .. class:: small - |loz| *page-layout.css (181)*. + |loz| *page-layout.css (180)*. 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. -.. _`182`: -.. rubric:: .nojekyll (182) = +.. _`181`: +.. rubric:: .nojekyll (181) = .. parsed-literal:: :class: code @@ -8609,14 +8672,14 @@ bug in ``NamedChunk.tangle()`` that prevents handling zero-length text. .. class:: small - |loz| *.nojekyll (182)*. + |loz| *.nojekyll (181)*. Here's an ``index.html`` to redirect GitHub to the ``pyweb.html`` file. -.. _`183`: -.. rubric:: index.html (183) = +.. _`182`: +.. rubric:: index.html (182) = .. parsed-literal:: :class: code @@ -8633,7 +8696,7 @@ Here's an ``index.html`` to redirect GitHub to the ``pyweb.html`` file. .. class:: small - |loz| *index.html (183)*. + |loz| *index.html (182)*. @@ -8646,8 +8709,8 @@ to **py-web-tool**. Note that there are tabs in this file. We bootstrap the next version from the 3.0 version. -.. _`184`: -.. rubric:: Makefile (184) = +.. _`183`: +.. rubric:: Makefile (183) = .. parsed-literal:: :class: code @@ -8660,38 +8723,42 @@ Note that there are tabs in this file. We bootstrap the next version from the 3. .PHONY : test build # Note the bootstrapping new version from version 3.0 as baseline. + # Handy to keep this \*outside\* the project's Git repository. + PYWEB\_BOOTSTRAP=/Users/slott/Documents/Projects/PyWebTool-3/pyweb/pyweb.py test : $(SOURCE) - python3 pyweb-3.0.py -xw pyweb.w + python3 $(PYWEB\_BOOTSTRAP) -xw pyweb.w cd test && python3 ../pyweb.py pyweb\_test.w - cd test && PYTHONPATH=.. python3 test.py + PYTHONPATH=${PWD} pytest cd test && rst2html.py pyweb\_test.rst pyweb\_test.html - mypy --strict pyweb.py + mypy --strict --show-error-codes pyweb.py build : pyweb.py pyweb.html - pyweb.py pyweb.html : $(SOURCE) - python3 pyweb-3.0.py pyweb.w + pyweb.py pyweb.rst : $(SOURCE) + python3 $(PYWEB\_BOOTSTRAP) pyweb.w + pyweb.html : pyweb.rst + rst2html.py $< $@ .. .. class:: small - |loz| *Makefile (184)*. + |loz| *Makefile (183)*. **TODO:** Finish ``tox.ini`` or ``pyproject.toml``. -.. _`185`: -.. rubric:: pyproject.toml (185) = +.. _`184`: +.. rubric:: pyproject.toml (184) = .. parsed-literal:: :class: code [build-system] - requires = ["setuptools >= 61.2.0", "wheel >= 0.37.1"] + requires = ["setuptools >= 61.2.0", "wheel >= 0.37.1", "pytest == 7.1.2", "mypy == 0.910"] build-backend = "setuptools.build\_meta" [tool.tox] @@ -8701,9 +8768,12 @@ Note that there are tabs in this file. We bootstrap the next version from the 3. [testenv] deps = - pytest >= 3.0.0, <4 + pytest == 7.1.2 + mypy == 0.910 + setenv = + PYWEB\_BOOTSTRAP = /Users/slott/Documents/Projects/PyWebTool-3/pyweb/pyweb.py commands\_pre = - python3 pyweb-3.0.py pyweb.w + python3 {env:PYWEB\_BOOTSTRAP} pyweb.w python3 pyweb.py -o test test/pyweb\_test.w commands = python3 test/test.py @@ -8714,7 +8784,7 @@ Note that there are tabs in this file. We bootstrap the next version from the 3. .. class:: small - |loz| *pyproject.toml (185)*. + |loz| *pyproject.toml (184)*. @@ -8728,8 +8798,8 @@ JEdit so that it properly highlights your PyWeb commands. We'll define the overall properties plus two sets of rules. -.. _`186`: -.. rubric:: jedit/pyweb.xml (186) = +.. _`185`: +.. rubric:: jedit/pyweb.xml (185) = .. parsed-literal:: :class: code @@ -8737,22 +8807,22 @@ We'll define the overall properties plus two sets of rules. - |srarr|\ props for JEdit mode (`187`_) - |srarr|\ rules for JEdit PyWeb and RST (`188`_) - |srarr|\ rules for JEdit PyWeb XML-Like Constructs (`189`_) + |srarr|\ props for JEdit mode (`186`_) + |srarr|\ rules for JEdit PyWeb and RST (`187`_) + |srarr|\ rules for JEdit PyWeb XML-Like Constructs (`188`_) .. .. class:: small - |loz| *jedit/pyweb.xml (186)*. + |loz| *jedit/pyweb.xml (185)*. Here are some properties to define RST constructs to JEdit -.. _`187`: -.. rubric:: props for JEdit mode (187) = +.. _`186`: +.. rubric:: props for JEdit mode (186) = .. parsed-literal:: :class: code @@ -8771,14 +8841,14 @@ Here are some properties to define RST constructs to JEdit .. class:: small - |loz| *props for JEdit mode (187)*. Used by: jedit/pyweb.xml (`186`_) + |loz| *props for JEdit mode (186)*. Used by: jedit/pyweb.xml (`185`_) Here are some rules to define PyWeb and RST constructs to JEdit. -.. _`188`: -.. rubric:: rules for JEdit PyWeb and RST (188) = +.. _`187`: +.. rubric:: rules for JEdit PyWeb and RST (187) = .. parsed-literal:: :class: code @@ -8919,15 +8989,15 @@ Here are some rules to define PyWeb and RST constructs to JEdit. .. class:: small - |loz| *rules for JEdit PyWeb and RST (188)*. Used by: jedit/pyweb.xml (`186`_) + |loz| *rules for JEdit PyWeb and RST (187)*. Used by: jedit/pyweb.xml (`185`_) Here are some additional rules to define PyWeb constructs to JEdit that look like XML. -.. _`189`: -.. rubric:: rules for JEdit PyWeb XML-Like Constructs (189) = +.. _`188`: +.. rubric:: rules for JEdit PyWeb XML-Like Constructs (188) = .. parsed-literal:: :class: code @@ -8943,7 +9013,7 @@ that look like XML. .. class:: small - |loz| *rules for JEdit PyWeb XML-Like Constructs (189)*. Used by: jedit/pyweb.xml (`186`_) + |loz| *rules for JEdit PyWeb XML-Like Constructs (188)*. Used by: jedit/pyweb.xml (`185`_) Additionally, you'll want to update the JEdit catalog. @@ -8970,21 +9040,27 @@ Python 3.10 Migration 1. [x] Add type hints. -#. [ ] Replace all ``.format()`` with f-strings. +#. [x] Replace all ``.format()`` with f-strings. -#. [ ] Replace filename strings (and ``os.path``) with ``pathlib.Path``. +#. [x] Replace filename strings (and ``os.path``) with ``pathlib.Path``. -#. [ ] ``pyproject.toml``. This requires -o dir to write output to a directory of choice; which requires Pathlib - -#. [ ] Introduce ``match`` statements for some of the ``elif`` blocks +#. [x] Add ``abc`` to formalize Abstract Base Classes. + +#. [x] Use ``match`` statements for some of the ``elif`` blocks. -#. [ ] Replace mock class with ``unittest.mock.Mock`` objects. +#. [ ] Introduce pytest instead of building a test runner. + +#. [ ] ``pyproject.toml``. This requires ```-o dir`` option to write output to a directory of choice; which requires ``pathlib``. + +#. [ ] Replace various mock classes with ``unittest.mock.Mock`` objects and appropriate extended testing. To Do ======= -1. Silence the logging during testing. +1. Silence the ERROR-level logging during testing. + +2. Silence the error when creating an empty file i.e. ``.nojekyll`` #. Add a JSON-based (or TOML) configuration file to configure templates. @@ -9058,11 +9134,13 @@ Changes for 3.1 - Change to Python 3.10. -- Add type hints, match statements, f-strings, pathlib. +- Add type hints, f-strings, pathlib, abc.ABC + +- Replace some complex elif blocks with match statements - Remove the Jedit configuration file as an output. -- Add a makefile and tox.ini +- Add a ``Makefile``, ``pyproject.toml``, ``requirements.txt`` and ``requirements-dev.txt``. Changes for 3.0 @@ -9202,33 +9280,33 @@ Files :.nojekyll: - |srarr|\ (`182`_) + |srarr|\ (`181`_) :MANIFEST.in: - |srarr|\ (`177`_) + |srarr|\ (`176`_) :Makefile: - |srarr|\ (`184`_) + |srarr|\ (`183`_) :README: - |srarr|\ (`179`_) + |srarr|\ (`178`_) :docutils.conf: - |srarr|\ (`180`_) + |srarr|\ (`179`_) :index.html: - |srarr|\ (`183`_) + |srarr|\ (`182`_) :jedit/pyweb.xml: - |srarr|\ (`186`_) + |srarr|\ (`185`_) :page-layout.css: - |srarr|\ (`181`_) + |srarr|\ (`180`_) :pyproject.toml: - |srarr|\ (`185`_) + |srarr|\ (`184`_) :pyweb.py: - |srarr|\ (`156`_) + |srarr|\ (`155`_) :requirements-dev.txt: - |srarr|\ (`178`_) + |srarr|\ (`177`_) :setup.py: - |srarr|\ (`176`_) + |srarr|\ (`175`_) :tangle.py: - |srarr|\ (`171`_) + |srarr|\ (`170`_) :weave.py: - |srarr|\ (`172`_) + |srarr|\ (`171`_) @@ -9237,299 +9315,295 @@ Macros :Action call method actually does the real work: - |srarr|\ (`141`_) -:Action class hierarchy - used to describe basic actions of the application: - |srarr|\ (`139`_) + |srarr|\ (`140`_) +:Action class hierarchy - used to describe actions of the application: + |srarr|\ (`138`_) :Action final summary of what was done: - |srarr|\ (`142`_) + |srarr|\ (`141`_) :Action superclass has common features of all actions: - |srarr|\ (`140`_) + |srarr|\ (`139`_) :ActionSequence append adds a new action to the sequence: - |srarr|\ (`145`_) -:ActionSequence call method delegates the sequence of ations: |srarr|\ (`144`_) -:ActionSequence subclass that holds a sequence of other actions: +:ActionSequence call method delegates the sequence of ations: |srarr|\ (`143`_) +:ActionSequence subclass that holds a sequence of other actions: + |srarr|\ (`142`_) :ActionSequence summary summarizes each step: - |srarr|\ (`146`_) + |srarr|\ (`145`_) :Application Class: - |srarr|\ (`162`_) |srarr|\ (`163`_) + |srarr|\ (`161`_) |srarr|\ (`162`_) :Application class process all files: - |srarr|\ (`166`_) + |srarr|\ (`165`_) :Application default options: - |srarr|\ (`164`_) + |srarr|\ (`163`_) :Application parse command line: - |srarr|\ (`165`_) + |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|\ (`58`_) -:Chunk generate references from this Chunk: |srarr|\ (`59`_) +:Chunk generate references from this Chunk: + |srarr|\ (`60`_) :Chunk indent adjustments: - |srarr|\ (`63`_) + |srarr|\ (`64`_) :Chunk references to this Chunk: - |srarr|\ (`60`_) + |srarr|\ (`61`_) :Chunk superclass make Content definition: - |srarr|\ (`56`_) + |srarr|\ (`57`_) :Chunk tangle this Chunk into a code file: - |srarr|\ (`62`_) + |srarr|\ (`63`_) :Chunk weave this Chunk into the documentation: - |srarr|\ (`61`_) + |srarr|\ (`62`_) :CodeCommand class to contain a program source code block: - |srarr|\ (`82`_) + |srarr|\ (`83`_) :Command analysis features: starts-with and Regular Expression search: - |srarr|\ (`79`_) + |srarr|\ (`80`_) :Command class hierarchy - used to describe individual commands: - |srarr|\ (`77`_) -:Command superclass: |srarr|\ (`78`_) +:Command superclass: + |srarr|\ (`79`_) :Command tangle and weave functions: - |srarr|\ (`80`_) + |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|\ (`95`_) + |srarr|\ (`96`_) :FileXrefCommand class for an output file cross-reference: - |srarr|\ (`84`_) + |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|\ (`57`_) |srarr|\ (`97`_) |srarr|\ (`125`_) |srarr|\ (`130`_) |srarr|\ (`133`_) |srarr|\ (`135`_) |srarr|\ (`157`_) |srarr|\ (`161`_) |srarr|\ (`167`_) + |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|\ (`170`_) + |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|\ (`154`_) -:LoadAction subclass loads the document web: |srarr|\ (`153`_) +:LoadAction subclass loads the document web: + |srarr|\ (`152`_) :LoadAction summary provides lines read: - |srarr|\ (`155`_) + |srarr|\ (`154`_) :Logging Setup: - |srarr|\ (`168`_) |srarr|\ (`169`_) + |srarr|\ (`167`_) |srarr|\ (`168`_) :MacroXrefCommand class for a named chunk cross-reference: - |srarr|\ (`85`_) + |srarr|\ (`86`_) :NamedChunk add to the web: - |srarr|\ (`66`_) -:NamedChunk class: - |srarr|\ (`64`_) |srarr|\ (`69`_) + |srarr|\ (`67`_) +:NamedChunk class for defined names: + |srarr|\ (`65`_) |srarr|\ (`70`_) :NamedChunk tangle into the source file: - |srarr|\ (`68`_) + |srarr|\ (`69`_) :NamedChunk user identifiers set and get: - |srarr|\ (`65`_) + |srarr|\ (`66`_) :NamedChunk weave into the documentation: - |srarr|\ (`67`_) + |srarr|\ (`68`_) :NamedDocumentChunk class: - |srarr|\ (`74`_) + |srarr|\ (`75`_) :NamedDocumentChunk tangle: - |srarr|\ (`76`_) + |srarr|\ (`77`_) :NamedDocumentChunk weave: - |srarr|\ (`75`_) + |srarr|\ (`76`_) :Option Parser class - locates optional values on commands: - |srarr|\ (`136`_) |srarr|\ (`137`_) |srarr|\ (`138`_) + |srarr|\ (`135`_) |srarr|\ (`136`_) |srarr|\ (`137`_) :OutputChunk add to the web: - |srarr|\ (`71`_) + |srarr|\ (`72`_) :OutputChunk class: - |srarr|\ (`70`_) + |srarr|\ (`71`_) :OutputChunk tangle: - |srarr|\ (`73`_) + |srarr|\ (`74`_) :OutputChunk weave: - |srarr|\ (`72`_) + |srarr|\ (`73`_) :Overheads: - |srarr|\ (`158`_) |srarr|\ (`159`_) |srarr|\ (`160`_) + |srarr|\ (`157`_) |srarr|\ (`158`_) |srarr|\ (`159`_) :RST subclass of Weaver: - |srarr|\ (`22`_) + |srarr|\ (`23`_) :Reference class hierarchy - strategies for references to a chunk: - |srarr|\ (`92`_) |srarr|\ (`93`_) |srarr|\ (`94`_) + |srarr|\ (`93`_) |srarr|\ (`94`_) |srarr|\ (`95`_) :ReferenceCommand class for chunk references: - |srarr|\ (`87`_) + |srarr|\ (`88`_) :ReferenceCommand refers to a chunk: - |srarr|\ (`89`_) + |srarr|\ (`90`_) :ReferenceCommand resolve a referenced chunk name: - |srarr|\ (`88`_) + |srarr|\ (`89`_) :ReferenceCommand tangle a referenced chunk: - |srarr|\ (`91`_) + |srarr|\ (`92`_) :ReferenceCommand weave a reference to a chunk: - |srarr|\ (`90`_) + |srarr|\ (`91`_) :TangleAction call method does tangling of the output files: - |srarr|\ (`151`_) -:TangleAction subclass initiates the tangle action: |srarr|\ (`150`_) +:TangleAction subclass initiates the tangle action: + |srarr|\ (`149`_) :TangleAction summary method provides total lines tangled: - |srarr|\ (`152`_) + |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|\ (`81`_) + |srarr|\ (`82`_) :Tokenizer class - breaks input into tokens: - |srarr|\ (`134`_) + |srarr|\ (`133`_) :UserIdXrefCommand class for a user identifier cross-reference: - |srarr|\ (`86`_) + |srarr|\ (`87`_) :WeaveAction call method to pick the language: - |srarr|\ (`148`_) -:WeaveAction subclass initiates the weave action: |srarr|\ (`147`_) +:WeaveAction subclass initiates the weave action: + |srarr|\ (`146`_) :WeaveAction summary of language choice: - |srarr|\ (`149`_) + |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|\ (`106`_) + |srarr|\ (`107`_) :Web Chunk cross reference methods: - |srarr|\ (`105`_) |srarr|\ (`107`_) |srarr|\ (`108`_) |srarr|\ (`109`_) + |srarr|\ (`106`_) |srarr|\ (`108`_) |srarr|\ (`109`_) |srarr|\ (`110`_) :Web Chunk name resolution methods: - |srarr|\ (`103`_) |srarr|\ (`104`_) + |srarr|\ (`104`_) |srarr|\ (`105`_) :Web add a named macro chunk: - |srarr|\ (`101`_) + |srarr|\ (`102`_) :Web add an anonymous chunk: - |srarr|\ (`100`_) + |srarr|\ (`101`_) :Web add an output file definition chunk: - |srarr|\ (`102`_) + |srarr|\ (`103`_) :Web add full chunk names, ignoring abbreviated names: - |srarr|\ (`99`_) + |srarr|\ (`100`_) :Web class - describes the overall "web" of chunks: - |srarr|\ (`96`_) + |srarr|\ (`97`_) :Web construction methods used by Chunks and WebReader: - |srarr|\ (`98`_) + |srarr|\ (`99`_) :Web determination of the language from the first chunk: - |srarr|\ (`112`_) -:Web tangle the output files: |srarr|\ (`113`_) -:Web weave the output document: +:Web tangle the output files: |srarr|\ (`114`_) -:WebReader class - parses the input file, building the Web structure: +:Web weave the output document: |srarr|\ (`115`_) +:WebReader class - parses the input file, building the Web structure: + |srarr|\ (`116`_) :WebReader command literals: - |srarr|\ (`132`_) + |srarr|\ (`131`_) :WebReader handle a command string: - |srarr|\ (`116`_) |srarr|\ (`128`_) + |srarr|\ (`117`_) |srarr|\ (`127`_) :WebReader load the web: - |srarr|\ (`131`_) + |srarr|\ (`130`_) :WebReader location in the input stream: - |srarr|\ (`129`_) + |srarr|\ (`128`_) :XrefCommand superclass for all cross-reference commands: - |srarr|\ (`83`_) + |srarr|\ (`84`_) :add a reference command to the current chunk: - |srarr|\ (`124`_) + |srarr|\ (`123`_) :add an expression command to the current chunk: - |srarr|\ (`126`_) + |srarr|\ (`125`_) :assign user identifiers to the current chunk: - |srarr|\ (`123`_) + |srarr|\ (`122`_) :collect all user identifiers from a given map into ux: - |srarr|\ (`110`_) + |srarr|\ (`111`_) :double at-sign replacement, append this character to previous TextCommand: - |srarr|\ (`127`_) + |srarr|\ (`126`_) :find user identifier usage and update ux from the given map: - |srarr|\ (`111`_) + |srarr|\ (`112`_) :finish a chunk, start a new Chunk adding it to the web: |srarr|\ (`121`_) -:import another file: +:include another file: |srarr|\ (`120`_) -:major commands segment the input into separate Chunks: - |srarr|\ (`117`_) -:minor commands add Commands to the current Chunk: - |srarr|\ (`122`_) :props for JEdit mode: - |srarr|\ (`187`_) + |srarr|\ (`186`_) :rules for JEdit PyWeb XML-Like Constructs: - |srarr|\ (`189`_) -:rules for JEdit PyWeb and RST: |srarr|\ (`188`_) +:rules for JEdit PyWeb and RST: + |srarr|\ (`187`_) :start a NamedChunk or NamedDocumentChunk, adding it to the web: |srarr|\ (`119`_) :start an OutputChunk, adding it to the web: |srarr|\ (`118`_) :weave.py custom weaver definition to customize the Weaver being used: - |srarr|\ (`174`_) -:weave.py overheads for correct operation of a script: |srarr|\ (`173`_) +:weave.py overheads for correct operation of a script: + |srarr|\ (`172`_) :weaver.py processing: load and weave the document: - |srarr|\ (`175`_) + |srarr|\ (`174`_) @@ -9538,239 +9612,247 @@ User Identifiers :Action: - [`140`_] `143`_ `145`_ `147`_ `150`_ `153`_ + [`139`_] `142`_ `144`_ `146`_ `149`_ `152`_ :ActionSequence: - [`143`_] `164`_ + [`142`_] `163`_ :Application: - [`162`_] `170`_ + [`161`_] `169`_ :Chunk: - `15`_ `16`_ `17`_ `18`_ `45`_ `46`_ [`52`_] `58`_ `59`_ `64`_ `78`_ `87`_ `91`_ `92`_ `93`_ `94`_ `96`_ `100`_ `101`_ `102`_ `104`_ `105`_ `109`_ `115`_ `120`_ `121`_ `124`_ `131`_ + `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: - `64`_ [`82`_] + `65`_ [`83`_] :Command: - `52`_ `53`_ `56`_ `64`_ `74`_ [`78`_] `81`_ `83`_ `87`_ `166`_ + `53`_ `54`_ `57`_ `65`_ `75`_ [`79`_] `82`_ `84`_ `88`_ `165`_ :Emitter: - [`3`_] `4`_ `12`_ `43`_ + [`4`_] `5`_ `13`_ `44`_ :Error: - `59`_ `62`_ `68`_ `76`_ `83`_ `91`_ [`95`_] `101`_ `103`_ `104`_ `114`_ `119`_ `120`_ `126`_ `138`_ `148`_ `151`_ `154`_ `165`_ + `60`_ `63`_ `69`_ `77`_ `84`_ `92`_ [`96`_] `102`_ `104`_ `105`_ `115`_ `119`_ `120`_ `125`_ `137`_ `147`_ `150`_ `153`_ `164`_ :FileXrefCommand: - [`84`_] `122`_ + [`85`_] `117`_ :HTML: - `31`_ [`32`_] `112`_ `163`_ `174`_ + `32`_ [`33`_] `113`_ `162`_ `173`_ :LaTeX: - [`23`_] `112`_ `163`_ + [`24`_] `113`_ `162`_ :LoadAction: - [`153`_] `164`_ `171`_ `175`_ + [`152`_] `163`_ `170`_ `174`_ :MacroXrefCommand: - [`85`_] `122`_ + [`86`_] `117`_ :NamedChunk: - `58`_ [`64`_] `69`_ `70`_ `74`_ `119`_ + `59`_ [`65`_] `70`_ `71`_ `75`_ `119`_ :NamedDocumentChunk: - [`74`_] `119`_ + [`75`_] `119`_ :OutputChunk: - [`70`_] `118`_ + [`71`_] `118`_ +:Path: + [`3`_] `4`_ `5`_ `6`_ `14`_ `45`_ `50`_ `53`_ `97`_ `114`_ `116`_ `120`_ `130`_ `164`_ :ReferenceCommand: - [`87`_] `124`_ + [`88`_] `123`_ :TangleAction: - [`150`_] `164`_ `171`_ + [`149`_] `163`_ `170`_ :Tangler: - `3`_ [`43`_] `48`_ `62`_ `63`_ `68`_ `69`_ `73`_ `76`_ `80`_ `81`_ `82`_ `83`_ `91`_ `113`_ `165`_ + `4`_ [`44`_] `49`_ `63`_ `64`_ `69`_ `70`_ `74`_ `77`_ `81`_ `82`_ `83`_ `84`_ `92`_ `114`_ `164`_ :TanglerMake: - [`48`_] `165`_ `169`_ `171`_ `175`_ + [`49`_] `164`_ `168`_ `170`_ `174`_ :TextCommand: - `54`_ `56`_ `68`_ `74`_ [`81`_] `82`_ + `55`_ `57`_ `69`_ `75`_ [`82`_] `83`_ :Tokenizer: - `115`_ `131`_ [`134`_] + `116`_ `130`_ [`133`_] :UserIdXrefCommand: - [`86`_] `122`_ + [`87`_] `117`_ :WeaveAction: - [`147`_] `164`_ `175`_ + [`146`_] `163`_ `174`_ :Weaver: - [`12`_] `22`_ `23`_ `31`_ `60`_ `61`_ `67`_ `72`_ `75`_ `80`_ `81`_ `82`_ `83`_ `84`_ `85`_ `86`_ `90`_ `112`_ `114`_ `165`_ `166`_ + [`13`_] `23`_ `24`_ `32`_ `61`_ `62`_ `68`_ `73`_ `76`_ `81`_ `82`_ `83`_ `84`_ `85`_ `86`_ `87`_ `91`_ `113`_ `115`_ `164`_ `165`_ :Web: - `45`_ `52`_ `55`_ `59`_ `61`_ `62`_ `63`_ `66`_ `67`_ `68`_ `69`_ `71`_ `72`_ `73`_ `75`_ `76`_ `80`_ `81`_ `82`_ `83`_ `84`_ `85`_ `86`_ `88`_ `89`_ `90`_ `91`_ [`96`_] `115`_ `131`_ `140`_ `154`_ `166`_ `171`_ `175`_ `179`_ + `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`_ `178`_ :WebReader: - [`115`_] `120`_ `131`_ `165`_ `169`_ `171`_ `175`_ + [`116`_] `120`_ `130`_ `164`_ `168`_ `170`_ `174`_ :XrefCommand: - [`83`_] `84`_ `85`_ `86`_ + [`84`_] `85`_ `86`_ `87`_ +:__enter__: + [`5`_] `167`_ +:__exit__: + [`5`_] `167`_ :__version__: - `126`_ [`160`_] + `125`_ [`159`_] :_gatherUserId: - [`109`_] + [`110`_] :_updateUserId: - [`109`_] + [`110`_] :add: - `55`_ [`100`_] + `56`_ [`101`_] :addDefName: - [`99`_] `101`_ `124`_ + [`100`_] `102`_ `123`_ :addIndent: - `10`_ [`13`_] `63`_ `67`_ + `11`_ [`14`_] `64`_ `68`_ :addNamed: - `66`_ [`101`_] + `67`_ [`102`_] :addOutput: - `71`_ [`102`_] + `72`_ [`103`_] :append: - `10`_ `13`_ `53`_ `54`_ `94`_ `100`_ `101`_ `102`_ `105`_ `111`_ `122`_ `124`_ `138`_ [`145`_] + `11`_ `14`_ `54`_ `55`_ `95`_ `101`_ `102`_ `103`_ `106`_ `112`_ `117`_ `123`_ `137`_ [`144`_] :appendText: - [`54`_] `124`_ `126`_ `127`_ `131`_ + [`55`_] `123`_ `125`_ `126`_ `130`_ :argparse: - `140`_ [`161`_] `164`_ `165`_ `166`_ `171`_ `173`_ `175`_ + `139`_ [`160`_] `163`_ `164`_ `165`_ `170`_ `172`_ `174`_ :builtins: - [`125`_] `126`_ + [`124`_] `125`_ :chunkXref: - `85`_ [`108`_] + `86`_ [`109`_] :close: - [`4`_] `13`_ `44`_ `50`_ + [`5`_] `14`_ `45`_ `51`_ :clrIndent: - [`10`_] `63`_ `67`_ `69`_ + [`11`_] `64`_ `68`_ `70`_ :codeBegin: - `17`_ [`45`_] `67`_ `68`_ + `18`_ [`46`_] `68`_ `69`_ :codeBlock: - [`7`_] `67`_ `82`_ + [`8`_] `68`_ `83`_ :codeEnd: - `17`_ [`46`_] `67`_ `68`_ + `18`_ [`47`_] `68`_ `69`_ :codeFinish: - `4`_ `9`_ [`13`_] + `5`_ `10`_ [`14`_] :createUsedBy: - [`105`_] `154`_ + [`106`_] `153`_ :datetime: - `126`_ [`157`_] + `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`_] `61`_ + [`16`_] `62`_ :docEnd: - [`15`_] `61`_ + [`16`_] `62`_ :duration: - [`142`_] `149`_ `152`_ `155`_ + [`141`_] `148`_ `151`_ `154`_ :expand: - `75`_ `124`_ `164`_ [`165`_] + `76`_ `123`_ `163`_ [`164`_] :expect: - `118`_ `119`_ `124`_ `126`_ [`128`_] + `118`_ `119`_ `123`_ `125`_ [`127`_] :fileBegin: - `18`_ [`35`_] `72`_ + `19`_ [`36`_] `73`_ :fileEnd: - `18`_ [`36`_] `72`_ + `19`_ [`37`_] `73`_ :fileXref: - `84`_ [`108`_] + `85`_ [`109`_] :filecmp: - [`47`_] `50`_ + [`48`_] `51`_ :formatXref: - [`83`_] `84`_ `85`_ + [`84`_] `85`_ `86`_ :fullNameFor: - `67`_ `72`_ `88`_ `99`_ [`103`_] `104`_ `105`_ + `68`_ `73`_ `89`_ `100`_ [`104`_] `105`_ `106`_ :genReferences: - [`59`_] `105`_ + [`60`_] `106`_ :getUserIDRefs: - `58`_ [`65`_] `110`_ + `59`_ [`66`_] `111`_ :getchunk: - `88`_ [`104`_] `105`_ `114`_ + `89`_ [`105`_] `106`_ `115`_ :handleCommand: - [`116`_] `131`_ + [`117`_] `130`_ :language: - [`112`_] `148`_ `159`_ `179`_ + [`113`_] `147`_ `158`_ `178`_ :lineNumber: - `17`_ `18`_ `33`_ `35`_ `45`_ `54`_ `56`_ [`58`_] `64`_ `68`_ `74`_ `78`_ `81`_ `83`_ `87`_ `120`_ `122`_ `124`_ `126`_ `127`_ `129`_ `131`_ `134`_ `174`_ + `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: - `120`_ [`131`_] `154`_ `164`_ `166`_ + `120`_ [`130`_] `153`_ `163`_ `165`_ :location: - `116`_ `123`_ `126`_ `128`_ [`129`_] + `117`_ `122`_ `125`_ `127`_ [`128`_] :logging: - `3`_ `52`_ `78`_ `92`_ `96`_ `115`_ `140`_ `162`_ `164`_ `165`_ `166`_ [`167`_] `168`_ `169`_ `171`_ `173`_ `175`_ + `4`_ `53`_ `79`_ `93`_ `97`_ `116`_ `139`_ `161`_ `163`_ `164`_ `165`_ [`166`_] `167`_ `168`_ `170`_ `172`_ `174`_ :logging.config: - [`167`_] `168`_ + [`166`_] `167`_ :main: - [`170`_] + [`169`_] :makeContent: - `54`_ [`56`_] `64`_ `74`_ + `55`_ [`57`_] `65`_ `75`_ :multi_reference: - `106`_ [`107`_] + `107`_ [`108`_] :no_definition: - `106`_ [`107`_] + `107`_ [`108`_] :no_reference: - `106`_ [`107`_] + `107`_ [`108`_] :open: - [`4`_] `13`_ `44`_ `113`_ `114`_ `126`_ `131`_ + [`5`_] `14`_ `45`_ `114`_ `115`_ `125`_ `130`_ :os: - `44`_ `49`_ `50`_ `114`_ `126`_ [`157`_] + `50`_ `51`_ `125`_ [`156`_] :parse: - `118`_ `119`_ [`131`_] `138`_ + `118`_ `119`_ [`130`_] `137`_ :parseArgs: - [`165`_] `170`_ + [`164`_] `169`_ :perform: - [`154`_] + [`153`_] :platform: - [`125`_] `126`_ + [`124`_] `125`_ :process: - `126`_ [`166`_] `170`_ + `125`_ [`165`_] `169`_ :quote: - [`8`_] `82`_ + [`9`_] `83`_ :quoted_chars: - `8`_ `14`_ `29`_ [`38`_] + `9`_ `15`_ `30`_ [`39`_] :re: - `111`_ [`133`_] `134`_ `179`_ + `112`_ [`132`_] `133`_ `178`_ :readdIndent: - `3`_ [`10`_] `13`_ + `4`_ [`11`_] `14`_ :ref: - `28`_ `59`_ [`80`_] `89`_ `100`_ `101`_ `102`_ + `29`_ `60`_ [`81`_] `90`_ `101`_ `102`_ `103`_ :referenceSep: - [`19`_] `114`_ + [`20`_] `115`_ :referenceTo: - `19`_ `20`_ [`39`_] `67`_ + `20`_ `21`_ [`40`_] `68`_ :references: - `16`_ `17`_ `18`_ `19`_ `25`_ `32`_ `34`_ `36`_ [`42`_] `52`_ `59`_ `60`_ `106`_ `123`_ `159`_ `164`_ `174`_ + `17`_ `18`_ `19`_ `20`_ `26`_ `33`_ `35`_ `37`_ [`43`_] `53`_ `60`_ `61`_ `107`_ `122`_ `158`_ `163`_ `173`_ :resolve: - `68`_ [`88`_] `89`_ `90`_ `91`_ `104`_ + `69`_ [`89`_] `90`_ `91`_ `92`_ `105`_ :searchForRE: - `58`_ [`79`_] `81`_ `111`_ + `59`_ [`80`_] `82`_ `112`_ +:setIndent: + [`11`_] `70`_ :setUserIDRefs: - `58`_ [`65`_] `123`_ + `59`_ [`66`_] `122`_ :shlex: - [`135`_] `138`_ + [`134`_] `137`_ :startswith: - `58`_ [`79`_] `81`_ `103`_ `112`_ `131`_ `138`_ `166`_ + `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`_ `173`_ `174`_ + [`12`_] `17`_ `18`_ `19`_ `20`_ `21`_ `22`_ `25`_ `26`_ `29`_ `31`_ `34`_ `35`_ `36`_ `37`_ `38`_ `40`_ `41`_ `42`_ `43`_ `172`_ `173`_ :summary: - `142`_ `146`_ `149`_ `152`_ [`155`_] `166`_ `171`_ `175`_ + `141`_ `145`_ `148`_ `151`_ [`154`_] `165`_ `170`_ `174`_ :sys: - [`125`_] `126`_ `169`_ `170`_ + [`124`_] `125`_ `168`_ `169`_ :tangle: - `45`_ `62`_ `68`_ `70`_ `73`_ `74`_ `76`_ `80`_ `81`_ `82`_ `83`_ `91`_ [`113`_] `151`_ `164`_ `171`_ `179`_ + `46`_ `63`_ `69`_ `71`_ `74`_ `75`_ `77`_ `81`_ `82`_ `83`_ `84`_ `92`_ [`114`_] `150`_ `163`_ `170`_ `178`_ :tempfile: - [`47`_] `49`_ + [`48`_] `50`_ :time: - `126`_ `141`_ `142`_ [`157`_] + `125`_ `140`_ `141`_ [`156`_] :types: - `12`_ `126`_ [`157`_] + `13`_ `125`_ [`156`_] :usedBy: - [`89`_] + [`90`_] :userNamesXref: - `86`_ [`109`_] + `87`_ [`110`_] :weakref: - `52`_ [`97`_] `100`_ `101`_ `102`_ + `53`_ [`98`_] `101`_ `102`_ `103`_ :weave: - `61`_ `67`_ `72`_ `75`_ `80`_ `81`_ `82`_ `84`_ `85`_ `86`_ `90`_ [`114`_] `148`_ `164`_ `173`_ `179`_ + `62`_ `68`_ `73`_ `76`_ `81`_ `82`_ `83`_ `85`_ `86`_ `87`_ `91`_ [`115`_] `147`_ `163`_ `172`_ `178`_ :weaveChunk: - `90`_ [`114`_] + `91`_ [`115`_] :weaveReferenceTo: - `61`_ `67`_ [`75`_] `114`_ + `62`_ `68`_ [`76`_] `115`_ :weaveShortReferenceTo: - `61`_ `67`_ [`75`_] `114`_ + `62`_ `68`_ [`76`_] `115`_ :webAdd: - `55`_ `66`_ [`71`_] `118`_ `119`_ `120`_ `121`_ `131`_ + `56`_ `67`_ [`72`_] `118`_ `119`_ `120`_ `121`_ `130`_ :write: - [`4`_] `7`_ `9`_ `17`_ `18`_ `20`_ `21`_ `45`_ `81`_ `114`_ + [`5`_] `8`_ `10`_ `18`_ `19`_ `21`_ `22`_ `46`_ `82`_ `115`_ :xrefDefLine: - `21`_ [`41`_] `86`_ + `22`_ [`42`_] `87`_ :xrefFoot: - `20`_ [`40`_] `83`_ `86`_ + `21`_ [`41`_] `84`_ `87`_ :xrefHead: - `20`_ [`40`_] `83`_ `86`_ + `21`_ [`41`_] `84`_ `87`_ :xrefLine: - `20`_ [`40`_] `83`_ + `21`_ [`41`_] `84`_ @@ -9779,9 +9861,9 @@ User Identifiers .. class:: small - Created by pyweb-3.0.py at Fri Jun 10 08:31:18 2022. + Created by /Users/slott/Documents/Projects/PyWebTool-3/pyweb/pyweb.py at Fri Jun 10 16:54:51 2022. - Source pyweb.w modified Wed Jun 8 14:04:44 2022. + Source pyweb.w modified Fri Jun 10 10:48:04 2022. pyweb.__version__ '3.0'. diff --git a/requirements-dev.txt b/requirements-dev.txt index 88f7bc9..8eaeb4b 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -2,4 +2,4 @@ docutils==0.18.1 tox==3.25.0 mypy==0.910 -pytest == 7.1.2 +pytest==7.1.2 diff --git a/setup.py b/setup.py index 2fb0505..9ba9d37 100644 --- a/setup.py +++ b/setup.py @@ -5,15 +5,15 @@ setup(name='py-web-tool', version='3.1', - description='pyWeb 3.1: Yet Another Literate Programming Tool', + 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'], 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/test/func.w b/test/func.w index 758e3d9..b124ea5 100644 --- a/test/func.w +++ b/test/func.w @@ -29,14 +29,14 @@ We need to be able to load a web from one or more source files. 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 +``file_path``, we can simply provide a file-like input object to the ``WebReader`` instance. @d Load Test superclass... @{ class ParseTestcase(unittest.TestCase): text = "" - file_name = "" + file_path: Path def setUp(self) -> None: self.source = io.StringIO(self.text) self.web = pyweb.Web() @@ -50,6 +50,7 @@ find an expected next token. @d Load Test overheads... @{ import logging.handlers +from pathlib import Path @} @d Load Test error handling... @@ -58,7 +59,7 @@ import logging.handlers class Test_ParseErrors(ParseTestcase): text = test1_w - file_name = "test1.w" + file_path = Path("test1.w") def setUp(self) -> None: super().setUp() self.logger = logging.getLogger("WebReader") @@ -67,7 +68,7 @@ class Test_ParseErrors(ParseTestcase): self.logger.addHandler(self.buffer) self.logger.setLevel(logging.WARN) def test_error_should_count_1(self) -> None: - self.rdr.load(self.web, self.file_name, self.source) + self.rdr.load(self.web, self.file_path, self.source) self.assertEqual(3, self.rdr.errors) messages = [r.message for r in self.buffer.buffer] self.assertEqual( @@ -111,18 +112,17 @@ create a temporary file. It's hard to mock the include processing. class Test_IncludeParseErrors(ParseTestcase): text = test8_w - file_name = "test8.w" + file_path = Path("test8.w") def setUp(self) -> None: - with open('test8_inc.tmp','w') as temp: - temp.write(test8_inc_w) super().setUp() + Path('test8_inc.tmp').write_text(test8_inc_w) self.logger = logging.getLogger("WebReader") self.buffer = logging.handlers.BufferingHandler(12) self.buffer.setLevel(logging.WARN) self.logger.addHandler(self.buffer) self.logger.setLevel(logging.WARN) def test_error_should_count_2(self) -> None: - self.rdr.load(self.web, self.file_name, self.source) + self.rdr.load(self.web, self.file_path, self.source) self.assertEqual(1, self.rdr.errors) messages = [r.message for r in self.buffer.buffer] self.assertEqual( @@ -133,7 +133,7 @@ class Test_IncludeParseErrors(ParseTestcase): def tearDown(self) -> None: self.logger.setLevel(logging.CRITICAL) self.logger.removeHandler(self.buffer) - os.remove('test8_inc.tmp') + Path('test8_inc.tmp').unlink() super().tearDown() @} @@ -160,12 +160,15 @@ And now for an error - incorrect syntax in an included file! @d Load Test overheads... @{ """Loader and parsing tests.""" -import pyweb -import unittest +import io import logging import os -import io +from pathlib import Path +import string import types +import unittest + +import pyweb @} A main program that configures logging and then runs the test. @@ -205,8 +208,8 @@ exceptions raised. @{ class TangleTestcase(unittest.TestCase): text = "" - file_name = "" error = "" + file_path: Path def setUp(self) -> None: self.source = io.StringIO(self.text) self.web = pyweb.Web() @@ -214,18 +217,17 @@ class TangleTestcase(unittest.TestCase): self.tangler = pyweb.Tangler() def tangle_and_check_exception(self, exception_text: str) -> None: try: - self.rdr.load(self.web, self.file_name, self.source) + 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: - name, _ = os.path.splitext(self.file_name) try: - os.remove(name + ".tmp") - except OSError: - pass + self.file_path.with_suffix(".tmp").unlink() + except FileNotFoundError: + pass # If the test fails, nothing to remove... @} @d Tangle Test semantic error 2... @@ -234,7 +236,7 @@ class TangleTestcase(unittest.TestCase): class Test_SemanticError_2(TangleTestcase): text = test2_w - file_name = "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.") @} @@ -256,7 +258,7 @@ Okay, now for some errors: no part2! class Test_SemanticError_3(TangleTestcase): text = test3_w - file_name = "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.") @} @@ -280,7 +282,7 @@ Okay, now for some errors: attempt to tangle a cross-reference! class Test_SemanticError_4(TangleTestcase): text = test4_w - file_name = "test4.w" + file_path = Path("test4.w") def test_should_raise_noFullName(self) -> None: self.tangle_and_check_exception("No full name for 'part1...'") @} @@ -303,7 +305,7 @@ Okay, now for some errors: attempt to weave but no full name for part1.... class Test_SemanticError_5(TangleTestcase): text = test5_w - file_name = "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']") @} @@ -328,9 +330,9 @@ Okay, now for some errors: part1... is ambiguous class Test_SemanticError_6(TangleTestcase): text = test6_w - file_name = "test6.w" + file_path = Path("test6.w") def test_should_warn(self) -> None: - self.rdr.load(self.web, self.file_name, self.source) + 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())) @@ -358,19 +360,18 @@ Okay, now for some warnings: class Test_IncludeError_7(TangleTestcase): text = test7_w - file_name = "test7.w" + file_path = Path("test7.w") def setUp(self) -> None: - with open('test7_inc.tmp','w') as temp: - temp.write(test7_inc_w) + Path('test7_inc.tmp').write_text(test7_inc_w) super().setUp() def test_should_include(self) -> None: - self.rdr.load(self.web, self.file_name, self.source) + 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: - os.remove('test7_inc.tmp') + Path('test7_inc.tmp').unlink() super().tearDown() @} @@ -390,11 +391,13 @@ test7_inc_w = """The test7a.tmp chunk for test7.w @d Tangle Test overheads... @{ """Tangler tests exercise various semantic features.""" -import pyweb -import unittest +import io import logging import os -import io +from pathlib import Path +import unittest + +import pyweb @} @d Tangle Test main program... @@ -424,26 +427,25 @@ Weaving test cases have a common setup shown in this superclass. @d Weave Test superclass... @{ class WeaveTestcase(unittest.TestCase): text = "" - file_name = "" error = "" + file_path: Path def setUp(self) -> None: self.source = io.StringIO(self.text) self.web = pyweb.Web() self.rdr = pyweb.WebReader() def tangle_and_check_exception(self, exception_text: str) -> None: try: - self.rdr.load(self.web, self.file_name, self.source) + 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: - name, _ = os.path.splitext(self.file_name) try: - os.remove(name + ".html") - except OSError: - pass + self.file_path.with_suffix(".html").unlink() + except FileNotFoundError: + pass # if the test failed, nothing to remove @} @d Weave Test references... @{ @@ -452,17 +454,16 @@ class WeaveTestcase(unittest.TestCase): class Test_RefDefWeave(WeaveTestcase): text = test0_w - file_name = "test0.w" + file_path = Path("test0.w") def test_load_should_createChunks(self) -> None: - self.rdr.load(self.web, self.file_name, self.source) + 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_name, self.source) + self.rdr.load(self.web, self.file_path, self.source) doc = pyweb.HTML() doc.reference_style = pyweb.SimpleReference() self.web.weave(doc) - with open("test0.html","r") as source: - actual = source.read() + actual = self.file_path.with_suffix(".html").read_text() self.maxDiff = None self.assertEqual(test0_expected, actual) @@ -534,20 +535,19 @@ to properly provide a consistent output from ``time.asctime()``. class TestEvaluations(WeaveTestcase): text = test9_w - file_name = "test9.w" + file_path = Path("test9.w") def test_should_evaluate(self) -> None: - self.rdr.load(self.web, self.file_name, self.source) + self.rdr.load(self.web, self.file_path, self.source) doc = pyweb.HTML( ) doc.reference_style = pyweb.SimpleReference() self.web.weave(doc) - with open("test9.html","r") as source: - actual = source.readlines() + actual = self.file_path.with_suffix(".html").read_text().splitlines() #print(actual) - self.assertEqual("An anonymous chunk.\n", actual[0]) + self.assertEqual("An anonymous chunk.", actual[0]) self.assertTrue(actual[1].startswith("Time =")) - self.assertEqual("File = ('test9.w', 3)\n", actual[2]) - self.assertEqual('Version = 3.1\n', actual[3]) - self.assertEqual(f'CWD = {os.getcwd()}\n', actual[4]) + 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... @@ -563,12 +563,14 @@ CWD = @@(os.path.realpath('.')@@) @d Weave Test overheads... @{ """Weaver tests exercise various weaving features.""" -import pyweb -import unittest +import io import logging import os +from pathlib import Path import string -import io +import unittest + +import pyweb @} @d Weave Test main program... diff --git a/test/pyweb_test.html b/test/pyweb_test.html index e4f8047..f021b80 100644 --- a/test/pyweb_test.html +++ b/test/pyweb_test.html @@ -417,7 +417,7 @@

    Yet Another Lite
  • Tests for Weaving
  • -
  • Combined Test Script
  • +
  • Combined Test Runner
  • Additional Files
  • Indices
    • Files
    • @@ -672,15 +672,14 @@

      Emitter Tests

      def setUp(self) -> None: self.weaver = pyweb.Weaver() self.weaver.reference_style = pyweb.SimpleReference() - self.filename = "testweaver" + 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.referencedBy = [self.aFileChunk] def tearDown(self) -> None: - import os try: - pass #os.remove("testweaver.rst") + self.filepath.unlink() except OSError: pass @@ -693,48 +692,44 @@

      Emitter Tests

      self.assertEqual(r"|srarr|\ Chunk (`314`_)", result) def test_weaver_should_codeBegin(self) -> None: - self.weaver.open(self.filename) + 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() - with open("testweaver.rst", "r") as result: - txt = result.read() + 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.filename) + 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() - with open("testweaver.rst", "r") as result: - txt = result.read() + 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.filename) + 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() - with open("testweaver.rst", "r") as result: - txt = result.read() + 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.filename) + 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() - with open("testweaver.rst", "r") as result: - txt = result.read() + txt = self.filepath.with_suffix(".rst").read_text() self.assertEqual("\n:Chunk:\n `123`_ [`314`_] `567`_\n\n", txt) @@ -752,15 +747,14 @@

      Emitter Tests

      def setUp(self) -> None: self.weaver = pyweb.LaTeX() self.weaver.reference_style = pyweb.SimpleReference() - self.filename = "testweaver" + 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.referencedBy = [self.aFileChunk,] def tearDown(self) -> None: - import os try: - os.remove("testweaver.tex") + self.filepath.with_suffix(".tex").unlink() except OSError: pass @@ -783,18 +777,18 @@

      Emitter Tests

      def setUp(self) -> None: self.weaver = pyweb.HTML( ) self.weaver.reference_style = pyweb.SimpleReference() - self.filename = "testweaver" + 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.referencedBy = [self.aFileChunk,] def tearDown(self) -> None: - import os try: - os.remove("testweaver.html") + 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) @@ -825,16 +819,15 @@

      Emitter Tests

      class TestTangler(unittest.TestCase): def setUp(self) -> None: self.tangler = pyweb.Tangler() - self.filename = "testtangler.code" + 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: - import os try: - os.remove("testtangler.code") - except OSError: + self.filepath.unlink() + except FileNotFoundError: pass def test_tangler_functions(self) -> None: @@ -842,13 +835,12 @@

      Emitter Tests

      self.assertEqual(string.printable, result) def test_tangler_should_codeBegin(self) -> None: - self.tangler.open(self.filename) + 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() - with open("testtangler.code", "r") as result: - txt = result.read() + txt = self.filepath.read_text() self.assertEqual("*The* `Code`\n", txt) @@ -869,42 +861,41 @@

      Emitter Tests

      class TestTanglerMake(unittest.TestCase): def setUp(self) -> None: self.tangler = pyweb.TanglerMake() - self.filename = "testtangler.code" + self.filepath = Path("testtangler.code") self.aChunk = MockChunk("Chunk", 314, 278) #self.aChunk.references_list = [ ("Container", 123) ] - self.tangler.open(self.filename) + 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 = os.path.getmtime(self.filename) - self.original = os.lstat(self.filename) + self.time_original = self.filepath.stat().st_mtime + self.original = self.filepath.stat() #time.sleep(0.75) # Alternative to assure timestamps must be different def tearDown(self) -> None: - import os try: - os.remove("testtangler.code") + self.filepath.unlink() except OSError: pass def test_same_should_leave(self) -> None: - self.tangler.open(self.filename) + 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, os.lstat(self.filename))) - #self.assertEqual(self.time_original, os.path.getmtime(self.filename)) + 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.filename) + 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, os.lstat(self.filename))) - #self.assertNotEqual(self.time_original, os.path.getmtime(self.filename)) + self.assertFalse(os.path.samestat(self.original, self.filepath.stat())) + #self.assertNotEqual(self.time_original, self.filepath.stat().st_mtime)
      @@ -1523,7 +1514,7 @@

      Web Tests

      class TestWebProcessing(unittest.TestCase): def setUp(self) -> None: self.web = pyweb.Web() - self.web.webFileName = "TestWebProcessing.w" + self.web.web_path = Path("TestWebProcessing.w") self.chunk = pyweb.Chunk() self.chunk.appendText("some text") self.chunk.webAdd(self.web) @@ -1847,14 +1838,13 @@

      Action Tests

      self.action.web = self.web self.action.options = argparse.Namespace( webReader = self.webReader, - webFileName="TestLoadAction.w", + source_path=Path("TestLoadAction.w"), command="@", permitList = [], ) - with open("TestLoadAction.w","w") as web: - pass + Path("TestLoadAction.w").write_text("") def tearDown(self) -> None: try: - os.remove("TestLoadAction.w") + Path("TestLoadAction.w").unlink() except IOError: pass def test_should_execute_loading(self) -> None: @@ -1889,6 +1879,7 @@

      Overheads and Main ScriptTests for Loading

      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 +file_path, we can simply provide a file-like input object to the WebReader instance.

      Load Test superclass to refactor common setup (51) =

       class ParseTestcase(unittest.TestCase):
           text = ""
      -    file_name = ""
      +    file_path: Path
           def setUp(self) -> None:
               self.source = io.StringIO(self.text)
               self.web = pyweb.Web()
      @@ -1965,6 +1956,7 @@ 

      Tests for Loading

      Load Test overheads: imports, etc. (52) =

       import logging.handlers
      +from pathlib import Path
       
      @@ -1976,7 +1968,7 @@

      Tests for Loading

      class Test_ParseErrors(ParseTestcase): text = test1_w - file_name = "test1.w" + file_path = Path("test1.w") def setUp(self) -> None: super().setUp() self.logger = logging.getLogger("WebReader") @@ -1985,7 +1977,7 @@

      Tests for Loading

      self.logger.addHandler(self.buffer) self.logger.setLevel(logging.WARN) def test_error_should_count_1(self) -> None: - self.rdr.load(self.web, self.file_name, self.source) + self.rdr.load(self.web, self.file_path, self.source) self.assertEqual(3, self.rdr.errors) messages = [r.message for r in self.buffer.buffer] self.assertEqual( @@ -2032,18 +2024,17 @@

      Tests for Loading

      class Test_IncludeParseErrors(ParseTestcase): text = test8_w - file_name = "test8.w" + file_path = Path("test8.w") def setUp(self) -> None: - with open('test8_inc.tmp','w') as temp: - temp.write(test8_inc_w) super().setUp() + Path('test8_inc.tmp').write_text(test8_inc_w) self.logger = logging.getLogger("WebReader") self.buffer = logging.handlers.BufferingHandler(12) self.buffer.setLevel(logging.WARN) self.logger.addHandler(self.buffer) self.logger.setLevel(logging.WARN) def test_error_should_count_2(self) -> None: - self.rdr.load(self.web, self.file_name, self.source) + self.rdr.load(self.web, self.file_path, self.source) self.assertEqual(1, self.rdr.errors) messages = [r.message for r in self.buffer.buffer] self.assertEqual( @@ -2054,7 +2045,7 @@

      Tests for Loading

      def tearDown(self) -> None: self.logger.setLevel(logging.CRITICAL) self.logger.removeHandler(self.buffer) - os.remove('test8_inc.tmp') + Path('test8_inc.tmp').unlink() super().tearDown()
      @@ -2085,12 +2076,15 @@

      Tests for Loading

      Load Test overheads: imports, etc. (57) +=

       """Loader and parsing tests."""
      -import pyweb
      -import unittest
      +import io
       import logging
       import os
      -import io
      +from pathlib import Path
      +import string
       import types
      +import unittest
      +
      +import pyweb
       
      @@ -2136,8 +2130,8 @@

      Tests for Tangling

       class TangleTestcase(unittest.TestCase):
           text = ""
      -    file_name = ""
           error = ""
      +    file_path: Path
           def setUp(self) -> None:
               self.source = io.StringIO(self.text)
               self.web = pyweb.Web()
      @@ -2145,18 +2139,17 @@ 

      Tests for Tangling

      self.tangler = pyweb.Tangler() def tangle_and_check_exception(self, exception_text: str) -> None: try: - self.rdr.load(self.web, self.file_name, self.source) + 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: - name, _ = os.path.splitext(self.file_name) try: - os.remove(name + ".tmp") - except OSError: - pass + self.file_path.with_suffix(".tmp").unlink() + except FileNotFoundError: + pass # If the test fails, nothing to remove...
      @@ -2168,7 +2161,7 @@

      Tests for Tangling

      class Test_SemanticError_2(TangleTestcase): text = test2_w - file_name = "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.") @@ -2197,7 +2190,7 @@

      Tests for Tangling

      class Test_SemanticError_3(TangleTestcase): text = test3_w - file_name = "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.") @@ -2227,7 +2220,7 @@

      Tests for Tangling

      class Test_SemanticError_4(TangleTestcase): text = test4_w - file_name = "test4.w" + file_path = Path("test4.w") def test_should_raise_noFullName(self) -> None: self.tangle_and_check_exception("No full name for 'part1...'") @@ -2257,7 +2250,7 @@

      Tests for Tangling

      class Test_SemanticError_5(TangleTestcase): text = test5_w - file_name = "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']") @@ -2289,9 +2282,9 @@

      Tests for Tangling

      class Test_SemanticError_6(TangleTestcase): text = test6_w - file_name = "test6.w" + file_path = Path("test6.w") def test_should_warn(self) -> None: - self.rdr.load(self.web, self.file_name, self.source) + 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())) @@ -2326,19 +2319,18 @@

      Tests for Tangling

      class Test_IncludeError_7(TangleTestcase): text = test7_w - file_name = "test7.w" + file_path = Path("test7.w") def setUp(self) -> None: - with open('test7_inc.tmp','w') as temp: - temp.write(test7_inc_w) + Path('test7_inc.tmp').write_text(test7_inc_w) super().setUp() def test_should_include(self) -> None: - self.rdr.load(self.web, self.file_name, self.source) + 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: - os.remove('test7_inc.tmp') + Path('test7_inc.tmp').unlink() super().tearDown() @@ -2365,11 +2357,13 @@

      Tests for Tangling

      Tangle Test overheads: imports, etc. (73) =

       """Tangler tests exercise various semantic features."""
      -import pyweb
      -import unittest
      +import io
       import logging
       import os
      -import io
      +from pathlib import Path
      +import unittest
      +
      +import pyweb
       
      @@ -2407,26 +2401,25 @@

      Tests for Weaving

       class WeaveTestcase(unittest.TestCase):
           text = ""
      -    file_name = ""
           error = ""
      +    file_path: Path
           def setUp(self) -> None:
               self.source = io.StringIO(self.text)
               self.web = pyweb.Web()
               self.rdr = pyweb.WebReader()
           def tangle_and_check_exception(self, exception_text: str) -> None:
               try:
      -            self.rdr.load(self.web, self.file_name, self.source)
      +            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:
      -        name, _ = os.path.splitext(self.file_name)
               try:
      -            os.remove(name + ".html")
      -        except OSError:
      -            pass
      +            self.file_path.with_suffix(".html").unlink()
      +        except FileNotFoundError:
      +            pass  # if the test failed, nothing to remove
       
      @@ -2439,17 +2432,16 @@

      Tests for Weaving

      class Test_RefDefWeave(WeaveTestcase): text = test0_w - file_name = "test0.w" + file_path = Path("test0.w") def test_load_should_createChunks(self) -> None: - self.rdr.load(self.web, self.file_name, self.source) + 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_name, self.source) + self.rdr.load(self.web, self.file_path, self.source) doc = pyweb.HTML() doc.reference_style = pyweb.SimpleReference() self.web.weave(doc) - with open("test0.html","r") as source: - actual = source.read() + actual = self.file_path.with_suffix(".html").read_text() self.maxDiff = None self.assertEqual(test0_expected, actual) @@ -2530,20 +2522,19 @@

      Tests for Weaving

      class TestEvaluations(WeaveTestcase): text = test9_w - file_name = "test9.w" + file_path = Path("test9.w") def test_should_evaluate(self) -> None: - self.rdr.load(self.web, self.file_name, self.source) + self.rdr.load(self.web, self.file_path, self.source) doc = pyweb.HTML( ) doc.reference_style = pyweb.SimpleReference() self.web.weave(doc) - with open("test9.html","r") as source: - actual = source.readlines() + actual = self.file_path.with_suffix(".html").read_text().splitlines() #print(actual) - self.assertEqual("An anonymous chunk.\n", actual[0]) + self.assertEqual("An anonymous chunk.", actual[0]) self.assertTrue(actual[1].startswith("Time =")) - self.assertEqual("File = ('test9.w', 3)\n", actual[2]) - self.assertEqual('Version = 3.1\n', actual[3]) - self.assertEqual(f'CWD = {os.getcwd()}\n', actual[4]) + self.assertEqual("File = ('test9.w', 3)", actual[2]) + self.assertEqual('Version = 3.1', actual[3]) + self.assertEqual(f'CWD = {os.getcwd()}', actual[4])
      @@ -2565,12 +2556,14 @@

      Tests for Weaving

      Weave Test overheads: imports, etc. (82) =

       """Weaver tests exercise various weaving features."""
      -import pyweb
      -import unittest
      +import io
       import logging
       import os
      +from pathlib import Path
       import string
      -import io
      +import unittest
      +
      +import pyweb
       
      @@ -2589,11 +2582,13 @@

      Tests for Weaving

  • -
    -

    Combined Test Script

    - -

    The combined test script runs all tests in all test modules.

    -

    test.py (84) =

    +
    +

    Combined Test Runner

    + +

    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.

    +

    runner.py (84) =

     →Combined Test overheads, imports, etc. (85)
     →Combined Test suite which imports all other test modules (86)
    @@ -2602,7 +2597,7 @@ 

    Combined Test Script

    -

    test.py (84).

    +

    runner.py (84).

    The overheads import unittest and logging, because those are essential infrastructure. Additionally, each of the test modules is also imported.

    @@ -2620,7 +2615,7 @@

    Combined Test Script

    -

    Combined Test overheads, imports, etc. (85). Used by: test.py (84)

    +

    Combined Test overheads, imports, etc. (85). Used by: runner.py (84)

    The test suite is built from each of the individual test modules.

    Combined Test suite which imports all other test modules (86) =

    @@ -2633,7 +2628,7 @@

    Combined Test Script

    -

    Combined Test suite which imports all other test modules (86). Used by: test.py (84)

    +

    Combined Test suite which imports all other test modules (86). Used by: runner.py (84)

    In order to debug failing tests, we accept some command-line parameters to the combined testing script.

    @@ -2648,12 +2643,12 @@

    Combined Test Script

    verbosity=logging.CRITICAL, logger="" ) - config = parser.parse_args(namespace=defaults) + config = parser.parse_args(argv, namespace=defaults) return config
    -

    Combined Test command line options (87). Used by: test.py (84)

    +

    Combined Test command line options (87). Used by: runner.py (84)

    This means we can use -dlWebReader to debug the Web Reader. We can use -d -lWebReader,TanglerMake to debug both @@ -2682,6 +2677,7 @@

    Combined Test Script

    l = logging.getLogger(logger_name) l.setLevel(options.verbosity) logger.info(f"Setting {l}") + tr = unittest.TextTestRunner() result = tr.run(suite()) logging.shutdown() @@ -2689,7 +2685,7 @@

    Combined Test Script

    -

    Combined Test main script (88). Used by: test.py (84)

    +

    Combined Test main script (88). Used by: runner.py (84)

    @@ -2753,7 +2749,7 @@

    Files

    page-layout.css:  →(90) -test.py:→(84) +runner.py:→(84) test_loader.py:→(50) @@ -3015,8 +3011,8 @@

    User Identifiers

    (None)


    -Created by ../pyweb.py at Fri Jun 10 10:32:05 2022.
    -

    Source pyweb_test.w modified Thu Jun 9 12:12:11 2022.

    +Created by ../pyweb.py at Fri Jun 10 17:08:42 2022. +

    Source pyweb_test.w modified Fri Jun 10 17:07:24 2022.

    pyweb.__version__ '3.1'.

    Working directory '/Users/slott/Documents/Projects/py-web-tool/test'.

    diff --git a/test/pyweb_test.rst b/test/pyweb_test.rst index af32d9a..5a0e1f8 100644 --- a/test/pyweb_test.rst +++ b/test/pyweb_test.rst @@ -334,15 +334,14 @@ The default Weaver is an Emitter that uses templates to produce RST markup. def setUp(self) -> None: self.weaver = pyweb.Weaver() self.weaver.reference\_style = pyweb.SimpleReference() - self.filename = "testweaver" + 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.referencedBy = [self.aFileChunk] def tearDown(self) -> None: - import os try: - pass #os.remove("testweaver.rst") + self.filepath.unlink() except OSError: pass @@ -355,48 +354,44 @@ The default Weaver is an Emitter that uses templates to produce RST markup. self.assertEqual(r"\|srarr\|\\ Chunk (\`314\`\_)", result) def test\_weaver\_should\_codeBegin(self) -> None: - self.weaver.open(self.filename) + 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() - with open("testweaver.rst", "r") as result: - txt = result.read() + 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.filename) + 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() - with open("testweaver.rst", "r") as result: - txt = result.read() + 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.filename) + 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() - with open("testweaver.rst", "r") as result: - txt = result.read() + 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.filename) + 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() - with open("testweaver.rst", "r") as result: - txt = result.read() + txt = self.filepath.with\_suffix(".rst").read\_text() self.assertEqual("\\n:Chunk:\\n \`123\`\_ [\`314\`\_] \`567\`\_\\n\\n", txt) .. @@ -424,15 +419,14 @@ We'll examine a few features of the LaTeX templates. def setUp(self) -> None: self.weaver = pyweb.LaTeX() self.weaver.reference\_style = pyweb.SimpleReference() - self.filename = "testweaver" + 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.referencedBy = [self.aFileChunk,] def tearDown(self) -> None: - import os try: - os.remove("testweaver.tex") + self.filepath.with\_suffix(".tex").unlink() except OSError: pass @@ -464,17 +458,17 @@ We'll examine a few features of the HTML templates. def setUp(self) -> None: self.weaver = pyweb.HTML( ) self.weaver.reference\_style = pyweb.SimpleReference() - self.filename = "testweaver" + 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.referencedBy = [self.aFileChunk,] def tearDown(self) -> None: - import os try: - os.remove("testweaver.html") + self.filepath.with\_suffix(".html").unlink() except OSError: pass + def test\_weaver\_functions\_html(self) -> None: result = self.weaver.quote("a < b && c > d") @@ -523,16 +517,15 @@ compiler and language. class TestTangler(unittest.TestCase): def setUp(self) -> None: self.tangler = pyweb.Tangler() - self.filename = "testtangler.code" + 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: - import os try: - os.remove("testtangler.code") - except OSError: + self.filepath.unlink() + except FileNotFoundError: pass def test\_tangler\_functions(self) -> None: @@ -540,13 +533,12 @@ compiler and language. self.assertEqual(string.printable, result) def test\_tangler\_should\_codeBegin(self) -> None: - self.tangler.open(self.filename) + 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() - with open("testtangler.code", "r") as result: - txt = result.read() + txt = self.filepath.read\_text() self.assertEqual("\*The\* \`Code\`\\n", txt) .. @@ -579,42 +571,41 @@ need to wait for a full second to elapse or we need to mock the various class TestTanglerMake(unittest.TestCase): def setUp(self) -> None: self.tangler = pyweb.TanglerMake() - self.filename = "testtangler.code" + self.filepath = Path("testtangler.code") self.aChunk = MockChunk("Chunk", 314, 278) #self.aChunk.references\_list = [ ("Container", 123) ] - self.tangler.open(self.filename) + 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 = os.path.getmtime(self.filename) - self.original = os.lstat(self.filename) + self.time\_original = self.filepath.stat().st\_mtime + self.original = self.filepath.stat() #time.sleep(0.75) # Alternative to assure timestamps must be different def tearDown(self) -> None: - import os try: - os.remove("testtangler.code") + self.filepath.unlink() except OSError: pass def test\_same\_should\_leave(self) -> None: - self.tangler.open(self.filename) + 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, os.lstat(self.filename))) - #self.assertEqual(self.time\_original, os.path.getmtime(self.filename)) + 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.filename) + 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, os.lstat(self.filename))) - #self.assertNotEqual(self.time\_original, os.path.getmtime(self.filename)) + self.assertFalse(os.path.samestat(self.original, self.filepath.stat())) + #self.assertNotEqual(self.time\_original, self.filepath.stat().st\_mtime) .. @@ -1437,7 +1428,7 @@ This is more difficult to create mocks for. class TestWebProcessing(unittest.TestCase): def setUp(self) -> None: self.web = pyweb.Web() - self.web.webFileName = "TestWebProcessing.w" + self.web.web\_path = Path("TestWebProcessing.w") self.chunk = pyweb.Chunk() self.chunk.appendText("some text") self.chunk.webAdd(self.web) @@ -1871,14 +1862,13 @@ load, tangle, weave. self.action.web = self.web self.action.options = argparse.Namespace( webReader = self.webReader, - webFileName="TestLoadAction.w", + source\_path=Path("TestLoadAction.w"), command="@", permitList = [], ) - with open("TestLoadAction.w","w") as web: - pass + Path("TestLoadAction.w").write\_text("") def tearDown(self) -> None: try: - os.remove("TestLoadAction.w") + Path("TestLoadAction.w").unlink() except IOError: pass def test\_should\_execute\_loading(self) -> None: @@ -1928,6 +1918,7 @@ The boilerplate code for unit testing is the following. import io import logging import os + from pathlib import Path import re import string import time @@ -2008,7 +1999,7 @@ We need to be able to load a web from one or more source files. 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 +``file_path``, we can simply provide a file-like input object to the ``WebReader`` instance. @@ -2020,7 +2011,7 @@ input object to the ``WebReader`` instance. class ParseTestcase(unittest.TestCase): text = "" - file\_name = "" + file\_path: Path def setUp(self) -> None: self.source = io.StringIO(self.text) self.web = pyweb.Web() @@ -2045,6 +2036,7 @@ find an expected next token. import logging.handlers + from pathlib import Path .. @@ -2064,7 +2056,7 @@ find an expected next token. class Test\_ParseErrors(ParseTestcase): text = test1\_w - file\_name = "test1.w" + file\_path = Path("test1.w") def setUp(self) -> None: super().setUp() self.logger = logging.getLogger("WebReader") @@ -2073,7 +2065,7 @@ find an expected next token. self.logger.addHandler(self.buffer) self.logger.setLevel(logging.WARN) def test\_error\_should\_count\_1(self) -> None: - self.rdr.load(self.web, self.file\_name, self.source) + self.rdr.load(self.web, self.file\_path, self.source) self.assertEqual(3, self.rdr.errors) messages = [r.message for r in self.buffer.buffer] self.assertEqual( @@ -2139,18 +2131,17 @@ create a temporary file. It's hard to mock the include processing. class Test\_IncludeParseErrors(ParseTestcase): text = test8\_w - file\_name = "test8.w" + file\_path = Path("test8.w") def setUp(self) -> None: - with open('test8\_inc.tmp','w') as temp: - temp.write(test8\_inc\_w) super().setUp() + Path('test8\_inc.tmp').write\_text(test8\_inc\_w) self.logger = logging.getLogger("WebReader") self.buffer = logging.handlers.BufferingHandler(12) self.buffer.setLevel(logging.WARN) self.logger.addHandler(self.buffer) self.logger.setLevel(logging.WARN) def test\_error\_should\_count\_2(self) -> None: - self.rdr.load(self.web, self.file\_name, self.source) + self.rdr.load(self.web, self.file\_path, self.source) self.assertEqual(1, self.rdr.errors) messages = [r.message for r in self.buffer.buffer] self.assertEqual( @@ -2161,7 +2152,7 @@ create a temporary file. It's hard to mock the include processing. def tearDown(self) -> None: self.logger.setLevel(logging.CRITICAL) self.logger.removeHandler(self.buffer) - os.remove('test8\_inc.tmp') + Path('test8\_inc.tmp').unlink() super().tearDown() .. @@ -2210,12 +2201,15 @@ be given to the included document by ``setUp``. """Loader and parsing tests.""" - import pyweb - import unittest + import io import logging import os - import io + from pathlib import Path + import string import types + import unittest + + import pyweb .. @@ -2288,8 +2282,8 @@ exceptions raised. class TangleTestcase(unittest.TestCase): text = "" - file\_name = "" error = "" + file\_path: Path def setUp(self) -> None: self.source = io.StringIO(self.text) self.web = pyweb.Web() @@ -2297,18 +2291,17 @@ exceptions raised. self.tangler = pyweb.Tangler() def tangle\_and\_check\_exception(self, exception\_text: str) -> None: try: - self.rdr.load(self.web, self.file\_name, self.source) + 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: - name, \_ = os.path.splitext(self.file\_name) try: - os.remove(name + ".tmp") - except OSError: - pass + self.file\_path.with\_suffix(".tmp").unlink() + except FileNotFoundError: + pass # If the test fails, nothing to remove... .. @@ -2328,7 +2321,7 @@ exceptions raised. class Test\_SemanticError\_2(TangleTestcase): text = test2\_w - file\_name = "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.") @@ -2373,7 +2366,7 @@ exceptions raised. class Test\_SemanticError\_3(TangleTestcase): text = test3\_w - file\_name = "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.") @@ -2420,7 +2413,7 @@ exceptions raised. class Test\_SemanticError\_4(TangleTestcase): text = test4\_w - file\_name = "test4.w" + file\_path = Path("test4.w") def test\_should\_raise\_noFullName(self) -> None: self.tangle\_and\_check\_exception("No full name for 'part1...'") @@ -2466,7 +2459,7 @@ exceptions raised. class Test\_SemanticError\_5(TangleTestcase): text = test5\_w - file\_name = "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']") @@ -2514,9 +2507,9 @@ exceptions raised. class Test\_SemanticError\_6(TangleTestcase): text = test6\_w - file\_name = "test6.w" + file\_path = Path("test6.w") def test\_should\_warn(self) -> None: - self.rdr.load(self.web, self.file\_name, self.source) + 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())) @@ -2567,19 +2560,18 @@ exceptions raised. class Test\_IncludeError\_7(TangleTestcase): text = test7\_w - file\_name = "test7.w" + file\_path = Path("test7.w") def setUp(self) -> None: - with open('test7\_inc.tmp','w') as temp: - temp.write(test7\_inc\_w) + Path('test7\_inc.tmp').write\_text(test7\_inc\_w) super().setUp() def test\_should\_include(self) -> None: - self.rdr.load(self.web, self.file\_name, self.source) + 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: - os.remove('test7\_inc.tmp') + Path('test7\_inc.tmp').unlink() super().tearDown() .. @@ -2622,11 +2614,13 @@ exceptions raised. """Tangler tests exercise various semantic features.""" - import pyweb - import unittest + import io import logging import os - import io + from pathlib import Path + import unittest + + import pyweb .. @@ -2690,26 +2684,25 @@ Weaving test cases have a common setup shown in this superclass. class WeaveTestcase(unittest.TestCase): text = "" - file\_name = "" error = "" + file\_path: Path def setUp(self) -> None: self.source = io.StringIO(self.text) self.web = pyweb.Web() self.rdr = pyweb.WebReader() def tangle\_and\_check\_exception(self, exception\_text: str) -> None: try: - self.rdr.load(self.web, self.file\_name, self.source) + 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: - name, \_ = os.path.splitext(self.file\_name) try: - os.remove(name + ".html") - except OSError: - pass + self.file\_path.with\_suffix(".html").unlink() + except FileNotFoundError: + pass # if the test failed, nothing to remove .. @@ -2730,17 +2723,16 @@ Weaving test cases have a common setup shown in this superclass. class Test\_RefDefWeave(WeaveTestcase): text = test0\_w - file\_name = "test0.w" + file\_path = Path("test0.w") def test\_load\_should\_createChunks(self) -> None: - self.rdr.load(self.web, self.file\_name, self.source) + 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\_name, self.source) + self.rdr.load(self.web, self.file\_path, self.source) doc = pyweb.HTML() doc.reference\_style = pyweb.SimpleReference() self.web.weave(doc) - with open("test0.html","r") as source: - actual = source.read() + actual = self.file\_path.with\_suffix(".html").read\_text() self.maxDiff = None self.assertEqual(test0\_expected, actual) @@ -2847,20 +2839,19 @@ to properly provide a consistent output from ``time.asctime()``. class TestEvaluations(WeaveTestcase): text = test9\_w - file\_name = "test9.w" + file\_path = Path("test9.w") def test\_should\_evaluate(self) -> None: - self.rdr.load(self.web, self.file\_name, self.source) + self.rdr.load(self.web, self.file\_path, self.source) doc = pyweb.HTML( ) doc.reference\_style = pyweb.SimpleReference() self.web.weave(doc) - with open("test9.html","r") as source: - actual = source.readlines() + actual = self.file\_path.with\_suffix(".html").read\_text().splitlines() #print(actual) - self.assertEqual("An anonymous chunk.\\n", actual[0]) + self.assertEqual("An anonymous chunk.", actual[0]) self.assertTrue(actual[1].startswith("Time =")) - self.assertEqual("File = ('test9.w', 3)\\n", actual[2]) - self.assertEqual('Version = 3.1\\n', actual[3]) - self.assertEqual(f'CWD = {os.getcwd()}\\n', actual[4]) + self.assertEqual("File = ('test9.w', 3)", actual[2]) + self.assertEqual('Version = 3.1', actual[3]) + self.assertEqual(f'CWD = {os.getcwd()}', actual[4]) .. @@ -2898,12 +2889,14 @@ to properly provide a consistent output from ``time.asctime()``. """Weaver tests exercise various weaving features.""" - import pyweb - import unittest + import io import logging import os + from pathlib import Path import string - import io + import unittest + + import pyweb .. @@ -2932,16 +2925,18 @@ to properly provide a consistent output from ``time.asctime()``. -Combined Test Script +Combined Test Runner ===================== -.. test/combined.w +.. test/runner.w -The combined test script runs all tests in all test modules. +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. .. _`84`: -.. rubric:: test.py (84) = +.. rubric:: runner.py (84) = .. parsed-literal:: :class: code @@ -2954,7 +2949,7 @@ The combined test script runs all tests in all test modules. .. class:: small - |loz| *test.py (84)*. + |loz| *runner.py (84)*. The overheads import unittest and logging, because those are essential @@ -2981,7 +2976,7 @@ infrastructure. Additionally, each of the test modules is also imported. .. class:: small - |loz| *Combined Test overheads, imports, etc. (85)*. Used by: test.py (`84`_) + |loz| *Combined Test overheads, imports, etc. (85)*. Used by: runner.py (`84`_) The test suite is built from each of the individual test modules. @@ -3003,7 +2998,7 @@ The test suite is built from each of the individual test modules. .. class:: small - |loz| *Combined Test suite which imports all other test modules (86)*. Used by: test.py (`84`_) + |loz| *Combined Test suite which imports all other test modules (86)*. Used by: runner.py (`84`_) In order to debug failing tests, we accept some command-line @@ -3025,14 +3020,14 @@ parameters to the combined testing script. verbosity=logging.CRITICAL, logger="" ) - config = parser.parse\_args(namespace=defaults) + config = parser.parse\_args(argv, namespace=defaults) return config .. .. class:: small - |loz| *Combined Test command line options (87)*. Used by: test.py (`84`_) + |loz| *Combined Test command line options (87)*. Used by: runner.py (`84`_) This means we can use ``-dlWebReader`` to debug the Web Reader. @@ -3071,6 +3066,7 @@ Once logging is running, it executes the ``unittest.TextTestRunner`` on the test l = logging.getLogger(logger\_name) l.setLevel(options.verbosity) logger.info(f"Setting {l}") + tr = unittest.TextTestRunner() result = tr.run(suite()) logging.shutdown() @@ -3080,7 +3076,7 @@ Once logging is running, it executes the ``unittest.TextTestRunner`` on the test .. class:: small - |loz| *Combined Test main script (88)*. Used by: test.py (`84`_) + |loz| *Combined Test main script (88)*. Used by: runner.py (`84`_) @@ -3158,7 +3154,7 @@ Files |srarr|\ (`89`_) :page-layout.css: |srarr|\ (`90`_) -:test.py: +:runner.py: |srarr|\ (`84`_) :test_loader.py: |srarr|\ (`50`_) @@ -3342,9 +3338,9 @@ User Identifiers .. class:: small - Created by ../pyweb.py at Fri Jun 10 10:32:05 2022. + Created by ../pyweb.py at Fri Jun 10 17:08:42 2022. - Source pyweb_test.w modified Thu Jun 9 12:12:11 2022. + Source pyweb_test.w modified Fri Jun 10 17:07:24 2022. pyweb.__version__ '3.1'. diff --git a/test/pyweb_test.w b/test/pyweb_test.w index 57045c9..24abdfa 100644 --- a/test/pyweb_test.w +++ b/test/pyweb_test.w @@ -19,7 +19,7 @@ Yet Another Literate Programming Tool @i func.w -@i combined.w +@i runner.w Additional Files ================= diff --git a/test/test.py b/test/runner.py similarity index 95% rename from test/test.py rename to test/runner.py index ed5f7c6..6097e61 100644 --- a/test/test.py +++ b/test/runner.py @@ -26,7 +26,7 @@ def get_options(argv: list[str] = sys.argv[1:]) -> argparse.Namespace: verbosity=logging.CRITICAL, logger="" ) - config = parser.parse_args(namespace=defaults) + config = parser.parse_args(argv, namespace=defaults) return config @@ -38,6 +38,7 @@ def get_options(argv: list[str] = sys.argv[1:]) -> argparse.Namespace: l = logging.getLogger(logger_name) l.setLevel(options.verbosity) logger.info(f"Setting {l}") + tr = unittest.TextTestRunner() result = tr.run(suite()) logging.shutdown() diff --git a/test/combined.w b/test/runner.w similarity index 89% rename from test/combined.w rename to test/runner.w index b3b2d0f..6edbcd1 100644 --- a/test/combined.w +++ b/test/runner.w @@ -1,11 +1,13 @@ -Combined Test Script +Combined Test Runner ===================== -.. test/combined.w +.. test/runner.w -The combined test script runs all tests in all test modules. +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 test.py +@o runner.py @{@ @ @ @@ -53,7 +55,7 @@ def get_options(argv: list[str] = sys.argv[1:]) -> argparse.Namespace: verbosity=logging.CRITICAL, logger="" ) - config = parser.parse_args(namespace=defaults) + config = parser.parse_args(argv, namespace=defaults) return config @} @@ -88,6 +90,7 @@ if __name__ == "__main__": l = logging.getLogger(logger_name) l.setLevel(options.verbosity) logger.info(f"Setting {l}") + tr = unittest.TextTestRunner() result = tr.run(suite()) logging.shutdown() diff --git a/test/test_latex.w b/test/test_latex.w index fd9faf9..4729298 100644 --- a/test/test_latex.w +++ b/test/test_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 latex_test_1.py @{ @< Import the os module @> @< Get the OS description @> @@ -34,7 +34,7 @@ message: The second uses string substitution: -@o test2.py +@o latex_test_2.py @{ @< Import the os module @> @< Get the OS description @> @@ -75,8 +75,8 @@ better: 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} diff --git a/test/test_loader.py b/test/test_loader.py index ac06ea2..aabb179 100644 --- a/test/test_loader.py +++ b/test/test_loader.py @@ -1,18 +1,22 @@ import logging.handlers +from pathlib import Path """Loader and parsing tests.""" -import pyweb -import unittest +import io import logging import os -import io +from pathlib import Path +import string import types +import unittest + +import pyweb class ParseTestcase(unittest.TestCase): text = "" - file_name = "" + file_path: Path def setUp(self) -> None: self.source = io.StringIO(self.text) self.web = pyweb.Web() @@ -34,7 +38,7 @@ def setUp(self) -> None: class Test_ParseErrors(ParseTestcase): text = test1_w - file_name = "test1.w" + file_path = Path("test1.w") def setUp(self) -> None: super().setUp() self.logger = logging.getLogger("WebReader") @@ -43,7 +47,7 @@ def setUp(self) -> None: self.logger.addHandler(self.buffer) self.logger.setLevel(logging.WARN) def test_error_should_count_1(self) -> None: - self.rdr.load(self.web, self.file_name, self.source) + self.rdr.load(self.web, self.file_path, self.source) self.assertEqual(3, self.rdr.errors) messages = [r.message for r in self.buffer.buffer] self.assertEqual( @@ -75,18 +79,17 @@ def tearDown(self) -> None: class Test_IncludeParseErrors(ParseTestcase): text = test8_w - file_name = "test8.w" + file_path = Path("test8.w") def setUp(self) -> None: - with open('test8_inc.tmp','w') as temp: - temp.write(test8_inc_w) super().setUp() + Path('test8_inc.tmp').write_text(test8_inc_w) self.logger = logging.getLogger("WebReader") self.buffer = logging.handlers.BufferingHandler(12) self.buffer.setLevel(logging.WARN) self.logger.addHandler(self.buffer) self.logger.setLevel(logging.WARN) def test_error_should_count_2(self) -> None: - self.rdr.load(self.web, self.file_name, self.source) + self.rdr.load(self.web, self.file_path, self.source) self.assertEqual(1, self.rdr.errors) messages = [r.message for r in self.buffer.buffer] self.assertEqual( @@ -97,7 +100,7 @@ def test_error_should_count_2(self) -> None: def tearDown(self) -> None: self.logger.setLevel(logging.CRITICAL) self.logger.removeHandler(self.buffer) - os.remove('test8_inc.tmp') + Path('test8_inc.tmp').unlink() super().tearDown() diff --git a/test/test_rst.w b/test/test_rst.w index 71522ec..cfb3608 100644 --- a/test/test_rst.w +++ b/test/test_rst.w @@ -30,7 +30,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 rst_test_1.py @{ @< Import the os module @> @< Get the OS description @> @@ -40,7 +40,7 @@ message: The second uses string substitution: -@o test2.py +@o rst_test_2.py @{ @< Import the os module @> @< Get the OS description @> @@ -83,8 +83,8 @@ better: 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 ---------------------- diff --git a/test/test_tangler.py b/test/test_tangler.py index 256dac8..be82673 100644 --- a/test/test_tangler.py +++ b/test/test_tangler.py @@ -1,16 +1,18 @@ """Tangler tests exercise various semantic features.""" -import pyweb -import unittest +import io import logging import os -import io +from pathlib import Path +import unittest + +import pyweb class TangleTestcase(unittest.TestCase): text = "" - file_name = "" error = "" + file_path: Path def setUp(self) -> None: self.source = io.StringIO(self.text) self.web = pyweb.Web() @@ -18,18 +20,17 @@ def setUp(self) -> None: self.tangler = pyweb.Tangler() def tangle_and_check_exception(self, exception_text: str) -> None: try: - self.rdr.load(self.web, self.file_name, self.source) + 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: - name, _ = os.path.splitext(self.file_name) try: - os.remove(name + ".tmp") - except OSError: - pass + self.file_path.with_suffix(".tmp").unlink() + except FileNotFoundError: + pass # If the test fails, nothing to remove... @@ -45,7 +46,7 @@ def tearDown(self) -> None: class Test_SemanticError_2(TangleTestcase): text = test2_w - file_name = "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.") @@ -64,7 +65,7 @@ def test_should_raise_undefined(self) -> None: class Test_SemanticError_3(TangleTestcase): text = test3_w - file_name = "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.") @@ -83,7 +84,7 @@ def test_should_raise_bad_xref(self) -> None: class Test_SemanticError_4(TangleTestcase): text = test4_w - file_name = "test4.w" + file_path = Path("test4.w") def test_should_raise_noFullName(self) -> None: self.tangle_and_check_exception("No full name for 'part1...'") @@ -104,7 +105,7 @@ def test_should_raise_noFullName(self) -> None: class Test_SemanticError_5(TangleTestcase): text = test5_w - file_name = "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']") @@ -125,9 +126,9 @@ def test_should_raise_ambiguous(self) -> None: class Test_SemanticError_6(TangleTestcase): text = test6_w - file_name = "test6.w" + file_path = Path("test6.w") def test_should_warn(self) -> None: - self.rdr.load(self.web, self.file_name, self.source) + 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())) @@ -150,19 +151,18 @@ def test_should_warn(self) -> None: class Test_IncludeError_7(TangleTestcase): text = test7_w - file_name = "test7.w" + file_path = Path("test7.w") def setUp(self) -> None: - with open('test7_inc.tmp','w') as temp: - temp.write(test7_inc_w) + Path('test7_inc.tmp').write_text(test7_inc_w) super().setUp() def test_should_include(self) -> None: - self.rdr.load(self.web, self.file_name, self.source) + 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: - os.remove('test7_inc.tmp') + Path('test7_inc.tmp').unlink() super().tearDown() diff --git a/test/test_unit.py b/test/test_unit.py index 54be13c..1eb4296 100644 --- a/test/test_unit.py +++ b/test/test_unit.py @@ -3,6 +3,7 @@ import io import logging import os +from pathlib import Path import re import string import time @@ -79,15 +80,14 @@ class TestWeaver(unittest.TestCase): def setUp(self) -> None: self.weaver = pyweb.Weaver() self.weaver.reference_style = pyweb.SimpleReference() - self.filename = "testweaver" + 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.referencedBy = [self.aFileChunk] def tearDown(self) -> None: - import os try: - pass #os.remove("testweaver.rst") + self.filepath.unlink() except OSError: pass @@ -100,48 +100,44 @@ def test_weaver_functions_generic(self) -> None: self.assertEqual(r"|srarr|\ Chunk (`314`_)", result) def test_weaver_should_codeBegin(self) -> None: - self.weaver.open(self.filename) + 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() - with open("testweaver.rst", "r") as result: - txt = result.read() + 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.filename) + 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() - with open("testweaver.rst", "r") as result: - txt = result.read() + 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.filename) + 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() - with open("testweaver.rst", "r") as result: - txt = result.read() + 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.filename) + 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() - with open("testweaver.rst", "r") as result: - txt = result.read() + txt = self.filepath.with_suffix(".rst").read_text() self.assertEqual("\n:Chunk:\n `123`_ [`314`_] `567`_\n\n", txt) @@ -149,15 +145,14 @@ class TestLaTeX(unittest.TestCase): def setUp(self) -> None: self.weaver = pyweb.LaTeX() self.weaver.reference_style = pyweb.SimpleReference() - self.filename = "testweaver" + 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.referencedBy = [self.aFileChunk,] def tearDown(self) -> None: - import os try: - os.remove("testweaver.tex") + self.filepath.with_suffix(".tex").unlink() except OSError: pass @@ -174,17 +169,17 @@ class TestHTML(unittest.TestCase): def setUp(self) -> None: self.weaver = pyweb.HTML( ) self.weaver.reference_style = pyweb.SimpleReference() - self.filename = "testweaver" + 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.referencedBy = [self.aFileChunk,] def tearDown(self) -> None: - import os try: - os.remove("testweaver.html") + self.filepath.with_suffix(".html").unlink() except OSError: pass + def test_weaver_functions_html(self) -> None: result = self.weaver.quote("a < b && c > d") @@ -200,16 +195,15 @@ def test_weaver_functions_html(self) -> None: class TestTangler(unittest.TestCase): def setUp(self) -> None: self.tangler = pyweb.Tangler() - self.filename = "testtangler.code" + 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: - import os try: - os.remove("testtangler.code") - except OSError: + self.filepath.unlink() + except FileNotFoundError: pass def test_tangler_functions(self) -> None: @@ -217,55 +211,53 @@ def test_tangler_functions(self) -> None: self.assertEqual(string.printable, result) def test_tangler_should_codeBegin(self) -> None: - self.tangler.open(self.filename) + 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() - with open("testtangler.code", "r") as result: - txt = result.read() + txt = self.filepath.read_text() self.assertEqual("*The* `Code`\n", txt) class TestTanglerMake(unittest.TestCase): def setUp(self) -> None: self.tangler = pyweb.TanglerMake() - self.filename = "testtangler.code" + self.filepath = Path("testtangler.code") self.aChunk = MockChunk("Chunk", 314, 278) #self.aChunk.references_list = [ ("Container", 123) ] - self.tangler.open(self.filename) + 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 = os.path.getmtime(self.filename) - self.original = os.lstat(self.filename) + self.time_original = self.filepath.stat().st_mtime + self.original = self.filepath.stat() #time.sleep(0.75) # Alternative to assure timestamps must be different def tearDown(self) -> None: - import os try: - os.remove("testtangler.code") + self.filepath.unlink() except OSError: pass def test_same_should_leave(self) -> None: - self.tangler.open(self.filename) + 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, os.lstat(self.filename))) - #self.assertEqual(self.time_original, os.path.getmtime(self.filename)) + 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.filename) + 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, os.lstat(self.filename))) - #self.assertNotEqual(self.time_original, os.path.getmtime(self.filename)) + self.assertFalse(os.path.samestat(self.original, self.filepath.stat())) + #self.assertNotEqual(self.time_original, self.filepath.stat().st_mtime) @@ -722,7 +714,7 @@ def test_chunks_should_add_and_index(self) -> None: class TestWebProcessing(unittest.TestCase): def setUp(self) -> None: self.web = pyweb.Web() - self.web.webFileName = "TestWebProcessing.w" + self.web.web_path = Path("TestWebProcessing.w") self.chunk = pyweb.Chunk() self.chunk.appendText("some text") self.chunk.webAdd(self.web) @@ -896,14 +888,13 @@ def setUp(self) -> None: self.action.web = self.web self.action.options = argparse.Namespace( webReader = self.webReader, - webFileName="TestLoadAction.w", + source_path=Path("TestLoadAction.w"), command="@", permitList = [], ) - with open("TestLoadAction.w","w") as web: - pass + Path("TestLoadAction.w").write_text("") def tearDown(self) -> None: try: - os.remove("TestLoadAction.w") + Path("TestLoadAction.w").unlink() except IOError: pass def test_should_execute_loading(self) -> None: diff --git a/test/test_weaver.py b/test/test_weaver.py index c8c95b7..dc0f9e2 100644 --- a/test/test_weaver.py +++ b/test/test_weaver.py @@ -1,35 +1,36 @@ """Weaver tests exercise various weaving features.""" -import pyweb -import unittest +import io import logging import os +from pathlib import Path import string -import io +import unittest + +import pyweb class WeaveTestcase(unittest.TestCase): text = "" - file_name = "" error = "" + file_path: Path def setUp(self) -> None: self.source = io.StringIO(self.text) self.web = pyweb.Web() self.rdr = pyweb.WebReader() def tangle_and_check_exception(self, exception_text: str) -> None: try: - self.rdr.load(self.web, self.file_name, self.source) + 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: - name, _ = os.path.splitext(self.file_name) try: - os.remove(name + ".html") - except OSError: - pass + self.file_path.with_suffix(".html").unlink() + except FileNotFoundError: + pass # if the test failed, nothing to remove @@ -90,17 +91,16 @@ def fastExp(n, p): class Test_RefDefWeave(WeaveTestcase): text = test0_w - file_name = "test0.w" + file_path = Path("test0.w") def test_load_should_createChunks(self) -> None: - self.rdr.load(self.web, self.file_name, self.source) + 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_name, self.source) + self.rdr.load(self.web, self.file_path, self.source) doc = pyweb.HTML() doc.reference_style = pyweb.SimpleReference() self.web.weave(doc) - with open("test0.html","r") as source: - actual = source.read() + actual = self.file_path.with_suffix(".html").read_text() self.maxDiff = None self.assertEqual(test0_expected, actual) @@ -117,20 +117,19 @@ def test_weave_should_createFile(self) -> None: class TestEvaluations(WeaveTestcase): text = test9_w - file_name = "test9.w" + file_path = Path("test9.w") def test_should_evaluate(self) -> None: - self.rdr.load(self.web, self.file_name, self.source) + self.rdr.load(self.web, self.file_path, self.source) doc = pyweb.HTML( ) doc.reference_style = pyweb.SimpleReference() self.web.weave(doc) - with open("test9.html","r") as source: - actual = source.readlines() + actual = self.file_path.with_suffix(".html").read_text().splitlines() #print(actual) - self.assertEqual("An anonymous chunk.\n", actual[0]) + self.assertEqual("An anonymous chunk.", actual[0]) self.assertTrue(actual[1].startswith("Time =")) - self.assertEqual("File = ('test9.w', 3)\n", actual[2]) - self.assertEqual('Version = 3.1\n', actual[3]) - self.assertEqual(f'CWD = {os.getcwd()}\n', actual[4]) + self.assertEqual("File = ('test9.w', 3)", actual[2]) + self.assertEqual('Version = 3.1', actual[3]) + self.assertEqual(f'CWD = {os.getcwd()}', actual[4]) if __name__ == "__main__": diff --git a/test/unit.w b/test/unit.w index 275cce2..5e9935f 100644 --- a/test/unit.w +++ b/test/unit.w @@ -204,15 +204,14 @@ class TestWeaver(unittest.TestCase): def setUp(self) -> None: self.weaver = pyweb.Weaver() self.weaver.reference_style = pyweb.SimpleReference() - self.filename = "testweaver" + 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.referencedBy = [self.aFileChunk] def tearDown(self) -> None: - import os try: - pass #os.remove("testweaver.rst") + self.filepath.unlink() except OSError: pass @@ -225,48 +224,44 @@ class TestWeaver(unittest.TestCase): self.assertEqual(r"|srarr|\ Chunk (`314`_)", result) def test_weaver_should_codeBegin(self) -> None: - self.weaver.open(self.filename) + 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() - with open("testweaver.rst", "r") as result: - txt = result.read() + 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.filename) + 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() - with open("testweaver.rst", "r") as result: - txt = result.read() + 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.filename) + 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() - with open("testweaver.rst", "r") as result: - txt = result.read() + 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.filename) + 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() - with open("testweaver.rst", "r") as result: - txt = result.read() + txt = self.filepath.with_suffix(".rst").read_text() self.assertEqual("\n:Chunk:\n `123`_ [`314`_] `567`_\n\n", txt) @} @@ -282,15 +277,14 @@ class TestLaTeX(unittest.TestCase): def setUp(self) -> None: self.weaver = pyweb.LaTeX() self.weaver.reference_style = pyweb.SimpleReference() - self.filename = "testweaver" + 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.referencedBy = [self.aFileChunk,] def tearDown(self) -> None: - import os try: - os.remove("testweaver.tex") + self.filepath.with_suffix(".tex").unlink() except OSError: pass @@ -310,17 +304,17 @@ class TestHTML(unittest.TestCase): def setUp(self) -> None: self.weaver = pyweb.HTML( ) self.weaver.reference_style = pyweb.SimpleReference() - self.filename = "testweaver" + 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.referencedBy = [self.aFileChunk,] def tearDown(self) -> None: - import os try: - os.remove("testweaver.html") + self.filepath.with_suffix(".html").unlink() except OSError: pass + def test_weaver_functions_html(self) -> None: result = self.weaver.quote("a < b && c > d") @@ -346,16 +340,15 @@ compiler and language. class TestTangler(unittest.TestCase): def setUp(self) -> None: self.tangler = pyweb.Tangler() - self.filename = "testtangler.code" + 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: - import os try: - os.remove("testtangler.code") - except OSError: + self.filepath.unlink() + except FileNotFoundError: pass def test_tangler_functions(self) -> None: @@ -363,13 +356,12 @@ class TestTangler(unittest.TestCase): self.assertEqual(string.printable, result) def test_tangler_should_codeBegin(self) -> None: - self.tangler.open(self.filename) + 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() - with open("testtangler.code", "r") as result: - txt = result.read() + txt = self.filepath.read_text() self.assertEqual("*The* `Code`\n", txt) @} @@ -390,42 +382,41 @@ need to wait for a full second to elapse or we need to mock the various class TestTanglerMake(unittest.TestCase): def setUp(self) -> None: self.tangler = pyweb.TanglerMake() - self.filename = "testtangler.code" + self.filepath = Path("testtangler.code") self.aChunk = MockChunk("Chunk", 314, 278) #self.aChunk.references_list = [ ("Container", 123) ] - self.tangler.open(self.filename) + 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 = os.path.getmtime(self.filename) - self.original = os.lstat(self.filename) + self.time_original = self.filepath.stat().st_mtime + self.original = self.filepath.stat() #time.sleep(0.75) # Alternative to assure timestamps must be different def tearDown(self) -> None: - import os try: - os.remove("testtangler.code") + self.filepath.unlink() except OSError: pass def test_same_should_leave(self) -> None: - self.tangler.open(self.filename) + 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, os.lstat(self.filename))) - #self.assertEqual(self.time_original, os.path.getmtime(self.filename)) + 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.filename) + 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, os.lstat(self.filename))) - #self.assertNotEqual(self.time_original, os.path.getmtime(self.filename)) + self.assertFalse(os.path.samestat(self.original, self.filepath.stat())) + #self.assertNotEqual(self.time_original, self.filepath.stat().st_mtime) @} Chunk Tests @@ -982,7 +973,7 @@ class TestWebConstruction(unittest.TestCase): class TestWebProcessing(unittest.TestCase): def setUp(self) -> None: self.web = pyweb.Web() - self.web.webFileName = "TestWebProcessing.w" + self.web.web_path = Path("TestWebProcessing.w") self.chunk = pyweb.Chunk() self.chunk.appendText("some text") self.chunk.webAdd(self.web) @@ -1262,14 +1253,13 @@ class TestLoadAction(unittest.TestCase): self.action.web = self.web self.action.options = argparse.Namespace( webReader = self.webReader, - webFileName="TestLoadAction.w", + source_path=Path("TestLoadAction.w"), command="@@", permitList = [], ) - with open("TestLoadAction.w","w") as web: - pass + Path("TestLoadAction.w").write_text("") def tearDown(self) -> None: try: - os.remove("TestLoadAction.w") + Path("TestLoadAction.w").unlink() except IOError: pass def test_should_execute_loading(self) -> None: @@ -1296,6 +1286,7 @@ import argparse import io import logging import os +from pathlib import Path import re import string import time diff --git a/tests.w b/tests.w index b153176..ac181af 100644 --- a/tests.w +++ b/tests.w @@ -1,4 +1,4 @@ -.. pyweb/test.w +.. py-web-tool/test.w Unit Tests =========== diff --git a/todo.w b/todo.w index a0694ba..af048bc 100644 --- a/todo.w +++ b/todo.w @@ -1,4 +1,4 @@ -.. pyweb/todo.w +.. py-web-tool/todo.w Python 3.10 Migration ===================== @@ -8,21 +8,28 @@ Python 3.10 Migration #. [x] Replace all ``.format()`` with f-strings. -#. [ ] Replace filename strings (and ``os.path``) with ``pathlib.Path``. +#. [x] Replace filename strings (and ``os.path``) with ``pathlib.Path``. -#. [ ] Introduce ``match`` statements for some of the ``elif`` blocks +#. [x] Add ``abc`` to formalize Abstract Base Classes. -#. [ ] Introduce pytest instead of building a test runner. +#. [x] Use ``match`` statements for some of the ``elif`` blocks. -#. [ ] ``pyproject.toml``. This requires -o dir to write output to a directory of choice; which requires Pathlib +#. [x] Introduce pytest instead of building a test runner from ``runner.w``. + +#. [ ] ``pyproject.toml``. This requires ```-o dir`` option to write output to a directory of choice; which requires ``pathlib``. -#. [ ] Replace various mock classes with ``unittest.mock.Mock`` objects. +#. [ ] Rename the module from ``pyweb`` to ``pylpweb`` to avoid namespace squatting issues. + Rename the project from ``py-web-tool`` to ``py-lpweb-tool``. + +#. [ ] Replace various mock classes with ``unittest.mock.Mock`` objects and appropriate extended testing. To Do ======= -1. Silence the logging during testing. +1. Silence the ERROR-level logging during testing. + +2. Silence the error when creating an empty file i.e. ``.nojekyll`` #. Add a JSON-based (or TOML) configuration file to configure templates. From 348253aad08c0edd22cc5cc39c2fb3981f440cba Mon Sep 17 00:00:00 2001 From: "S.Lott" Date: Sat, 11 Jun 2022 07:36:04 -0400 Subject: [PATCH 3/8] Add ``-o dir`` option to write output to a directory of choice, simplifying **tox** setup. --- done.w | 6 ++-- impl.w | 73 ++++++++++++++------------------------------ pyweb.py | 70 +++++++++++++++++++++--------------------- test/func.w | 9 +----- test/pyweb_test.html | 35 +++++++++++---------- test/pyweb_test.rst | 35 +++++++++++---------- test/test_unit.py | 24 +++++++++------ test/test_weaver.py | 9 +----- test/testtangler | 1 - test/testweaver.rst | 4 --- test/unit.w | 24 +++++++++------ testweaver.rst | 4 --- todo.w | 8 ++++- 13 files changed, 133 insertions(+), 169 deletions(-) delete mode 100644 test/testtangler delete mode 100644 test/testweaver.rst delete mode 100644 testweaver.rst diff --git a/done.w b/done.w index 8ff5a64..a346053 100644 --- a/done.w +++ b/done.w @@ -7,15 +7,15 @@ Changes for 3.1 - Change to Python 3.10. -- Add type hints, f-strings, pathlib, abc.ABC +- Add type hints, f-strings, pathlib, abc.ABC. -- Replace some complex elif blocks with match statements +- 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``. - +- Add ``-o dir`` option to write output to a directory of choice, simplifying **tox** setup. Changes for 3.0 diff --git a/impl.w b/impl.w index 627c9fa..3cf312a 100644 --- a/impl.w +++ b/impl.w @@ -276,7 +276,8 @@ import abc class Emitter: """Emit an output file; handling indentation context.""" code_indent = 0 # Used by a Tangler - filePath : Path + filePath : Path # File within the base directory + output : Path # Base directory theFile: TextIO def __init__(self) -> None: @@ -319,9 +320,11 @@ characters to the file. @{ def open(self, aPath: Path) -> "Emitter": """Open a file.""" - self.filePath = aPath + if not hasattr(self, 'output'): + self.output = Path.cwd() + self.filePath = self.output / aPath.name self.linesWritten = 0 - self.doOpen(aPath) + self.doOpen() return self @ @@ -354,7 +357,7 @@ methods are overridden by the various subclasses to perform the unique operation for the subclass. @d Emitter doOpen... @{ -def doOpen(self, aFile: Path) -> None: +def doOpen(self) -> None: self.logger.debug("Creating %r", self.filePath) @| doOpen @} @@ -635,8 +638,9 @@ we're not always starting a fresh line with ``weaveReferenceTo()``. @d Weaver doOpen... @{ -def doOpen(self, basename: Path) -> None: - self.filePath = basename.with_suffix(self.extension) +def doOpen(self) -> None: + """Create the final woven document.""" + self.filePath = self.filePath.with_suffix(self.extension) self.logger.info("Weaving %r", self.filePath) self.theFile = self.filePath.open("w") self.readdIndent(self.code_indent) @@ -1325,11 +1329,12 @@ actual file created by open. def checkPath(self) -> None: self.filePath.parent.mkdir(parents=True, exist_ok=True) -def doOpen(self, aFile: Path) -> None: - self.filePath = aFile +def doOpen(self) -> None: + """Tangle out of the output files.""" self.checkPath() self.theFile = self.filePath.open("w") - self.logger.info("Tangling %r", aFile) + self.logger.info("Tangling %r", self.filePath) + def doClose(self) -> None: self.theFile.close() self.logger.info("Wrote %d lines to %r", self.linesWritten, self.filePath) @@ -1417,10 +1422,10 @@ a "touch" if the new file is the same as the original. @d TanglerMake doOpen... @{ -def doOpen(self, aFile: Path) -> None: +def doOpen(self) -> None: fd, self.tempname = tempfile.mkstemp(dir=os.curdir) self.theFile = os.fdopen(fd, "w") - self.logger.info("Tangling %r", aFile) + self.logger.info("Tangling %r", self.filePath) @| doOpen @} @@ -3665,22 +3670,6 @@ def handleCommand(self, token: str) -> bool: @| handleCommand @} -The following sequence of ``if``-``elif`` statements identifies -the structural commands that partition the input into separate ``Chunks``. - -:: - - @@d OLD major commands... - @@{ - if token[:2] == self.cmdo: - @@ - elif token[:2] == self.cmdd: - @@ - elif token[:2] == self.cmdi: - @@ - elif token[:2] in (self.cmdrcurl,self.cmdrbrak): - @@ - @@} An output chunk has the form ``@@o`` *name* ``@@{`` *content* ``@@}``. We use the first two tokens to name the ``OutputChunk``. We simply expect @@ -3821,27 +3810,6 @@ self.aChunk.webAdd(self.theWeb) The following sequence of ``elif`` statements identifies the minor commands that add ``Command`` instances to the current open ``Chunk``. -:: - - @@d OLD minor commands... - @@{ - elif token[:2] == self.cmdpipe: - @@ - 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: - @@ - elif token[:2] == self.cmdlexpr: - @@ - elif token[:2] == self.cmdcmd: - @@ - @@} - - User identifiers occur after a ``@@|`` in a ``NamedChunk``. Note that no check is made to assure that the previous ``Chunk`` was indeed a named @@ -4310,7 +4278,6 @@ some log file, ``source.log``. The third step runs **py-web-tool** excluding t 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 **py-web-tool** application. This class hierarchy defines an extensible set of fundamental actions. This gives us the flexibility to create a simple sequence @@ -4532,6 +4499,7 @@ def __call__(self) -> None: 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") @@ -4589,6 +4557,7 @@ with any of ``@@d`` or ``@@o`` and use ``@@{`` ``@@}`` brackets. 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: @@ -4977,8 +4946,9 @@ self.defaults = argparse.Namespace( 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() @@ -5012,6 +4982,7 @@ def parseArgs(self, argv: list[str]) -> argparse.Namespace: 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_directory", action="store", type=Path) p.add_argument("files", nargs='+', type=Path) config = p.parse_args(argv, namespace=self.defaults) self.expand(config) @@ -5029,6 +5000,7 @@ def expand(self, config: argparse.Namespace) -> argparse.Namespace: case _: raise Error("Improper configuration") + # Weaver try: weaver_class = weavers[config.weaver.lower()] except KeyError: @@ -5039,6 +5011,7 @@ def expand(self, config: argparse.Namespace) -> argparse.Namespace: raise TypeError(f"{weaver_class!r} not a subclass of Weaver") config.theWeaver = weaver_class() + # Tangler config.theTangler = TanglerMake() if config.permit: diff --git a/pyweb.py b/pyweb.py index 5d77dde..193559f 100644 --- a/pyweb.py +++ b/pyweb.py @@ -1,30 +1,10 @@ #!/usr/bin/env python """py-web-tool Literate Programming. -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. +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""" @@ -1109,7 +1089,8 @@ def expect(self, tokens: Iterable[str]) -> str | None: class Emitter: """Emit an output file; handling indentation context.""" code_indent = 0 # Used by a Tangler - filePath : Path + filePath : Path # File within the base directory + output : Path # Base directory theFile: TextIO def __init__(self) -> None: @@ -1131,13 +1112,15 @@ def __str__(self) -> str: def open(self, aPath: Path) -> "Emitter": """Open a file.""" - self.filePath = aPath + if not hasattr(self, 'output'): + self.output = Path.cwd() + self.filePath = self.output / aPath.name self.linesWritten = 0 - self.doOpen(aPath) + self.doOpen() return self - def doOpen(self, aFile: Path) -> None: + def doOpen(self) -> None: self.logger.debug("Creating %r", self.filePath) @@ -1267,8 +1250,9 @@ def __init__(self) -> None: super().__init__() - def doOpen(self, basename: Path) -> None: - self.filePath = basename.with_suffix(self.extension) + def doOpen(self) -> None: + """Create the final woven document.""" + self.filePath = self.filePath.with_suffix(self.extension) self.logger.info("Weaving %r", self.filePath) self.theFile = self.filePath.open("w") self.readdIndent(self.code_indent) @@ -1590,11 +1574,12 @@ def __init__(self) -> None: def checkPath(self) -> None: self.filePath.parent.mkdir(parents=True, exist_ok=True) - def doOpen(self, aFile: Path) -> None: - self.filePath = aFile + def doOpen(self) -> None: + """Tangle out of the output files.""" self.checkPath() self.theFile = self.filePath.open("w") - self.logger.info("Tangling %r", aFile) + self.logger.info("Tangling %r", self.filePath) + def doClose(self) -> None: self.theFile.close() self.logger.info("Wrote %d lines to %r", self.linesWritten, self.filePath) @@ -1626,10 +1611,10 @@ def __init__(self, *args: Any) -> None: super().__init__(*args) - def doOpen(self, aFile: Path) -> None: + def doOpen(self) -> None: fd, self.tempname = tempfile.mkstemp(dir=os.curdir) self.theFile = os.fdopen(fd, "w") - self.logger.info("Tangling %r", aFile) + self.logger.info("Tangling %r", self.filePath) @@ -1710,6 +1695,7 @@ def __call__(self) -> None: self.start = time.process_time() + def duration(self) -> float: """Return duration of the action.""" @@ -1741,11 +1727,13 @@ def __call__(self) -> None: o() + def append(self, anAction: Action) -> None: self.opSequence.append(anAction) + def summary(self) -> str: return ", ".join([o.summary() for o in self.opSequence]) @@ -1770,6 +1758,7 @@ def __call__(self) -> None: 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") @@ -1778,6 +1767,7 @@ def __call__(self) -> None: #raise + def summary(self) -> str: if self.options.theWeaver and self.options.theWeaver.linesWritten > 0: @@ -1799,6 +1789,7 @@ def __init__(self) -> None: 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: @@ -1806,6 +1797,7 @@ def __call__(self) -> None: #raise + def summary(self) -> str: if self.options.theTangler and self.options.theTangler.linesWritten > 0: @@ -1825,6 +1817,7 @@ def __init__(self) -> None: def __str__(self) -> str: return f"Load [{self.webReader!s}, {self.web!s}]" + def __call__(self) -> None: super().__call__() self.webReader = self.options.webReader @@ -1849,6 +1842,7 @@ def __call__(self) -> None: raise + def summary(self) -> str: return ( @@ -1873,8 +1867,9 @@ def __init__(self) -> None: 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() @@ -1899,6 +1894,7 @@ def parseArgs(self, argv: list[str]) -> argparse.Namespace: 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_directory", action="store", type=Path) p.add_argument("files", nargs='+', type=Path) config = p.parse_args(argv, namespace=self.defaults) self.expand(config) @@ -1916,6 +1912,7 @@ def expand(self, config: argparse.Namespace) -> argparse.Namespace: case _: raise Error("Improper configuration") + # Weaver try: weaver_class = weavers[config.weaver.lower()] except KeyError: @@ -1926,6 +1923,7 @@ def expand(self, config: argparse.Namespace) -> argparse.Namespace: raise TypeError(f"{weaver_class!r} not a subclass of Weaver") config.theWeaver = weaver_class() + # Tangler config.theTangler = TanglerMake() if config.permit: diff --git a/test/func.w b/test/func.w index b124ea5..a777416 100644 --- a/test/func.w +++ b/test/func.w @@ -433,14 +433,7 @@ class WeaveTestcase(unittest.TestCase): self.source = io.StringIO(self.text) self.web = pyweb.Web() self.rdr = pyweb.WebReader() - 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(".html").unlink() diff --git a/test/pyweb_test.html b/test/pyweb_test.html index f021b80..8c65b6f 100644 --- a/test/pyweb_test.html +++ b/test/pyweb_test.html @@ -596,7 +596,7 @@

    Emitter Tests

    Unit Test of Emitter Superclass (3) =

     class EmitterExtension(pyweb.Emitter):
    -    def doOpen(self, fileName: str) -> None:
    +    def doOpen(self) -> None:
             self.theFile = io.StringIO()
         def doClose(self) -> None:
             self.theFile.flush()
    @@ -605,18 +605,18 @@ 

    Emitter Tests

    def setUp(self) -> None: self.emitter = EmitterExtension() def test_emitter_should_open_close_write(self) -> None: - self.emitter.open("test.tmp") + 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("test.tmp") + 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("test.tmp") + self.emitter.open(Path("test.tmp")) self.emitter.codeBlock("Begin\n") self.emitter.addIndent(4) self.emitter.codeBlock("More Code\n") @@ -625,7 +625,7 @@

    Emitter Tests

    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("test.tmp") + self.emitter.open(Path("test.tmp")) self.emitter.codeBlock("Begin\n") self.emitter.setIndent(0) self.emitter.codeBlock("More Code\n") @@ -679,7 +679,7 @@

    Emitter Tests

    self.aChunk.referencedBy = [self.aFileChunk] def tearDown(self) -> None: try: - self.filepath.unlink() + self.filepath.with_suffix('.rst').unlink() except OSError: pass @@ -1800,7 +1800,9 @@

    Action Tests

    self.action.web = self.web self.action.options = argparse.Namespace( theWeaver=self.weaver, - reference_style=pyweb.SimpleReference() ) + reference_style=pyweb.SimpleReference(), + output=Path.cwd(), + ) def test_should_execute_weaving(self) -> None: self.action() self.assertTrue(self.web.wove is self.weaver) @@ -1819,7 +1821,9 @@

    Action Tests

    self.action.web = self.web self.action.options = argparse.Namespace( theTangler = self.tangler, - tangler_line_numbers = False, ) + tangler_line_numbers = False, + output=Path.cwd() + ) def test_should_execute_tangling(self) -> None: self.action() self.assertTrue(self.web.tangled is self.tangler) @@ -1840,7 +1844,9 @@

    Action Tests

    webReader = self.webReader, source_path=Path("TestLoadAction.w"), command="@", - permitList = [], ) + permitList = [], + output=Path.cwd(), + ) Path("TestLoadAction.w").write_text("") def tearDown(self) -> None: try: @@ -2407,14 +2413,7 @@

    Tests for Weaving

    self.source = io.StringIO(self.text) self.web = pyweb.Web() self.rdr = pyweb.WebReader() - 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(".html").unlink() @@ -3011,7 +3010,7 @@

    User Identifiers

    (None)


    -Created by ../pyweb.py at Fri Jun 10 17:08:42 2022.
    +Created by ../pyweb.py at Sat Jun 11 07:35:24 2022.

    Source pyweb_test.w modified Fri Jun 10 17:07:24 2022.

    pyweb.__version__ '3.1'.

    diff --git a/test/pyweb_test.rst b/test/pyweb_test.rst index 5a0e1f8..eff04a5 100644 --- a/test/pyweb_test.rst +++ b/test/pyweb_test.rst @@ -240,7 +240,7 @@ emitter is Tangler-like. class EmitterExtension(pyweb.Emitter): - def doOpen(self, fileName: str) -> None: + def doOpen(self) -> None: self.theFile = io.StringIO() def doClose(self) -> None: self.theFile.flush() @@ -249,18 +249,18 @@ emitter is Tangler-like. def setUp(self) -> None: self.emitter = EmitterExtension() def test\_emitter\_should\_open\_close\_write(self) -> None: - self.emitter.open("test.tmp") + 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("test.tmp") + 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("test.tmp") + self.emitter.open(Path("test.tmp")) self.emitter.codeBlock("Begin\\n") self.emitter.addIndent(4) self.emitter.codeBlock("More Code\\n") @@ -269,7 +269,7 @@ emitter is Tangler-like. 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("test.tmp") + self.emitter.open(Path("test.tmp")) self.emitter.codeBlock("Begin\\n") self.emitter.setIndent(0) self.emitter.codeBlock("More Code\\n") @@ -341,7 +341,7 @@ The default Weaver is an Emitter that uses templates to produce RST markup. self.aChunk.referencedBy = [self.aFileChunk] def tearDown(self) -> None: try: - self.filepath.unlink() + self.filepath.with\_suffix('.rst').unlink() except OSError: pass @@ -1808,7 +1808,9 @@ load, tangle, weave. self.action.web = self.web self.action.options = argparse.Namespace( theWeaver=self.weaver, - reference\_style=pyweb.SimpleReference() ) + reference\_style=pyweb.SimpleReference(), + output=Path.cwd(), + ) def test\_should\_execute\_weaving(self) -> None: self.action() self.assertTrue(self.web.wove is self.weaver) @@ -1835,7 +1837,9 @@ load, tangle, weave. self.action.web = self.web self.action.options = argparse.Namespace( theTangler = self.tangler, - tangler\_line\_numbers = False, ) + tangler\_line\_numbers = False, + output=Path.cwd() + ) def test\_should\_execute\_tangling(self) -> None: self.action() self.assertTrue(self.web.tangled is self.tangler) @@ -1864,7 +1868,9 @@ load, tangle, weave. webReader = self.webReader, source\_path=Path("TestLoadAction.w"), command="@", - permitList = [], ) + permitList = [], + output=Path.cwd(), + ) Path("TestLoadAction.w").write\_text("") def tearDown(self) -> None: try: @@ -2690,14 +2696,7 @@ Weaving test cases have a common setup shown in this superclass. self.source = io.StringIO(self.text) self.web = pyweb.Web() self.rdr = pyweb.WebReader() - 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(".html").unlink() @@ -3338,7 +3337,7 @@ User Identifiers .. class:: small - Created by ../pyweb.py at Fri Jun 10 17:08:42 2022. + Created by ../pyweb.py at Sat Jun 11 07:35:24 2022. Source pyweb_test.w modified Fri Jun 10 17:07:24 2022. diff --git a/test/test_unit.py b/test/test_unit.py index 1eb4296..9212b9c 100644 --- a/test/test_unit.py +++ b/test/test_unit.py @@ -37,7 +37,7 @@ def tangle(self, aWeb: "Web", aTangler: "Tangler") -> None: class EmitterExtension(pyweb.Emitter): - def doOpen(self, fileName: str) -> None: + def doOpen(self) -> None: self.theFile = io.StringIO() def doClose(self) -> None: self.theFile.flush() @@ -46,18 +46,18 @@ class TestEmitter(unittest.TestCase): def setUp(self) -> None: self.emitter = EmitterExtension() def test_emitter_should_open_close_write(self) -> None: - self.emitter.open("test.tmp") + 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("test.tmp") + 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("test.tmp") + self.emitter.open(Path("test.tmp")) self.emitter.codeBlock("Begin\n") self.emitter.addIndent(4) self.emitter.codeBlock("More Code\n") @@ -66,7 +66,7 @@ def test_emitter_should_indent(self) -> None: 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("test.tmp") + self.emitter.open(Path("test.tmp")) self.emitter.codeBlock("Begin\n") self.emitter.setIndent(0) self.emitter.codeBlock("More Code\n") @@ -87,7 +87,7 @@ def setUp(self) -> None: self.aChunk.referencedBy = [self.aFileChunk] def tearDown(self) -> None: try: - self.filepath.unlink() + self.filepath.with_suffix('.rst').unlink() except OSError: pass @@ -890,7 +890,9 @@ def setUp(self) -> None: webReader = self.webReader, source_path=Path("TestLoadAction.w"), command="@", - permitList = [], ) + permitList = [], + output=Path.cwd(), + ) Path("TestLoadAction.w").write_text("") def tearDown(self) -> None: try: @@ -910,7 +912,9 @@ def setUp(self) -> None: self.action.web = self.web self.action.options = argparse.Namespace( theTangler = self.tangler, - tangler_line_numbers = False, ) + tangler_line_numbers = False, + output=Path.cwd() + ) def test_should_execute_tangling(self) -> None: self.action() self.assertTrue(self.web.tangled is self.tangler) @@ -924,7 +928,9 @@ def setUp(self) -> None: self.action.web = self.web self.action.options = argparse.Namespace( theWeaver=self.weaver, - reference_style=pyweb.SimpleReference() ) + reference_style=pyweb.SimpleReference(), + output=Path.cwd(), + ) def test_should_execute_weaving(self) -> None: self.action() self.assertTrue(self.web.wove is self.weaver) diff --git a/test/test_weaver.py b/test/test_weaver.py index dc0f9e2..fd370ca 100644 --- a/test/test_weaver.py +++ b/test/test_weaver.py @@ -18,14 +18,7 @@ def setUp(self) -> None: self.source = io.StringIO(self.text) self.web = pyweb.Web() self.rdr = pyweb.WebReader() - 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(".html").unlink() diff --git a/test/testtangler b/test/testtangler deleted file mode 100644 index 987558a..0000000 --- a/test/testtangler +++ /dev/null @@ -1 +0,0 @@ -*The* `Code` diff --git a/test/testweaver.rst b/test/testweaver.rst deleted file mode 100644 index 2efd65a..0000000 --- a/test/testweaver.rst +++ /dev/null @@ -1,4 +0,0 @@ - -:Chunk: - `123`_ [`314`_] `567`_ - diff --git a/test/unit.w b/test/unit.w index 5e9935f..8f444be 100644 --- a/test/unit.w +++ b/test/unit.w @@ -133,7 +133,7 @@ emitter is Tangler-like. @d Unit Test of Emitter Superclass... @{ class EmitterExtension(pyweb.Emitter): - def doOpen(self, fileName: str) -> None: + def doOpen(self) -> None: self.theFile = io.StringIO() def doClose(self) -> None: self.theFile.flush() @@ -142,18 +142,18 @@ class TestEmitter(unittest.TestCase): def setUp(self) -> None: self.emitter = EmitterExtension() def test_emitter_should_open_close_write(self) -> None: - self.emitter.open("test.tmp") + 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("test.tmp") + 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("test.tmp") + self.emitter.open(Path("test.tmp")) self.emitter.codeBlock("Begin\n") self.emitter.addIndent(4) self.emitter.codeBlock("More Code\n") @@ -162,7 +162,7 @@ class TestEmitter(unittest.TestCase): 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("test.tmp") + self.emitter.open(Path("test.tmp")) self.emitter.codeBlock("Begin\n") self.emitter.setIndent(0) self.emitter.codeBlock("More Code\n") @@ -211,7 +211,7 @@ class TestWeaver(unittest.TestCase): self.aChunk.referencedBy = [self.aFileChunk] def tearDown(self) -> None: try: - self.filepath.unlink() + self.filepath.with_suffix('.rst').unlink() except OSError: pass @@ -1223,7 +1223,9 @@ class TestWeaveAction(unittest.TestCase): self.action.web = self.web self.action.options = argparse.Namespace( theWeaver=self.weaver, - reference_style=pyweb.SimpleReference() ) + reference_style=pyweb.SimpleReference(), + output=Path.cwd(), + ) def test_should_execute_weaving(self) -> None: self.action() self.assertTrue(self.web.wove is self.weaver) @@ -1238,7 +1240,9 @@ class TestTangleAction(unittest.TestCase): self.action.web = self.web self.action.options = argparse.Namespace( theTangler = self.tangler, - tangler_line_numbers = False, ) + tangler_line_numbers = False, + output=Path.cwd() + ) def test_should_execute_tangling(self) -> None: self.action() self.assertTrue(self.web.tangled is self.tangler) @@ -1255,7 +1259,9 @@ class TestLoadAction(unittest.TestCase): webReader = self.webReader, source_path=Path("TestLoadAction.w"), command="@@", - permitList = [], ) + permitList = [], + output=Path.cwd(), + ) Path("TestLoadAction.w").write_text("") def tearDown(self) -> None: try: diff --git a/testweaver.rst b/testweaver.rst deleted file mode 100644 index 2efd65a..0000000 --- a/testweaver.rst +++ /dev/null @@ -1,4 +0,0 @@ - -:Chunk: - `123`_ [`314`_] `567`_ - diff --git a/todo.w b/todo.w index af048bc..e0a3079 100644 --- a/todo.w +++ b/todo.w @@ -16,13 +16,19 @@ Python 3.10 Migration #. [x] Introduce pytest instead of building a test runner from ``runner.w``. -#. [ ] ``pyproject.toml``. This requires ```-o dir`` option to write output to a directory of choice; which requires ``pathlib``. +#. [x] Add ``-o dir`` option to write output to a directory of choice. Requires ``pathlib``. + +#. [ ] Finish ``pyproject.toml``. Requires ``-o dir`` option. + +#. [ ] Test cases for ``weave.py`` and ``tangle.py`` #. [ ] Rename the module from ``pyweb`` to ``pylpweb`` to avoid namespace squatting issues. Rename the project from ``py-web-tool`` to ``py-lpweb-tool``. #. [ ] Replace various mock classes with ``unittest.mock.Mock`` objects and appropriate extended testing. +#. [ ] Separate ``tests``, ``examples``, and ``src`` from each other. Add ``bootstrap`` directory. + To Do ======= From 8c22074a57d9d3c8a1eaa8101e854aed758c8c58 Mon Sep 17 00:00:00 2001 From: "S.Lott" Date: Sat, 11 Jun 2022 08:28:38 -0400 Subject: [PATCH 4/8] Finish tox setup Finish ``pyproject.toml``. (Requires ``-o dir`` option.) Update Makefile, also. Add ``bootstrap`` directory with previous release. --- Makefile | 30 +- additional.w | 39 +- bootstrap/pyweb.py | 1914 +++++++++++++++++++++++++++++ done.w | 2 + {test => examples}/test_latex.log | 0 {test => examples}/test_latex.pdf | Bin {test => examples}/test_latex.tex | 0 {test => examples}/test_latex.w | 0 {test => examples}/test_rest.tex | 0 {test => examples}/test_rst.html | 0 {test => examples}/test_rst.log | 0 {test => examples}/test_rst.pdf | Bin {test => examples}/test_rst.rst | 0 {test => examples}/test_rst.tex | 0 {test => examples}/test_rst.w | 0 impl.w | 54 +- pyproject.toml | 9 +- pyweb.py | 54 +- pyweb.rst | 260 ++-- test/pyweb_test.html | 6 +- test/pyweb_test.rst | 6 +- todo.w | 12 +- 22 files changed, 2163 insertions(+), 223 deletions(-) create mode 100644 bootstrap/pyweb.py rename {test => examples}/test_latex.log (100%) rename {test => examples}/test_latex.pdf (100%) rename {test => examples}/test_latex.tex (100%) rename {test => examples}/test_latex.w (100%) rename {test => examples}/test_rest.tex (100%) rename {test => examples}/test_rst.html (100%) rename {test => examples}/test_rst.log (100%) rename {test => examples}/test_rst.pdf (100%) rename {test => examples}/test_rst.rst (100%) rename {test => examples}/test_rst.tex (100%) rename {test => examples}/test_rst.w (100%) diff --git a/Makefile b/Makefile index 6a33919..db1101a 100644 --- a/Makefile +++ b/Makefile @@ -1,26 +1,30 @@ # Makefile for py-web-tool. # Requires a pyweb-3.0.py (untouched) to bootstrap the current version. -SOURCE = pyweb.w intro.w overview.w impl.w tests.w additional.w todo.w done.w \ - test/pyweb_test.w test/intro.w test/unit.w test/func.w test/runner.w +SOURCE_PYLPWEB = pyweb.w intro.w overview.w impl.w tests.w additional.w todo.w done.w +TEST_PYLPWEB = test/pyweb_test.w test/intro.w test/unit.w test/func.w test/runner.w -.PHONY : test build +.PHONY : test doc weave build # Note the bootstrapping new version from version 3.0 as baseline. # Handy to keep this *outside* the project's Git repository. -PYWEB_BOOTSTRAP=/Users/slott/Documents/Projects/PyWebTool-3/pyweb/pyweb.py +PYLPWEB_BOOTSTRAP=bootstrap/pyweb.py -test : $(SOURCE) - python3 $(PYWEB_BOOTSTRAP) -xw pyweb.w - cd test && python3 ../pyweb.py pyweb_test.w +test : $(SOURCE_PYLPWEB) $(TEST_PYLPWEB) + python3 $(PYLPWEB_BOOTSTRAP) -xw pyweb.w + python3 pyweb.py test/pyweb_test.w -o test PYTHONPATH=${PWD} pytest - cd test && rst2html.py pyweb_test.rst pyweb_test.html - mypy --strict --show-error-codes pyweb.py + rst2html.py test/pyweb_test.rst test/pyweb_test.html + mypy --strict --show-error-codes pyweb.py tangle.py weave.py -build : pyweb.py pyweb.html - -pyweb.py pyweb.rst : $(SOURCE) - python3 $(PYWEB_BOOTSTRAP) pyweb.w +weave : pyweb.py tangle.py weave.py +doc : pyweb.html + +build : pyweb.py tangle.py weave.py pyweb.html + +pyweb.py pyweb.rst : $(SOURCE_PYLPWEB) + python3 $(PYLPWEB_BOOTSTRAP) pyweb.w + pyweb.html : pyweb.rst rst2html.py $< $@ diff --git a/additional.w b/additional.w index e9cc136..dfcf128 100644 --- a/additional.w +++ b/additional.w @@ -383,27 +383,31 @@ Note that there are tabs in this file. We bootstrap the next version from the 3. @{# Makefile for py-web-tool. # Requires a pyweb-3.0.py (untouched) to bootstrap the current version. -SOURCE = pyweb.w intro.w overview.w impl.w tests.w additional.w todo.w done.w \ - test/pyweb_test.w test/intro.w test/unit.w test/func.w test/runner.w +SOURCE_PYLPWEB = pyweb.w intro.w overview.w impl.w tests.w additional.w todo.w done.w +TEST_PYLPWEB = test/pyweb_test.w test/intro.w test/unit.w test/func.w test/runner.w -.PHONY : test build +.PHONY : test doc weave build # Note the bootstrapping new version from version 3.0 as baseline. # Handy to keep this *outside* the project's Git repository. -PYWEB_BOOTSTRAP=/Users/slott/Documents/Projects/PyWebTool-3/pyweb/pyweb.py +PYLPWEB_BOOTSTRAP=bootstrap/pyweb.py -test : $(SOURCE) - python3 $(PYWEB_BOOTSTRAP) -xw pyweb.w - cd test && python3 ../pyweb.py pyweb_test.w +test : $(SOURCE_PYLPWEB) $(TEST_PYLPWEB) + python3 $(PYLPWEB_BOOTSTRAP) -xw pyweb.w + python3 pyweb.py test/pyweb_test.w -o test PYTHONPATH=${PWD} pytest - cd test && rst2html.py pyweb_test.rst pyweb_test.html - mypy --strict --show-error-codes pyweb.py + rst2html.py test/pyweb_test.rst test/pyweb_test.html + mypy --strict --show-error-codes pyweb.py tangle.py weave.py -build : pyweb.py pyweb.html - -pyweb.py pyweb.rst : $(SOURCE) - python3 $(PYWEB_BOOTSTRAP) pyweb.w +weave : pyweb.py tangle.py weave.py +doc : pyweb.html + +build : pyweb.py tangle.py weave.py pyweb.html + +pyweb.py pyweb.rst : $(SOURCE_PYLPWEB) + python3 $(PYLPWEB_BOOTSTRAP) pyweb.w + pyweb.html : pyweb.rst rst2html.py $< $@ @} @@ -426,12 +430,13 @@ deps = pytest == 7.1.2 mypy == 0.910 setenv = - PYWEB_BOOTSTRAP = /Users/slott/Documents/Projects/PyWebTool-3/pyweb/pyweb.py + PYLPWEB_BOOTSTRAP = bootstrap/pyweb.py + PYTHONPATH = {toxinidir} commands_pre = - python3 {env:PYWEB_BOOTSTRAP} pyweb.w + python3 {env:PYLPWEB_BOOTSTRAP} pyweb.w python3 pyweb.py -o test test/pyweb_test.w commands = - python3 test/test.py - mypy --strict pyweb.py + pytest + mypy --strict --show-error-codes pyweb.py tangle.py weave.py """ @} diff --git a/bootstrap/pyweb.py b/bootstrap/pyweb.py new file mode 100644 index 0000000..58a7fe8 --- /dev/null +++ b/bootstrap/pyweb.py @@ -0,0 +1,1914 @@ +#!/usr/bin/env python +"""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. +""" +__version__ = """3.0""" + +### DO NOT EDIT THIS FILE! +### It was created by /Users/slott/Documents/Projects/PyWebTool-3/pyweb/pyweb.py, __version__='2.3.2'. +### From source pyweb.w modified Sat Jun 16 08:10:37 2018. +### In working directory '/Users/slott/Documents/Projects/PyWebTool-3/pyweb'. + +import string + +import tempfile +import filecmp + + +import weakref + + +import builtins +import sys +import platform + + +import re + + +import shlex + + +import os +import time +import datetime +import types + +import argparse + + +import logging +import logging.config + + + + +class Error( Exception ): pass + + + +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) + + def startswith( self, prefix ): + return None + def searchForRE( self, rePat ): + return None + def indent( self ): + return None + + + + def ref( self, aWeb ): + return None + def weave( self, aWeb, aWeaver ): + pass + def tangle( self, aWeb, aTangler ): + pass + + + + + +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 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 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 FileXrefCommand( XrefCommand ): + """A FileXref command.""" + def weave( self, aWeb, aWeaver ): + """Weave a File Xref from @o commands.""" + self.formatXref( aWeb.fileXref(), aWeaver ) + + + +class MacroXrefCommand( XrefCommand ): + """A MacroXref command.""" + def weave( self, aWeb, aWeaver ): + """Weave the Macro Xref from @d commands.""" + self.formatXref( aWeb.chunkXref(), aWeaver ) + + + +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() + + + +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) + + def resolve( self, aWeb ): + """Expand our chunk name and list of parts""" + self.fullName= aWeb.fullNameFor( self.refTo ) + self.chunkList= aWeb.getchunk( self.refTo ) + + + + def ref( self, aWeb ): + """Find and return the full name for this reference.""" + self.resolve( aWeb ) + return self.fullName + + + + def weave( self, aWeb, aWeaver ): + """Create the nicely formatted reference to a chunk of code.""" + self.resolve( aWeb ) + aWeb.weaveChunk( self.fullName, aWeaver ) + + + + 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 ) + + + + + + + +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 ) + + def append( self, command ): + """Add another Command to this chunk.""" + self.commands.append( command ) + command.chunk= self + + + + 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 webAdd( self, web ): + """Add self to a Web as anonymous chunk.""" + web.add( self ) + + + + + 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 + + + + def makeContent( self, text, lineNumber=0 ): + return TextCommand( text, lineNumber ) + + + + 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 [] + + + + 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 ) ] + + + + 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.""") + + + + def tangle( self, aWeb, aTangler ): + """Create source code -- except anonymous chunks should not be tangled""" + raise Error( 'Cannot tangle an anonymous chunk', self ) + + + + 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() + + + + +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 ) + + 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 + + + + def webAdd( self, web ): + """Add self to a Web as named chunk, update xrefs.""" + web.addNamed( self ) + + + + 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 ) + + + + 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 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() + + +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 + + def webAdd( self, web ): + """Add self to a Web as output chunk, update xrefs.""" + web.addOutput( self ) + + + + 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 ) + + + + def tangle( self, aWeb, aTangler ): + 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, lineNumber=0 ): + return TextCommand( text, lineNumber ) + + 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 ) + + + + def tangle( self, aWeb, aTangler ): + """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 ): + 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 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 + + + + def add( self, chunk ): + """Add an anonymous chunk.""" + self.chunkSeq.append( chunk ) + chunk.web= weakref.ref(self) + + + + 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) + + + + 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 + + + + + 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 + + + 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()) ) + + + + 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 + + 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 + + + + 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 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 + + + 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 ): + + for n,cList in chunkMap.items(): + for c in cList: + for id in c.getUserIDRefs(): + ux[id]= ( c.seq, [] ) + + def _updateUserId( self, chunkMap, ux ): + + # 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 ) + + + + + 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() + + + + def tangle( self, aTangler ): + for f, c in self.output.items(): + with aTangler.open(f): + for p in c: + p.tangle( self, aTangler ) + + + + 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 ) + + + + + +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 OptionDef: + def __init__( self, name, **kw ): + self.name= name + self.__dict__.update( kw ) + +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) + + +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 + + + # 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 ): + return self.__class__.__name__ + + def location( self ): + return (self.fileName, self.tokenizer.lineNumber+1) + + + + 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 ) + + + + def handleCommand( self, token ): + self.logger.debug( "Reading {!r}".format(token) ) + + if token[:2] == self.cmdo: + + 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 @} + + elif token[:2] == self.cmdd: + + 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 @] + + elif token[:2] == self.cmdi: + + 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 ) + + elif token[:2] in (self.cmdrcurl,self.cmdrbrak): + + self.aChunk= Chunk() + self.aChunk.webAdd( self.theWeb ) + + + + elif token[:2] == self.cmdpipe: + + 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 + + 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: + + # 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) ) + + elif token[:2] == self.cmdlexpr: + + # 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 ) + + elif token[:2] == self.cmdcmd: + + self.aChunk.appendText( self.command, self.tokenizer.lineNumber ) + + + 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 + + + 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 + + + + + +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__ + + def open( self, aFile ): + """Open a file.""" + self.fileName= aFile + self.linesWritten= 0 + self.doOpen( aFile ) + return self + + def doOpen( self, aFile ): + self.logger.debug( "creating {!r}".format(self.fileName) ) + + + def close( self ): + self.codeFinish() # Trailing newline for tangler only. + self.doClose() + self.totalFiles += 1 + self.totalLines += self.linesWritten + + def doClose( self ): + self.logger.debug( "wrote {:d} lines to {!s}".format( + self.linesWritten, self.fileName) ) + + + 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 + + + + + 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 + + + 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 + + + def codeFinish( self ): + if self.fragment: + self.write('\n') + + + + 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) ) + + + + + +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 + header= """\n.. include:: \n.. include:: \n""" + + def __init__( self ): + super().__init__() + self.reference_style= None # Must be configured. + + + 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 + + + + # Template Expansions. + + + quoted_chars = [ + # prevent some RST markup from being recognized + ('\\',r'\\'), # Must be first. + ('`',r'\`'), + ('_',r'\_'), + ('*',r'\*'), + ('|',r'\|'), + ] + + + def docBegin( self, aChunk ): + pass + def docEnd( self, aChunk ): + pass + + + + 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 "" + + + + 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) + + + + 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 ) + + + + 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 + + + + 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() ) + + 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 RST(Weaver): + pass + + +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""" + + + 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 + + + + ce_template= string.Template(""" + \\end{Verbatim} + ${references} + \\end{flushleft}\n""") # Prevent indentation + + + + fb_template= cb_template + + + + fe_template= ce_template + + + + 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""") + + + + quoted_chars = [ + ("\\end{Verbatim}", "\\end\,{Verbatim}"), # Allow \end{Verbatim} + ("\\{","\\\,{"), # 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(""" + + +

    ${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}.' ) + + + + quoted_chars = [ + ("&", "&"), # Must be first + ("<", "<"), + (">", ">"), + ('"', """), + ] + + + + 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 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 ): + super().__init__() + self.comment_start= None + self.comment_end= "" + self.include_line_numbers= False + + 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) ) + + + + def codeBegin( self, aChunk ): + self.log_indent.debug( "{!s}".format(aChunk.fullName) ) + + + + + +class TanglerMake( Tangler ): + """Tangle output files, leaving files untouched if there are no changes.""" + def __init__( self, *args ): + super().__init__( *args ) + self.tempname= None + + 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 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 Reference: + def __init__( self ): + self.logger= logging.getLogger( self.__class__.__qualname__ ) + def chunkReferencedBy( self, aChunk ): + """Return a list of Chunks.""" + pass + +class SimpleReference( Reference ): + def chunkReferencedBy( self, aChunk ): + refBy= aChunk.referencedBy + return refBy + +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 + + + + +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 ) + + def __call__( self ): + self.logger.info( "Starting {!s}".format(self.name) ) + self.start= time.process_time() + + + + 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 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 ] ) + + def __call__( self ): + for o in self.opSequence: + o.web= self.web + o.options= self.options + o() + + + + def append( self, anAction ): + self.opSequence.append( anAction ) + + + + def summary( self ): + return ", ".join( [ o.summary() for o in self.opSequence ] ) + + + + + +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 __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 + + + + 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 TangleAction( Action ): + """Tangle source files.""" + def __init__( self ): + super().__init__( "Tangle" ) + + 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 + + + + 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, ) + + + + + +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 ) + + 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 + + + + 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 Application: + def __init__( self ): + 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, + ) + 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 ): + 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 + + + + + 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() ) + + + + +# Global list of available weaver classes. +weavers = { + 'html': HTML, + 'htmlshort': HTMLShort, + 'latex': LaTeX, + 'rst': RST, +} + + +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 + +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 }, + }, +) + + +def main(): + a= Application() + config= a.parseArgs() + a.process(config) + +if __name__ == "__main__": + with Logger( log_config ): + main( ) + diff --git a/done.w b/done.w index a346053..8e80fb4 100644 --- a/done.w +++ b/done.w @@ -17,6 +17,8 @@ Changes for 3.1 - Add ``-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. + Changes for 3.0 - Move to GitHub diff --git a/test/test_latex.log b/examples/test_latex.log similarity index 100% rename from test/test_latex.log rename to examples/test_latex.log diff --git a/test/test_latex.pdf b/examples/test_latex.pdf similarity index 100% rename from test/test_latex.pdf rename to examples/test_latex.pdf diff --git a/test/test_latex.tex b/examples/test_latex.tex similarity index 100% rename from test/test_latex.tex rename to examples/test_latex.tex diff --git a/test/test_latex.w b/examples/test_latex.w similarity index 100% rename from test/test_latex.w rename to examples/test_latex.w diff --git a/test/test_rest.tex b/examples/test_rest.tex similarity index 100% rename from test/test_rest.tex rename to examples/test_rest.tex diff --git a/test/test_rst.html b/examples/test_rst.html similarity index 100% rename from test/test_rst.html rename to examples/test_rst.html diff --git a/test/test_rst.log b/examples/test_rst.log similarity index 100% rename from test/test_rst.log rename to examples/test_rst.log diff --git a/test/test_rst.pdf b/examples/test_rst.pdf similarity index 100% rename from test/test_rst.pdf rename to examples/test_rst.pdf diff --git a/test/test_rst.rst b/examples/test_rst.rst similarity index 100% rename from test/test_rst.rst rename to examples/test_rst.rst diff --git a/test/test_rst.tex b/examples/test_rst.tex similarity index 100% rename from test/test_rst.tex rename to examples/test_rst.tex diff --git a/test/test_rst.w b/examples/test_rst.w similarity index 100% rename from test/test_rst.w rename to examples/test_rst.w diff --git a/impl.w b/impl.w index 3cf312a..de363f7 100644 --- a/impl.w +++ b/impl.w @@ -276,8 +276,8 @@ import abc class Emitter: """Emit an output file; handling indentation context.""" code_indent = 0 # Used by a Tangler - filePath : Path # File within the base directory - output : Path # Base directory + filePath : Path # Path within the base directory (on the name is used) + output : Path # Base directory to write theFile: TextIO def __init__(self) -> None: @@ -323,6 +323,7 @@ def open(self, aPath: Path) -> "Emitter": 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 @@ -641,7 +642,7 @@ we're not always starting a fresh line with ``weaveReferenceTo()``. def doOpen(self) -> None: """Create the final woven document.""" self.filePath = self.filePath.with_suffix(self.extension) - self.logger.info("Weaving %r", self.filePath) + self.logger.info("Weaving '%s'", self.filePath) self.theFile = self.filePath.open("w") self.readdIndent(self.code_indent) @@ -813,7 +814,7 @@ a simple ``" "`` because it looks better. @d Weaver reference command... @{ refto_name_template = string.Template(r"|srarr|\ ${fullName} (`${seq}`_)") -refto_seq_template = string.Template("|srarr|\ (`${seq}`_)") +refto_seq_template = string.Template(r"|srarr|\ (`${seq}`_)") refto_seq_separator = ", " def referenceTo(self, aName: str | None, seq: int) -> str: @@ -1049,8 +1050,8 @@ block. Our one compromise is a thin space if the phrase @d LaTeX write a line... @{ quoted_chars: list[tuple[str, str]] = [ - ("\\end{Verbatim}", "\\end\,{Verbatim}"), # Allow \end{Verbatim} in a Verbatim context - ("\\{", "\\\,{"), # Prevent unexpected commands in Verbatim + ("\\end{Verbatim}", "\\end\\,{Verbatim}"), # Allow \end{Verbatim} in a Verbatim context + ("\\{", "\\\\,{"), # Prevent unexpected commands in Verbatim ("$", "\\$"), # Prevent unexpected math in Verbatim ] @| quoted_chars @@ -1333,7 +1334,7 @@ def doOpen(self) -> None: """Tangle out of the output files.""" self.checkPath() self.theFile = self.filePath.open("w") - self.logger.info("Tangling %r", self.filePath) + self.logger.info("Tangling '%s'", self.filePath) def doClose(self) -> None: self.theFile.close() @@ -1425,7 +1426,7 @@ a "touch" if the new file is the same as the original. def doOpen(self) -> None: fd, self.tempname = tempfile.mkstemp(dir=os.curdir) self.theFile = os.fdopen(fd, "w") - self.logger.info("Tangling %r", self.filePath) + self.logger.info("Tangling '%s'", self.filePath) @| doOpen @} @@ -1447,7 +1448,7 @@ def doClose(self) -> None: except OSError as e: same = False # Doesn't exist. (Could check for errno.ENOENT) if same: - self.logger.info("No change to %r", self.filePath) + self.logger.info("Unchanged '%s'", self.filePath) os.remove(self.tempname) else: # Windows requires the original file name be removed first. @@ -1458,7 +1459,7 @@ def doClose(self) -> None: self.checkPath() self.filePath.hardlink_to(self.tempname) # type: ignore [attr-defined] os.remove(self.tempname) - self.logger.info("Wrote %e lines to %s", self.linesWritten, self.filePath) + self.logger.info("Wrote %d lines to %s", self.linesWritten, self.filePath) @| doClose @} @@ -3442,7 +3443,7 @@ The decision is delegated to the referenced chunk. @d Web weave... @{ def weave(self, aWeaver: "Weaver") -> None: - self.logger.debug("Weaving file from %r", self.web_path) + 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): @@ -3566,19 +3567,18 @@ class WebReader: OptionDef("-noindent", nargs=0), OptionDef("argument", nargs='*'), ) - - # State of reading and parsing. - tokenizer: Tokenizer - aChunk: Chunk # 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__) @@ -3603,7 +3603,9 @@ class WebReader: return self.__class__.__name__ @ + @ + @ @| WebReader @} @@ -3769,8 +3771,10 @@ test output into the final document via the ``@@i`` command. @{ incPath = Path(next(self.tokenizer).strip()) try: - self.logger.info("Including %r", incPath) 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 @@ -3882,10 +3886,15 @@ 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 ('breakpoint', 'compile', 'eval', 'exec', 'execfile', 'globals', 'help', 'input', 'memoryview', 'open', 'print', 'super', '__import__') + if name not in dangerous )) globals = dict( __builtins__=safe, @@ -3897,7 +3906,8 @@ try: theWebReader=self, theFile=self.theWeb.web_path, thisApplication=sys.argv[0], - __version__=__version__, + __version__=__version__, # Legacy compatibility. Deprecated. + version=__version__, ) # Evaluate result = str(eval(expression, globals)) @@ -3977,8 +3987,9 @@ is that it's always loading a single top-level web. 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. + # 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 @@ -4978,11 +4989,12 @@ def parseArgs(self, argv: list[str]) -> argparse.Namespace: 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("-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_directory", action="store", type=Path) + 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) diff --git a/pyproject.toml b/pyproject.toml index 7ee3be3..8c5091e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,11 +13,12 @@ deps = pytest == 7.1.2 mypy == 0.910 setenv = - PYWEB_BOOTSTRAP = /Users/slott/Documents/Projects/PyWebTool-3/pyweb/pyweb.py + PYLPWEB_BOOTSTRAP = bootstrap/pyweb.py + PYTHONPATH = {toxinidir} commands_pre = - python3 {env:PYWEB_BOOTSTRAP} pyweb.w + python3 {env:PYLPWEB_BOOTSTRAP} pyweb.w python3 pyweb.py -o test test/pyweb_test.w commands = - python3 test/test.py - mypy --strict pyweb.py + pytest + mypy --strict --show-error-codes pyweb.py tangle.py weave.py """ diff --git a/pyweb.py b/pyweb.py index 193559f..806c1bc 100644 --- a/pyweb.py +++ b/pyweb.py @@ -9,7 +9,7 @@ __version__ = """3.1""" ### DO NOT EDIT THIS FILE! -### It was created by /Users/slott/Documents/Projects/PyWebTool-3/pyweb/pyweb.py, __version__='3.0'. +### It was created by bootstrap/pyweb.py, __version__='3.0'. ### From source pyweb.w modified Fri Jun 10 10:48:04 2022. ### In working directory '/Users/slott/Documents/Projects/py-web-tool'. @@ -710,7 +710,7 @@ def tangle(self, aTangler: "Tangler") -> None: def weave(self, aWeaver: "Weaver") -> None: - self.logger.debug("Weaving file from %r", self.web_path) + 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): @@ -823,19 +823,18 @@ class WebReader: OptionDef("-noindent", nargs=0), OptionDef("argument", nargs='*'), ) - - # State of reading and parsing. - tokenizer: Tokenizer - aChunk: Chunk # 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__) @@ -886,12 +885,14 @@ 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 filename once using the first file. + # 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 @@ -926,6 +927,7 @@ def parse_source(self) -> None: pass + def handleCommand(self, token: str) -> bool: self.logger.debug("Reading %r", token) @@ -972,8 +974,10 @@ def handleCommand(self, token: str) -> bool: incPath = Path(next(self.tokenizer).strip()) try: - self.logger.info("Including %r", incPath) 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 @@ -1028,10 +1032,15 @@ def handleCommand(self, token: str) -> bool: 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 ('breakpoint', 'compile', 'eval', 'exec', 'execfile', 'globals', 'help', 'input', 'memoryview', 'open', 'print', 'super', '__import__') + if name not in dangerous )) globals = dict( __builtins__=safe, @@ -1043,7 +1052,8 @@ def handleCommand(self, token: str) -> bool: theWebReader=self, theFile=self.theWeb.web_path, thisApplication=sys.argv[0], - __version__=__version__, + __version__=__version__, # Legacy compatibility. Deprecated. + version=__version__, ) # Evaluate result = str(eval(expression, globals)) @@ -1089,8 +1099,8 @@ def expect(self, tokens: Iterable[str]) -> str | None: class Emitter: """Emit an output file; handling indentation context.""" code_indent = 0 # Used by a Tangler - filePath : Path # File within the base directory - output : Path # Base directory + filePath : Path # Path within the base directory (on the name is used) + output : Path # Base directory to write theFile: TextIO def __init__(self) -> None: @@ -1115,6 +1125,7 @@ def open(self, aPath: Path) -> "Emitter": 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 @@ -1253,7 +1264,7 @@ def __init__(self) -> None: def doOpen(self) -> None: """Create the final woven document.""" self.filePath = self.filePath.with_suffix(self.extension) - self.logger.info("Weaving %r", self.filePath) + self.logger.info("Weaving '%s'", self.filePath) self.theFile = self.filePath.open("w") self.readdIndent(self.code_indent) @@ -1357,7 +1368,7 @@ def fileEnd(self, aChunk: Chunk) -> None: refto_name_template = string.Template(r"|srarr|\ ${fullName} (`${seq}`_)") - refto_seq_template = string.Template("|srarr|\ (`${seq}`_)") + refto_seq_template = string.Template(r"|srarr|\ (`${seq}`_)") refto_seq_separator = ", " def referenceTo(self, aName: str | None, seq: int) -> str: @@ -1465,8 +1476,8 @@ class LaTeX(Weaver): quoted_chars: list[tuple[str, str]] = [ - ("\\end{Verbatim}", "\\end\,{Verbatim}"), # Allow \end{Verbatim} in a Verbatim context - ("\\{", "\\\,{"), # Prevent unexpected commands in Verbatim + ("\\end{Verbatim}", "\\end\\,{Verbatim}"), # Allow \end{Verbatim} in a Verbatim context + ("\\{", "\\\\,{"), # Prevent unexpected commands in Verbatim ("$", "\\$"), # Prevent unexpected math in Verbatim ] @@ -1578,7 +1589,7 @@ def doOpen(self) -> None: """Tangle out of the output files.""" self.checkPath() self.theFile = self.filePath.open("w") - self.logger.info("Tangling %r", self.filePath) + self.logger.info("Tangling '%s'", self.filePath) def doClose(self) -> None: self.theFile.close() @@ -1614,7 +1625,7 @@ def __init__(self, *args: Any) -> None: def doOpen(self) -> None: fd, self.tempname = tempfile.mkstemp(dir=os.curdir) self.theFile = os.fdopen(fd, "w") - self.logger.info("Tangling %r", self.filePath) + self.logger.info("Tangling '%s'", self.filePath) @@ -1626,7 +1637,7 @@ def doClose(self) -> None: except OSError as e: same = False # Doesn't exist. (Could check for errno.ENOENT) if same: - self.logger.info("No change to %r", self.filePath) + self.logger.info("Unchanged '%s'", self.filePath) os.remove(self.tempname) else: # Windows requires the original file name be removed first. @@ -1637,7 +1648,7 @@ def doClose(self) -> None: self.checkPath() self.filePath.hardlink_to(self.tempname) # type: ignore [attr-defined] os.remove(self.tempname) - self.logger.info("Wrote %e lines to %s", self.linesWritten, self.filePath) + self.logger.info("Wrote %d lines to %s", self.linesWritten, self.filePath) @@ -1894,7 +1905,8 @@ def parseArgs(self, argv: list[str]) -> argparse.Namespace: 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_directory", action="store", type=Path) + 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) diff --git a/pyweb.rst b/pyweb.rst index 4bf4aff..61bca46 100644 --- a/pyweb.rst +++ b/pyweb.rst @@ -12,7 +12,7 @@ Yet Another Literate Programming Tool .. contents:: -.. pyweb/intro.w +.. py-web-tool/intro.w Introduction ============ @@ -891,7 +891,7 @@ for two large development efforts, I finally understood the feature set I really Jason Fruit and others contributed to the previous version. -.. pyweb/overview.w +.. py-web-tool/overview.w Architecture and Design Overview ================================ @@ -1032,7 +1032,7 @@ The idea is that the Weaver Action should be visible to tools like `PyInvoke None: @@ -1402,9 +1403,12 @@ characters to the file. def open(self, aPath: Path) -> "Emitter": """Open a file.""" - self.filePath = aPath + 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(aPath) + self.doOpen() return self |srarr|\ Emitter doOpen, to be overridden by subclasses (`6`_) @@ -1449,7 +1453,7 @@ perform the unique operation for the subclass. :class: code - def doOpen(self, aFile: Path) -> None: + def doOpen(self) -> None: self.logger.debug("Creating %r", self.filePath) @@ -1819,9 +1823,10 @@ we're not always starting a fresh line with ``weaveReferenceTo()``. :class: code - def doOpen(self, basename: Path) -> None: - self.filePath = basename.with\_suffix(self.extension) - self.logger.info("Weaving %r", self.filePath) + 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) @@ -2059,7 +2064,7 @@ a simple ``" "`` because it looks better. refto\_name\_template = string.Template(r"\|srarr\|\\ ${fullName} (\`${seq}\`\_)") - refto\_seq\_template = string.Template("\|srarr\|\\ (\`${seq}\`\_)") + refto\_seq\_template = string.Template(r"\|srarr\|\\ (\`${seq}\`\_)") refto\_seq\_separator = ", " def referenceTo(self, aName: str \| None, seq: int) -> str: @@ -2405,8 +2410,8 @@ block. Our one compromise is a thin space if the phrase quoted\_chars: list[tuple[str, str]] = [ - ("\\\\end{Verbatim}", "\\\\end\\,{Verbatim}"), # Allow \\end{Verbatim} in a Verbatim context - ("\\\\{", "\\\\\\,{"), # Prevent unexpected commands in Verbatim + ("\\\\end{Verbatim}", "\\\\end\\\\,{Verbatim}"), # Allow \\end{Verbatim} in a Verbatim context + ("\\\\{", "\\\\\\\\,{"), # Prevent unexpected commands in Verbatim ("$", "\\\\$"), # Prevent unexpected math in Verbatim ] @@ -2850,11 +2855,12 @@ actual file created by open. def checkPath(self) -> None: self.filePath.parent.mkdir(parents=True, exist\_ok=True) - def doOpen(self, aFile: Path) -> None: - self.filePath = aFile + def doOpen(self) -> None: + """Tangle out of the output files.""" self.checkPath() self.theFile = self.filePath.open("w") - self.logger.info("Tangling %r", aFile) + 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) @@ -2997,10 +3003,10 @@ a "touch" if the new file is the same as the original. :class: code - def doOpen(self, aFile: Path) -> None: + def doOpen(self) -> None: fd, self.tempname = tempfile.mkstemp(dir=os.curdir) self.theFile = os.fdopen(fd, "w") - self.logger.info("Tangling %r", aFile) + self.logger.info("Tangling '%s'", self.filePath) .. @@ -3033,7 +3039,7 @@ and time) if nothing has changed. except OSError as e: same = False # Doesn't exist. (Could check for errno.ENOENT) if same: - self.logger.info("No change to %r", self.filePath) + self.logger.info("Unchanged '%s'", self.filePath) os.remove(self.tempname) else: # Windows requires the original file name be removed first. @@ -3044,7 +3050,7 @@ and time) if nothing has changed. self.checkPath() self.filePath.hardlink\_to(self.tempname) # type: ignore [attr-defined] os.remove(self.tempname) - self.logger.info("Wrote %e lines to %s", self.linesWritten, self.filePath) + self.logger.info("Wrote %d lines to %s", self.linesWritten, self.filePath) .. @@ -5732,7 +5738,7 @@ The decision is delegated to the referenced chunk. def weave(self, aWeaver: "Weaver") -> None: - self.logger.debug("Weaving file from %r", self.web\_path) + 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): @@ -5867,19 +5873,18 @@ The class has the following attributes: OptionDef("-noindent", nargs=0), OptionDef("argument", nargs='\*'), ) - - # State of reading and parsing. - tokenizer: Tokenizer - aChunk: Chunk # 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\_\_) @@ -5904,7 +5909,9 @@ The class has the following attributes: return self.\_\_class\_\_.\_\_name\_\_ |srarr|\ WebReader location in the input stream (`128`_) + |srarr|\ WebReader load the web (`130`_) + |srarr|\ WebReader handle a command string (`117`_), |srarr|\ (`127`_) .. @@ -5988,22 +5995,6 @@ A subclass can override ``handleCommand()`` to |loz| *WebReader handle a command string (117)*. Used by: WebReader class... (`116`_) -The following sequence of ``if``-``elif`` statements identifies -the structural commands that partition the input into separate ``Chunks``. - -:: - - @d OLD major commands... - @{ - if token[:2] == self.cmdo: - @ - elif token[:2] == self.cmdd: - @ - elif token[:2] == self.cmdi: - @ - elif token[:2] in (self.cmdrcurl,self.cmdrbrak): - @ - @} An output chunk has the form ``@o`` *name* ``@{`` *content* ``@}``. We use the first two tokens to name the ``OutputChunk``. We simply expect @@ -6130,8 +6121,10 @@ test output into the final document via the ``@i`` command. incPath = Path(next(self.tokenizer).strip()) try: - self.logger.info("Including %r", incPath) 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 @@ -6188,27 +6181,6 @@ For the base ``Chunk`` class, this would be false, but for all other subclasses The following sequence of ``elif`` statements identifies the minor commands that add ``Command`` instances to the current open ``Chunk``. -:: - - @d OLD minor commands... - @{ - elif token[:2] == self.cmdpipe: - @ - 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: - @ - elif token[:2] == self.cmdlexpr: - @ - elif token[:2] == self.cmdcmd: - @ - @} - - User identifiers occur after a ``@|`` in a ``NamedChunk``. Note that no check is made to assure that the previous ``Chunk`` was indeed a named @@ -6319,10 +6291,15 @@ An ``os.getcwd()`` could be changed to ``os.path.realpath('.')``. 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 ('breakpoint', 'compile', 'eval', 'exec', 'execfile', 'globals', 'help', 'input', 'memoryview', 'open', 'print', 'super', '\_\_import\_\_') + if name not in dangerous )) globals = dict( \_\_builtins\_\_=safe, @@ -6469,8 +6446,9 @@ is that it's always loading a single top-level web. 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. + # 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 @@ -6838,12 +6816,12 @@ This two pass action might be embedded in the following type of Python program. .. parsed-literal:: - import pyweb, os, runpy, sys + import pyweb, os, runpy, sys, pathlib, contextlib + log = pathlib.Path("source.log") pyweb.tangle("source.w") - with open("source.log", "w") as target: - sys.stdout = target - runpy.run_path('source.py') - sys.stdout = sys.__stdout__ + with log.open("w") as target: + with contextlib.redirect_stdout(target): + runpy.run_path('source.py') pyweb.weave("source.w") @@ -6853,7 +6831,6 @@ some log file, ``source.log``. The third step runs **py-web-tool** excluding t 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 **py-web-tool** application. This class hierarchy defines an extensible set of fundamental actions. This gives us the flexibility to create a simple sequence @@ -6947,6 +6924,7 @@ An ``Action`` has a number of common attributes. 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`_) @@ -7039,7 +7017,9 @@ an ``append()`` method that is used to construct the sequence of actions. 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`_) @@ -7149,6 +7129,7 @@ Otherwise, the ``web.language()`` method function is used to guess what weaver t return f"{self.name!s} [{self.web!s}, {self.options.theWeaver!s}]" |srarr|\ WeaveAction call method to pick the language (`147`_) + |srarr|\ WeaveAction summary of language choice (`148`_) @@ -7180,6 +7161,7 @@ is never defined. 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") @@ -7247,6 +7229,7 @@ The options **must** include ``theTangler``, with the ``Tangler`` instance to be super().\_\_init\_\_("Tangle") |srarr|\ TangleAction call method does tangling of the output files (`150`_) + |srarr|\ TangleAction summary method provides total lines tangled (`151`_) @@ -7272,6 +7255,7 @@ with any of ``@d`` or ``@o`` and use ``@{`` ``@}`` brackets. 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: @@ -7339,7 +7323,9 @@ The options **must** include ``webReader``, with the ``WebReader`` instance to b 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`_) @@ -7561,30 +7547,10 @@ detailed usage information. """py-web-tool Literate Programming. - 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. + 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. """ .. @@ -7611,7 +7577,7 @@ source files. \_\_version\_\_ = """3.1""" ### DO NOT EDIT THIS FILE! - ### It was created by /Users/slott/Documents/Projects/PyWebTool-3/pyweb/pyweb.py, \_\_version\_\_='3.0'. + ### It was created by bootstrap/pyweb.py, \_\_version\_\_='3.0'. ### From source pyweb.w modified Fri Jun 10 10:48:04 2022. ### In working directory '/Users/slott/Documents/Projects/py-web-tool'. @@ -7825,8 +7791,9 @@ on these simple text values to create more useful objects. 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() @@ -7871,6 +7838,7 @@ instances. 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("files", nargs='+', type=Path) config = p.parse\_args(argv, namespace=self.defaults) self.expand(config) @@ -7888,6 +7856,7 @@ instances. case \_: raise Error("Improper configuration") + # Weaver try: weaver\_class = weavers[config.weaver.lower()] except KeyError: @@ -7898,6 +7867,7 @@ instances. raise TypeError(f"{weaver\_class!r} not a subclass of Weaver") config.theWeaver = weaver\_class() + # Tangler config.theTangler = TanglerMake() if config.permit: @@ -8157,7 +8127,7 @@ 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/test.w Unit Tests =========== @@ -8184,7 +8154,7 @@ Note that the last line really does set an environment variable and run a program on a single line. -.. pyweb/additional.w +.. py-web-tool/additional.w Additional Files ================ @@ -8418,16 +8388,16 @@ In order to support a pleasant installation, the ``setup.py`` file is helpful. setup(name='py-web-tool', version='3.1', - description='pyWeb 3.1: Yet Another Literate Programming Tool', + 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'], 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', ] ) @@ -8717,27 +8687,31 @@ Note that there are tabs in this file. We bootstrap the next version from the 3. # Makefile for py-web-tool. # Requires a pyweb-3.0.py (untouched) to bootstrap the current version. - SOURCE = pyweb.w intro.w overview.w impl.w tests.w additional.w todo.w done.w \\ - test/pyweb\_test.w test/intro.w test/unit.w test/func.w test/combined.w + SOURCE\_PYLPWEB = pyweb.w intro.w overview.w impl.w tests.w additional.w todo.w done.w + TEST\_PYLPWEB = test/pyweb\_test.w test/intro.w test/unit.w test/func.w test/runner.w - .PHONY : test build + .PHONY : test doc weave build # Note the bootstrapping new version from version 3.0 as baseline. # Handy to keep this \*outside\* the project's Git repository. - PYWEB\_BOOTSTRAP=/Users/slott/Documents/Projects/PyWebTool-3/pyweb/pyweb.py + PYLPWEB\_BOOTSTRAP=bootstrap/pyweb.py - test : $(SOURCE) - python3 $(PYWEB\_BOOTSTRAP) -xw pyweb.w - cd test && python3 ../pyweb.py pyweb\_test.w + test : $(SOURCE\_PYLPWEB) $(TEST\_PYLPWEB) + python3 $(PYLPWEB\_BOOTSTRAP) -xw pyweb.w + python3 pyweb.py test/pyweb\_test.w -o test PYTHONPATH=${PWD} pytest - cd test && rst2html.py pyweb\_test.rst pyweb\_test.html - mypy --strict --show-error-codes pyweb.py + rst2html.py test/pyweb\_test.rst test/pyweb\_test.html + mypy --strict --show-error-codes pyweb.py tangle.py weave.py - build : pyweb.py pyweb.html - - pyweb.py pyweb.rst : $(SOURCE) - python3 $(PYWEB\_BOOTSTRAP) pyweb.w + weave : pyweb.py tangle.py weave.py + doc : pyweb.html + + build : pyweb.py tangle.py weave.py pyweb.html + + pyweb.py pyweb.rst : $(SOURCE\_PYLPWEB) + python3 $(PYLPWEB\_BOOTSTRAP) pyweb.w + pyweb.html : pyweb.rst rst2html.py $< $@ @@ -8771,13 +8745,14 @@ Note that there are tabs in this file. We bootstrap the next version from the 3. pytest == 7.1.2 mypy == 0.910 setenv = - PYWEB\_BOOTSTRAP = /Users/slott/Documents/Projects/PyWebTool-3/pyweb/pyweb.py + PYLPWEB\_BOOTSTRAP = bootstrap/pyweb.py + PYTHONPATH = {toxinidir} commands\_pre = - python3 {env:PYWEB\_BOOTSTRAP} pyweb.w + python3 {env:PYLPWEB\_BOOTSTRAP} pyweb.w python3 pyweb.py -o test test/pyweb\_test.w commands = - python3 test/test.py - mypy --strict pyweb.py + pytest + mypy --strict --show-error-codes pyweb.py tangle.py weave.py """ .. @@ -8788,7 +8763,7 @@ Note that there are tabs in this file. We bootstrap the next version from the 3. -.. pyweb/jedit.w +.. py-web-tool/jedit.w JEdit Configuration ==================== @@ -9032,7 +9007,7 @@ Additionally, you'll want to update the JEdit catalog. .. End -.. pyweb/todo.w +.. py-web-tool/todo.w Python 3.10 Migration ===================== @@ -9048,12 +9023,23 @@ Python 3.10 Migration #. [x] Use ``match`` statements for some of the ``elif`` blocks. -#. [ ] Introduce pytest instead of building a test runner. +#. [x] Introduce pytest instead of building a test runner from ``runner.w``. + +#. [x] Add ``-o dir`` option to write output to a directory of choice. Requires ``pathlib``. + +#. [x] Finish ``pyproject.toml``. Requires ``-o dir`` option. -#. [ ] ``pyproject.toml``. This requires ```-o dir`` option to write output to a directory of choice; which requires ``pathlib``. +#. [x] Add ``bootstrap`` directory. + +#. [ ] Test cases for ``weave.py`` and ``tangle.py`` +#. [ ] Rename the module from ``pyweb`` to ``pylpweb`` to avoid namespace squatting issues. + Rename the project from ``py-web-tool`` to ``py-lpweb-tool``. + #. [ ] Replace various mock classes with ``unittest.mock.Mock`` objects and appropriate extended testing. +#. [ ] Separate ``tests``, ``examples``, and ``src`` from each other. + To Do ======= @@ -9125,7 +9111,7 @@ 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 +.. py-web-tool/done.w Change Log =========== @@ -9134,14 +9120,16 @@ Changes for 3.1 - Change to Python 3.10. -- Add type hints, f-strings, pathlib, abc.ABC +- Add type hints, f-strings, pathlib, abc.ABC. -- Replace some complex elif blocks with match statements +- Replace some complex ``elif`` blocks with ``match`` statements. -- Remove the Jedit configuration file as an output. +- Use pytest as a test runner. - Add a ``Makefile``, ``pyproject.toml``, ``requirements.txt`` and ``requirements-dev.txt``. +- Add ``-o dir`` option to write output to a directory of choice, simplifying **tox** setup. + Changes for 3.0 - Move to GitHub @@ -9644,7 +9632,7 @@ User Identifiers :OutputChunk: [`71`_] `118`_ :Path: - [`3`_] `4`_ `5`_ `6`_ `14`_ `45`_ `50`_ `53`_ `97`_ `114`_ `116`_ `120`_ `130`_ `164`_ + [`3`_] `4`_ `5`_ `53`_ `97`_ `114`_ `116`_ `120`_ `130`_ `163`_ `164`_ :ReferenceCommand: [`88`_] `123`_ :TangleAction: @@ -9800,7 +9788,7 @@ User Identifiers :referenceTo: `20`_ `21`_ [`40`_] `68`_ :references: - `17`_ `18`_ `19`_ `20`_ `26`_ `33`_ `35`_ `37`_ [`43`_] `53`_ `60`_ `61`_ `107`_ `122`_ `158`_ `163`_ `173`_ + `17`_ `18`_ `19`_ `20`_ `26`_ `33`_ `35`_ `37`_ [`43`_] `53`_ `60`_ `61`_ `107`_ `122`_ `163`_ `173`_ :resolve: `69`_ [`89`_] `90`_ `91`_ `92`_ `105`_ :searchForRE: @@ -9820,7 +9808,7 @@ User Identifiers :sys: [`124`_] `125`_ `168`_ `169`_ :tangle: - `46`_ `63`_ `69`_ `71`_ `74`_ `75`_ `77`_ `81`_ `82`_ `83`_ `84`_ `92`_ [`114`_] `150`_ `163`_ `170`_ `178`_ + `46`_ `63`_ `69`_ `71`_ `74`_ `75`_ `77`_ `81`_ `82`_ `83`_ `84`_ `92`_ [`114`_] `150`_ `163`_ `170`_ `178`_ `183`_ `184`_ :tempfile: [`48`_] `50`_ :time: @@ -9834,7 +9822,7 @@ User Identifiers :weakref: `53`_ [`98`_] `101`_ `102`_ `103`_ :weave: - `62`_ `68`_ `73`_ `76`_ `81`_ `82`_ `83`_ `85`_ `86`_ `87`_ `91`_ [`115`_] `147`_ `163`_ `172`_ `178`_ + `62`_ `68`_ `73`_ `76`_ `81`_ `82`_ `83`_ `85`_ `86`_ `87`_ `91`_ [`115`_] `147`_ `163`_ `172`_ `178`_ `183`_ `184`_ :weaveChunk: `91`_ [`115`_] :weaveReferenceTo: @@ -9844,7 +9832,7 @@ User Identifiers :webAdd: `56`_ `67`_ [`72`_] `118`_ `119`_ `120`_ `121`_ `130`_ :write: - [`5`_] `8`_ `10`_ `18`_ `19`_ `21`_ `22`_ `46`_ `82`_ `115`_ + `4`_ [`5`_] `8`_ `10`_ `18`_ `19`_ `21`_ `22`_ `46`_ `82`_ `115`_ :xrefDefLine: `22`_ [`42`_] `87`_ :xrefFoot: @@ -9861,7 +9849,7 @@ User Identifiers .. class:: small - Created by /Users/slott/Documents/Projects/PyWebTool-3/pyweb/pyweb.py at Fri Jun 10 16:54:51 2022. + Created by bootstrap/pyweb.py at Sat Jun 11 08:20:45 2022. Source pyweb.w modified Fri Jun 10 10:48:04 2022. diff --git a/test/pyweb_test.html b/test/pyweb_test.html index 8c65b6f..56ed07a 100644 --- a/test/pyweb_test.html +++ b/test/pyweb_test.html @@ -3010,11 +3010,11 @@

    User Identifiers

    (None)


    -Created by ../pyweb.py at Sat Jun 11 07:35:24 2022.
    -

    Source pyweb_test.w modified Fri Jun 10 17:07:24 2022.

    +Created by pyweb.py at Sat Jun 11 08:26:49 2022.
    +

    Source test/pyweb_test.w modified Fri Jun 10 17:07:24 2022.

    pyweb.__version__ '3.1'.

    -

    Working directory '/Users/slott/Documents/Projects/py-web-tool/test'.

    +

    Working directory '/Users/slott/Documents/Projects/py-web-tool'.

    diff --git a/test/pyweb_test.rst b/test/pyweb_test.rst index eff04a5..4187655 100644 --- a/test/pyweb_test.rst +++ b/test/pyweb_test.rst @@ -3337,10 +3337,10 @@ User Identifiers .. class:: small - Created by ../pyweb.py at Sat Jun 11 07:35:24 2022. + Created by pyweb.py at Sat Jun 11 08:26:49 2022. - Source pyweb_test.w modified Fri Jun 10 17:07:24 2022. + Source test/pyweb_test.w modified Fri Jun 10 17:07:24 2022. pyweb.__version__ '3.1'. - Working directory '/Users/slott/Documents/Projects/py-web-tool/test'. + Working directory '/Users/slott/Documents/Projects/py-web-tool'. diff --git a/todo.w b/todo.w index e0a3079..4b7160b 100644 --- a/todo.w +++ b/todo.w @@ -18,16 +18,18 @@ Python 3.10 Migration #. [x] Add ``-o dir`` option to write output to a directory of choice. Requires ``pathlib``. -#. [ ] Finish ``pyproject.toml``. Requires ``-o dir`` option. +#. [x] Finish ``pyproject.toml``. Requires ``-o dir`` option. + +#. [x] Add ``bootstrap`` directory. #. [ ] Test cases for ``weave.py`` and ``tangle.py`` -#. [ ] Rename the module from ``pyweb`` to ``pylpweb`` to avoid namespace squatting issues. - Rename the project from ``py-web-tool`` to ``py-lpweb-tool``. - #. [ ] Replace various mock classes with ``unittest.mock.Mock`` objects and appropriate extended testing. -#. [ ] Separate ``tests``, ``examples``, and ``src`` from each other. Add ``bootstrap`` directory. +#. [ ] Separate ``tests``, ``examples``, and ``src`` from each other. + +#. [ ] Rename the module from ``pyweb`` to ``pylpweb`` to avoid namespace squatting issues. + Rename the project from ``py-web-tool`` to ``py-lpweb-tool``. To Do From 267866f9e1ef501a2a5e6dbf5f08d9b01057fce8 Mon Sep 17 00:00:00 2001 From: "S.Lott" Date: Sun, 12 Jun 2022 19:19:53 -0400 Subject: [PATCH 5/8] Testing Improvements Add Test cases for ``weave.py`` and ``tangle.py`` Replace hand-build mock classes with ``unittest.mock.Mock`` objects --- .gitignore | 2 +- .nojekyll | 1 + MANIFEST.in | 2 +- Makefile | 18 +- README | 16 +- README.rst | 86 - additional.w | 165 +- done.w | 4 + impl.w | 43 +- index.html | 18 +- pyproject.toml | 2 +- pyweb.html | 1235 +++++++------ pyweb.py | 45 +- pyweb.rst | 256 +-- tangle.py | 51 +- test/combined.rst | 3081 ------------------------------- test/test_loader.py | 111 -- tests.w | 16 +- {test => tests}/docutils.conf | 0 {test => tests}/func.w | 93 +- {test => tests}/intro.w | 0 {test => tests}/page-layout.css | 0 {test => tests}/pyweb.css | 0 {test => tests}/pyweb_test.html | 1222 +++++++----- {test => tests}/pyweb_test.rst | 1308 +++++++------ {test => tests}/pyweb_test.w | 6 +- {test => tests}/runner.py | 0 {test => tests}/runner.w | 0 tests/scripts.w | 227 +++ tests/test_loader.py | 100 + tests/test_scripts.py | 161 ++ {test => tests}/test_tangler.py | 10 +- {test => tests}/test_unit.py | 456 ++--- {test => tests}/test_weaver.py | 17 +- {test => tests}/unit.w | 496 ++--- todo.w | 4 +- weave.py | 51 +- 37 files changed, 3608 insertions(+), 5695 deletions(-) delete mode 100644 README.rst delete mode 100644 test/combined.rst delete mode 100644 test/test_loader.py rename {test => tests}/docutils.conf (100%) rename {test => tests}/func.w (88%) rename {test => tests}/intro.w (100%) rename {test => tests}/page-layout.css (100%) rename {test => tests}/pyweb.css (100%) rename {test => tests}/pyweb_test.html (78%) rename {test => tests}/pyweb_test.rst (74%) rename {test => tests}/pyweb_test.w (92%) rename {test => tests}/runner.py (100%) rename {test => tests}/runner.w (100%) create mode 100644 tests/scripts.w create mode 100644 tests/test_loader.py create mode 100644 tests/test_scripts.py rename {test => tests}/test_tangler.py (97%) rename {test => tests}/test_unit.py (74%) rename {test => tests}/test_weaver.py (89%) rename {test => tests}/unit.w (78%) diff --git a/.gitignore b/.gitignore index 6a4cb7f..0e3bc18 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,7 @@ dev.sh pyweb-3.0.py __pycache__ test/__pycache__/*.pyc -test/.svn/* +tests/.svn/* py_web_tool.egg-info/* *.pyc *.aux 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..d2c663e 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,3 @@ include *.w *.css *.html *.conf *.rst -include test/*.w test/*.css test/*.html test/*.conf test/*.py +include tests/*.w tests/*.css tests/*.html tests/*.conf tests/*.py include jedit/*.xml diff --git a/Makefile b/Makefile index db1101a..7a0a1f3 100644 --- a/Makefile +++ b/Makefile @@ -2,9 +2,9 @@ # Requires a pyweb-3.0.py (untouched) to bootstrap the current version. SOURCE_PYLPWEB = pyweb.w intro.w overview.w impl.w tests.w additional.w todo.w done.w -TEST_PYLPWEB = test/pyweb_test.w test/intro.w test/unit.w test/func.w test/runner.w +TEST_PYLPWEB = tests/pyweb_test.w tests/intro.w tests/unit.w tests/func.w tests/scripts.w -.PHONY : test doc weave build +.PHONY : test doc build # Note the bootstrapping new version from version 3.0 as baseline. # Handy to keep this *outside* the project's Git repository. @@ -12,13 +12,12 @@ PYLPWEB_BOOTSTRAP=bootstrap/pyweb.py test : $(SOURCE_PYLPWEB) $(TEST_PYLPWEB) python3 $(PYLPWEB_BOOTSTRAP) -xw pyweb.w - python3 pyweb.py test/pyweb_test.w -o test + python3 pyweb.py tests/pyweb_test.w -o tests PYTHONPATH=${PWD} pytest - rst2html.py test/pyweb_test.rst test/pyweb_test.html + python3 pyweb.py tests/pyweb_test.w -xt -o tests + rst2html.py tests/pyweb_test.rst tests/pyweb_test.html mypy --strict --show-error-codes pyweb.py tangle.py weave.py -weave : pyweb.py tangle.py weave.py - doc : pyweb.html build : pyweb.py tangle.py weave.py pyweb.html @@ -26,5 +25,12 @@ build : pyweb.py tangle.py weave.py pyweb.html pyweb.py pyweb.rst : $(SOURCE_PYLPWEB) python3 $(PYLPWEB_BOOTSTRAP) pyweb.w +tests/pyweb_test.rst : pyweb.py $(TEST_PYLPWEB) + python3 pyweb.py tests/pyweb_test.w -o tests + pyweb.html : pyweb.rst rst2html.py $< $@ + +tests/pyweb_test.html : tests/pyweb_test.rst + rst2html.py $< $@ + diff --git a/README b/README index fd24c02..5cce9bd 100644 --- a/README +++ b/README @@ -74,21 +74,21 @@ This will create the various output files from the source .w file. 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. 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. +``test_loader.py``, ``test_unit.py``, and ``test_scripts.py``. +Use **pytest** to run all the tests :: - cd test - python3 -m pyweb pyweb_test.w - PYTHONPATH=.. python3 test.py - rst2html.py pyweb_test.rst pyweb_test.html - mypy --strict pyweb.py + python3 bootstrap/pyweb.py -xw pyweb.w + python3 pyweb.py tests/pyweb_test.w -o tests + PYTHONPATH=${PWD} pytest + rst2html.py tests/pyweb_test.rst tests/pyweb_test.html + mypy --strict pyweb.py weave.py tangle.py 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 index dfcf128..1d623c1 100644 --- a/additional.w +++ b/additional.w @@ -35,32 +35,37 @@ Note the general flow of this top-level script. @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()) +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 @@ -74,17 +79,20 @@ 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 logging import string +from pathlib import Path +import pyweb @} @d weave.py custom weaver definition... @@ -135,28 +143,32 @@ class MyHTML(pyweb.HTML): @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()) +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")) @} The ``setup.py``, ``requirements-dev.txt`` and ``MANIFEST.in`` files @@ -193,7 +205,7 @@ 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 tests/*.w tests/*.css tests/*.html tests/*.conf tests/*.py include jedit/*.xml @} @@ -289,22 +301,22 @@ This will create the various output files from the source .w file. 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. 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. +``test_loader.py``, ``test_unit.py``, and ``test_scripts.py``. +Use **pytest** to run all the tests :: - cd test - python3 -m pyweb pyweb_test.w - PYTHONPATH=.. python3 test.py - rst2html.py pyweb_test.rst pyweb_test.html - mypy --strict pyweb.py + python3 bootstrap/pyweb.py -xw pyweb.w + python3 pyweb.py tests/pyweb_test.w -o tests + PYTHONPATH=${PWD} pytest + rst2html.py tests/pyweb_test.rst tests/pyweb_test.html + mypy --strict pyweb.py weave.py tangle.py @} @@ -355,18 +367,23 @@ bug in ``NamedChunk.tangle()`` that prevents handling zero-length text. @o .nojekyll @{ + @} Here's an ``index.html`` to redirect GitHub to the ``pyweb.html`` file. @o index.html -@{ - - -Redirect - - -Sorry, you should have been redirected pyweb.html. +@{ + + + + + + Redirect + + +

    Sorry, you should have been redirected pyweb.html.

    + @} @@ -384,9 +401,9 @@ Note that there are tabs in this file. We bootstrap the next version from the 3. # Requires a pyweb-3.0.py (untouched) to bootstrap the current version. SOURCE_PYLPWEB = pyweb.w intro.w overview.w impl.w tests.w additional.w todo.w done.w -TEST_PYLPWEB = test/pyweb_test.w test/intro.w test/unit.w test/func.w test/runner.w +TEST_PYLPWEB = tests/pyweb_test.w tests/intro.w tests/unit.w tests/func.w tests/scripts.w -.PHONY : test doc weave build +.PHONY : test doc build # Note the bootstrapping new version from version 3.0 as baseline. # Handy to keep this *outside* the project's Git repository. @@ -394,13 +411,12 @@ PYLPWEB_BOOTSTRAP=bootstrap/pyweb.py test : $(SOURCE_PYLPWEB) $(TEST_PYLPWEB) python3 $(PYLPWEB_BOOTSTRAP) -xw pyweb.w - python3 pyweb.py test/pyweb_test.w -o test + python3 pyweb.py tests/pyweb_test.w -o tests PYTHONPATH=${PWD} pytest - rst2html.py test/pyweb_test.rst test/pyweb_test.html + python3 pyweb.py tests/pyweb_test.w -xt -o tests + rst2html.py tests/pyweb_test.rst tests/pyweb_test.html mypy --strict --show-error-codes pyweb.py tangle.py weave.py -weave : pyweb.py tangle.py weave.py - doc : pyweb.html build : pyweb.py tangle.py weave.py pyweb.html @@ -408,8 +424,15 @@ build : pyweb.py tangle.py weave.py pyweb.html pyweb.py pyweb.rst : $(SOURCE_PYLPWEB) python3 $(PYLPWEB_BOOTSTRAP) pyweb.w +tests/pyweb_test.rst : pyweb.py $(TEST_PYLPWEB) + python3 pyweb.py tests/pyweb_test.w -o tests + pyweb.html : pyweb.rst rst2html.py $< $@ + +tests/pyweb_test.html : tests/pyweb_test.rst + rst2html.py $< $@ + @} **TODO:** Finish ``tox.ini`` or ``pyproject.toml``. @@ -434,7 +457,7 @@ setenv = PYTHONPATH = {toxinidir} commands_pre = python3 {env:PYLPWEB_BOOTSTRAP} pyweb.w - python3 pyweb.py -o test test/pyweb_test.w + python3 pyweb.py -o tests tests/pyweb_test.w commands = pytest mypy --strict --show-error-codes pyweb.py tangle.py weave.py diff --git a/done.w b/done.w index 8e80fb4..7e70d27 100644 --- a/done.w +++ b/done.w @@ -19,6 +19,10 @@ Changes for 3.1 - 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 + Changes for 3.0 - Move to GitHub diff --git a/impl.w b/impl.w index de363f7..f563419 100644 --- a/impl.w +++ b/impl.w @@ -719,7 +719,8 @@ def references(self, aChunk: Chunk) -> str: if len(references) != 0: refList = [ self.ref_item_template.substitute(seq=s, fullName=n) - for n,s in references ] + for n, s in references + ] return self.ref_template.substitute(refList=self.ref_separator.join(refList)) else: return "" @@ -1457,7 +1458,7 @@ def doClose(self) -> None: except OSError as e: pass # Doesn't exist. (Could check for errno.ENOENT) self.checkPath() - self.filePath.hardlink_to(self.tempname) # type: ignore [attr-defined] + self.filePath.hardlink_to(self.tempname) os.remove(self.tempname) self.logger.info("Wrote %d lines to %s", self.linesWritten, self.filePath) @| doClose @@ -1636,15 +1637,16 @@ The ``Chunk`` constructor initializes the following instance variables: @{ class Chunk: """Anonymous piece of input file: will be output through the weaver only.""" - web : weakref.ReferenceType["Web"] - previous_command : "Command" + 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.commands: list["Command"] = [] # The list of children of this chunk self.user_id_list: list[str] = [] - self.name: str = '' + self.name: str = "" self.fullName: str = "" self.seq: int = 0 self.referencedBy: list[Chunk] = [] # Chunks which reference this chunk. Ideally just one. @@ -1655,6 +1657,12 @@ class Chunk: 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 @ @ @@ -1695,16 +1703,12 @@ be a separate ``TextCommand`` because it will wind up indented. @d Chunk append text @{ def appendText(self, text: str, lineNumber: int = 0) -> None: - """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)) + """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)) @| appendText @} @@ -2339,6 +2343,13 @@ class Command(abc.ABC): 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 diff --git a/index.html b/index.html index bf878c6..2b4c32f 100644 --- a/index.html +++ b/index.html @@ -1,8 +1,12 @@ - - - -Redirect - - -Sorry, you should have been redirected pyweb.html. + + + + + + + Redirect + + +

    Sorry, you should have been redirected pyweb.html.

    + diff --git a/pyproject.toml b/pyproject.toml index 8c5091e..d482383 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,7 +17,7 @@ setenv = PYTHONPATH = {toxinidir} commands_pre = python3 {env:PYLPWEB_BOOTSTRAP} pyweb.w - python3 pyweb.py -o test test/pyweb_test.w + python3 pyweb.py -o tests tests/pyweb_test.w commands = pytest mypy --strict --show-error-codes pyweb.py tangle.py weave.py diff --git a/pyweb.html b/pyweb.html index f9fc219..110caf4 100644 --- a/pyweb.html +++ b/pyweb.html @@ -526,7 +526,7 @@

    Yet Another Lite - +

    Introduction

    Literate programming was pioneered by Knuth as a method for @@ -1268,7 +1268,7 @@

    Acknowledgements

    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.

    - +
    @@ -1379,7 +1379,7 @@

    Application

    and configures the actions, and then closes up shop when all done.

    The idea is that the Weaver Action should be visible to tools like PyInvoke. We want Weave("someFile.w") to be a sensible task.

    - +
    @@ -1455,9 +1455,9 @@

    Base Class Definitions

    →Web class - describes the overall "web" of chunks (97) -→Tokenizer class - breaks input into tokens (135) +→Tokenizer class - breaks input into tokens (133) -→Option Parser class - locates optional values on commands (137), →(138), →(139) +→Option Parser class - locates optional values on commands (135), →(136), →(137) →WebReader class - parses the input file, building the Web structure (116) @@ -1465,11 +1465,11 @@

    Base Class Definitions

    →Reference class hierarchy - strategies for references to a chunk (93), →(94), →(95) -→Action class hierarchy - used to describe actions of the application (140) +→Action class hierarchy - used to describe actions of the application (138)
    -

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

    +

    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. @@ -1620,17 +1620,19 @@

    Emitter Superclass

    Imports (3) =

     from pathlib import Path
    +import abc
     
    -

    Imports (3). Used by: pyweb.py (157)

    +

    Imports (3). Used by: pyweb.py (155)

    Emitter superclass (4) =

     class Emitter:
         """Emit an output file; handling indentation context."""
         code_indent = 0 # Used by a Tangler
    -    filePath : Path
    +    filePath : Path  # Path within the base directory (on the name is used)
    +    output : Path  # Base directory to write
     
         theFile: TextIO
         def __init__(self) -> None:
    @@ -1672,9 +1674,12 @@ 

    Emitter Superclass

     def open(self, aPath: Path) -> "Emitter":
         """Open a file."""
    -    self.filePath = aPath
    +    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(aPath)
    +    self.doOpen()
         return self
     
     →Emitter doOpen, to be overridden by subclasses (6)
    @@ -1709,8 +1714,8 @@ 

    Emitter Superclass

    perform the unique operation for the subclass.

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

    -def doOpen(self, aFile: Path) -> None:
    -    self.logger.debug("creating %r", self.filePath)
    +def doOpen(self) -> None:
    +    self.logger.debug("Creating %r", self.filePath)
     
    @@ -1719,7 +1724,7 @@

    Emitter Superclass

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

     def doClose(self) -> None:
    -    self.logger.debug("wrote %d lines to %r", self.linesWritten, self.filePath)
    +    self.logger.debug("Wrote %d lines to %r", self.linesWritten, self.filePath)
     
    @@ -1949,7 +1954,7 @@

    Weaver subclass of Emitter
    -

    Imports (12). Used by: pyweb.py (157)

    +

    Imports (12). Used by: pyweb.py (155)

    Weaver subclass of Emitter to create documentation (13) =

    @@ -1996,9 +2001,10 @@ 

    Weaver subclass of EmitterweaveReferenceTo().

    Weaver doOpen, doClose and addIndent overrides (14) =

    -def doOpen(self, basename: Path) -> None:
    -    self.filePath = basename.with_suffix(self.extension)
    -    self.logger.info("Weaving %r", self.filePath)
    +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)
     
    @@ -2075,7 +2081,8 @@ 

    Weaver subclass of EmitterWeaver subclass of EmitterWeaver reference command output (20) =

     refto_name_template = string.Template(r"|srarr|\ ${fullName} (`${seq}`_)")
    -refto_seq_template = string.Template("|srarr|\ (`${seq}`_)")
    +refto_seq_template = string.Template(r"|srarr|\ (`${seq}`_)")
     refto_seq_separator = ", "
     
     def referenceTo(self, aName: str | None, seq: int) -> str:
    @@ -2395,8 +2402,8 @@ 

    LaTeX subclass of Weaver

    LaTeX write a line of code (30) =

     quoted_chars: list[tuple[str, str]] = [
    -    ("\\end{Verbatim}", "\\end\,{Verbatim}"),  # Allow \end{Verbatim} in a Verbatim context
    -    ("\\{", "\\\,{"), # Prevent unexpected commands in Verbatim
    +    ("\\end{Verbatim}", "\\end\\,{Verbatim}"),  # Allow \end{Verbatim} in a Verbatim context
    +    ("\\{", "\\\\,{"), # Prevent unexpected commands in Verbatim
         ("$", "\\$"), # Prevent unexpected math in Verbatim
     ]
     
    @@ -2676,11 +2683,12 @@

    Tangler subclass of Emitter< def checkPath(self) -> None: self.filePath.parent.mkdir(parents=True, exist_ok=True) -def doOpen(self, aFile: Path) -> None: - self.filePath = aFile +def doOpen(self) -> None: + """Tangle out of the output files.""" self.checkPath() self.theFile = self.filePath.open("w") - self.logger.info("Tangling %r", aFile) + 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) @@ -2746,7 +2754,7 @@

    TanglerMake subclass of Tangler<

    -

    Imports (48). Used by: pyweb.py (157)

    +

    Imports (48). Used by: pyweb.py (155)

    TanglerMake subclass which is make-sensitive (49) =

    @@ -2770,10 +2778,10 @@ 

    TanglerMake subclass of Tangler< a "touch" if the new file is the same as the original.

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

    -def doOpen(self, aFile: Path) -> None:
    +def doOpen(self) -> None:
         fd, self.tempname = tempfile.mkstemp(dir=os.curdir)
         self.theFile = os.fdopen(fd, "w")
    -    self.logger.info("Tangling %r", aFile)
    +    self.logger.info("Tangling  '%s'", self.filePath)
     
    @@ -2793,7 +2801,7 @@

    TanglerMake subclass of Tangler< except OSError as e: same = False # Doesn't exist. (Could check for errno.ENOENT) if same: - self.logger.info("No change to %r", self.filePath) + self.logger.info("Unchanged '%s'", self.filePath) os.remove(self.tempname) else: # Windows requires the original file name be removed first. @@ -2802,9 +2810,9 @@

    TanglerMake subclass of Tangler< except OSError as e: pass # Doesn't exist. (Could check for errno.ENOENT) self.checkPath() - self.filePath.hardlink_to(self.tempname) # type: ignore [attr-defined] + self.filePath.hardlink_to(self.tempname) os.remove(self.tempname) - self.logger.info("Wrote %e lines to %s", self.linesWritten, self.filePath) + self.logger.info("Wrote %d lines to %s", self.linesWritten, self.filePath)

    @@ -2826,9 +2834,12 @@

    Chunks

    This text can be program source, a reference command, or the documentation source.

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

    -→Chunk class (53)
    -→NamedChunk class (65), →(70)
    +→Chunk base class for anonymous chunks of the file (53)
    +
    +→NamedChunk class for defined names (65), →(70)
    +
     →OutputChunk class (71)
    +
     →NamedDocumentChunk class (75)
     
    @@ -2957,19 +2968,20 @@

    Chunk Superclass

    -

    Chunk class (53) =

    +

    Chunk base class for anonymous chunks of the file (53) =

     class Chunk:
         """Anonymous piece of input file: will be output through the weaver only."""
    -    web : weakref.ReferenceType["Web"]
    -    previous_command : "Command"
    +    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.commands: list["Command"] = []  # The list of children of this chunk
             self.user_id_list: list[str] = []
    -        self.name: str = ''
    +        self.name: str = ""
             self.fullName: str = ""
             self.seq: int = 0
             self.referencedBy: list[Chunk] = []  # Chunks which reference this chunk.  Ideally just one.
    @@ -2980,6 +2992,12 @@ 

    Chunk Superclass

    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 →Chunk append a command (54) →Chunk append text (55) @@ -2996,7 +3014,7 @@

    Chunk Superclass

    -

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

    +

    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 (54) =

    @@ -3008,7 +3026,7 @@

    Chunk Superclass

    -

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

    +

    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.

    @@ -3020,20 +3038,16 @@

    Chunk Superclass

    Chunk append text (55) =

     def appendText(self, text: str, lineNumber: int = 0) -> None:
    -    """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))
    +    """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 (55). Used by: Chunk class (53)

    +

    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 @@ -3048,7 +3062,7 @@

    Chunk Superclass

    -

    Chunk add to the web (56). Used by: Chunk class (53)

    +

    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. @@ -3062,7 +3076,7 @@

    Chunk Superclass

    -

    Chunk superclass make Content definition (57). Used by: Chunk class (53)

    +

    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 @@ -3084,7 +3098,7 @@

    Chunk Superclass

    -

    Imports (58). Used by: pyweb.py (157)

    +

    Imports (58). Used by: pyweb.py (155)

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

    @@ -3114,7 +3128,7 @@ 

    Chunk Superclass

    -

    Chunk examination: starts with, matches pattern (59). Used by: Chunk class (53)

    +

    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.

    @@ -3138,7 +3152,7 @@

    Chunk Superclass

    -

    Chunk generate references from this Chunk (60). Used by: Chunk class (53)

    +

    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.

    @@ -3156,7 +3170,7 @@

    Chunk Superclass

    -

    Chunk references to this Chunk (61). Used by: Chunk class (53)

    +

    Chunk references to this Chunk (61). Used by: Chunk base class... (53)

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

      @@ -3186,7 +3200,7 @@

      Chunk Superclass

      -

      Chunk weave this Chunk into the documentation (62). Used by: Chunk class (53)

      +

      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.

      @@ -3198,7 +3212,7 @@

      Chunk Superclass

      -

      Chunk tangle this Chunk into a code file (63). Used by: Chunk class (53)

      +

      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, @@ -3214,7 +3228,7 @@

      Chunk Superclass

      -

      Chunk indent adjustments (64). Used by: Chunk class (53)

      +

      Chunk indent adjustments (64). Used by: Chunk base class... (53)

    @@ -3261,7 +3275,7 @@

    NamedChunk class

    has the sequence number associated with this chunk. This is set by the Web by the webAdd() method.
    -

    NamedChunk class (65) =

    +

    NamedChunk class for defined names (65) =

     class NamedChunk(Chunk):
         """Named piece of input file: will be output as both tangler and weaver."""
    @@ -3284,7 +3298,7 @@ 

    NamedChunk class

    -

    NamedChunk class (65). Used by: Chunk class hierarchy... (52)

    +

    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 @@ -3299,7 +3313,7 @@

    NamedChunk class

    -

    NamedChunk user identifiers set and get (66). Used by: NamedChunk class (65)

    +

    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 @@ -3313,7 +3327,7 @@

    NamedChunk class

    -

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

    +

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

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

      @@ -3354,7 +3368,7 @@

      NamedChunk class

      -

      NamedChunk weave into the documentation (68). Used by: NamedChunk class (65)

      +

      NamedChunk weave into the documentation (68). Used by: NamedChunk class... (65)

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

        @@ -3384,11 +3398,11 @@

        NamedChunk class

        -

        NamedChunk tangle into the source file (69). Used by: NamedChunk class (65)

        +

        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 (70) +=

        +

        NamedChunk class for defined names (70) +=

         class NamedChunk_Noindent(NamedChunk):
             """Named piece of input file: will be output as both tangler and weaver."""
        @@ -3400,7 +3414,7 @@ 

        NamedChunk class

        -

        NamedChunk class (70). Used by: Chunk class hierarchy... (52)

        +

        NamedChunk class for defined names (70). Used by: Chunk class hierarchy... (52)

    @@ -3444,7 +3458,7 @@

    OutputChunk class

    -

    OutputChunk add to the web (72). Used by: OutputChunk class (71)

    +

    OutputChunk add to the web (72). Used by: OutputChunk class... (71)

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

      @@ -3469,7 +3483,7 @@

      OutputChunk class

      -

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

      +

      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.

      @@ -3482,7 +3496,7 @@

      OutputChunk class

      -

      OutputChunk tangle (74). Used by: OutputChunk class (71)

      +

      OutputChunk tangle (74). Used by: OutputChunk class... (71)

    @@ -3538,7 +3552,7 @@

    NamedDocumentChunk class

    -

    NamedDocumentChunk weave (76). Used by: NamedDocumentChunk class (75)

    +

    NamedDocumentChunk weave (76). Used by: NamedDocumentChunk class... (75)

    NamedDocumentChunk tangle (77) =

    @@ -3548,7 +3562,7 @@ 

    NamedDocumentChunk class

    -

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

    +

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

    @@ -3567,12 +3581,19 @@

    Commands

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

     →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)
     
    @@ -3627,7 +3648,7 @@

    Command Superclass

    the command began, in lineNumber.

    Command superclass (79) =

    -class Command:
    +class Command(abc.ABC):
         """A Command is the lowest level of granularity in the input stream."""
         chunk : "Chunk"
         text : str
    @@ -3638,6 +3659,13 @@ 

    Command Superclass

    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)
    @@ -3662,10 +3690,14 @@

    Command Superclass

     def ref(self, aWeb: "Web") -> str | None:
         return None
    +
    +@abc.abstractmethod
     def weave(self, aWeb: "Web", aWeaver: "Weaver") -> None:
    -    pass
    +    ...
    +
    +@abc.abstractmethod
     def tangle(self, aWeb: "Web", aTangler: "Tangler") -> None:
    -    pass
    +    ...
     
    @@ -3994,12 +4026,14 @@

    Reference Superclass

    this object.

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

    -class Reference:
    +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."""
    -        return []
    +        ...
     
    @@ -4189,7 +4223,7 @@

    Web Construction

    -

    Imports (98). Used by: pyweb.py (157)

    +

    Imports (98). Used by: pyweb.py (155)

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

    @@ -4370,15 +4404,22 @@ 

    Web Chunk Name Resolution 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(f"Ambiguous abbreviation {name!r}, matches {list(sorted(best))!r}") - 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

    @@ -4465,7 +4506,7 @@

    Web Cross-Reference Support<

    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.warning("No reference to %r", nm)
    @@ -4675,7 +4716,7 @@ 

    Tangle and Weave Support

    Web weave the output document (115) =

     def weave(self, aWeaver: "Weaver") -> None:
    -    self.logger.debug("Weaving file from %r", self.web_path)
    +    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):
    @@ -4787,18 +4828,17 @@ 

    The WebReader Class

    OptionDef("argument", nargs='*'), ) - # State of reading and parsing. - tokenizer: Tokenizer - aChunk: Chunk - # 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__) @@ -4817,14 +4857,16 @@

    The WebReader Class

    self.totalFiles = 0 self.errors = 0 - →WebReader command literals (133) + →WebReader command literals (131) def __str__(self) -> str: return self.__class__.__name__ - →WebReader location in the input stream (130) - →WebReader load the web (132) - →WebReader handle a command string (117), →(129) + →WebReader location in the input stream (128) + + →WebReader load the web (130) + + →WebReader handle a command string (117), →(127)
    @@ -4857,37 +4899,41 @@

    The WebReader Class

    def handleCommand(self, token: str) -> bool: self.logger.debug("Reading %r", token) - →major commands segment the input into separate Chunks (118) - →minor commands add Commands to the current Chunk (123) - 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", token, self.location()) - self.errors += 1 - else: - return False # did not recogize the command + 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)

    -

    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 (118) =

    -
    -if token[:2] == self.cmdo:
    -    →start an OutputChunk, adding it to the web (119)
    -elif token[:2] == self.cmdd:
    -    →start a NamedChunk or NamedDocumentChunk, adding it to the web (120)
    -elif token[:2] == self.cmdi:
    -    →include another file (121)
    -elif token[:2] in (self.cmdrcurl,self.cmdrbrak):
    -    →finish a chunk, start a new Chunk adding it to the web (122)
    -
    - -
    -

    major commands segment the input into separate Chunks (118). Used by: WebReader handle a command... (117)

    -

    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 @@ -4895,7 +4941,7 @@

    The WebReader Class

    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 (119) =

    +

    start an OutputChunk, adding it to the web (118) =

     args = next(self.tokenizer)
     self.expect((self.cmdlcurl,))
    @@ -4911,7 +4957,7 @@ 

    The WebReader Class

    -

    start an OutputChunk, adding it to the web (119). Used by: major commands... (118)

    +

    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. @@ -4927,7 +4973,7 @@

    The WebReader Class

    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 (120) =

    +

    start a NamedChunk or NamedDocumentChunk, adding it to the web (119) =

     args = next(self.tokenizer)
     brack = self.expect((self.cmdlcurl,self.cmdlbrak))
    @@ -4952,7 +4998,7 @@ 

    The WebReader Class

    -

    start a NamedChunk or NamedDocumentChunk, adding it to the web (120). Used by: major commands... (118)

    +

    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 @@ -4974,12 +5020,14 @@

    The WebReader Class

    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.

    -

    include another file (121) =

    +

    include another file (120) =

     incPath = Path(next(self.tokenizer).strip())
     try:
    -    self.logger.info("Including %r", incPath)
         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
    @@ -4999,7 +5047,7 @@ 

    The WebReader Class

    -

    include another file (121). Used by: major commands... (118)

    +

    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.

    @@ -5009,38 +5057,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 (122) =

    +

    finish a chunk, start a new Chunk adding it to the web (121) =

     self.aChunk = Chunk()
     self.aChunk.webAdd(self.theWeb)
     
    -

    finish a chunk, start a new Chunk adding it to the web (122). Used by: major commands... (118)

    +

    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 (123) =

    -
    -elif token[:2] == self.cmdpipe:
    -    →assign user identifiers to the current chunk (124)
    -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 (125)
    -elif token[:2] == self.cmdlexpr:
    -    →add an expression command to the current chunk (127)
    -elif token[:2] == self.cmdcmd:
    -    →double at-sign replacement, append this character to previous TextCommand (128)
    -
    - -
    -

    minor commands add Commands to the current Chunk (123). Used by: WebReader handle a command... (117)

    -

    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 @{. @@ -5050,7 +5077,7 @@

    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 (124) =

    +

    assign user identifiers to the current chunk (122) =

     try:
         self.aChunk.setUserIDRefs(next(self.tokenizer).strip())
    @@ -5061,11 +5088,11 @@ 

    The WebReader Class

    -

    assign user identifiers to the current chunk (124). Used by: minor commands... (123)

    +

    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 (125) =

    +

    add a reference command to the current chunk (123) =

     # get the name, introduce into the named Chunk dictionary
     expand = next(self.tokenizer).strip()
    @@ -5077,7 +5104,7 @@ 

    The WebReader Class

    -

    add a reference command to the current chunk (125). Used by: minor commands... (123)

    +

    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 @@ -5095,7 +5122,7 @@

    The WebReader Class

    We use the Immediate Execution semantics.

    Note that we've removed the blanket os. We provide os.path library. An os.getcwd() could be changed to os.path.realpath('.').

    -

    Imports (126) +=

    +

    Imports (124) +=

     import builtins
     import sys
    @@ -5103,19 +5130,24 @@ 

    The WebReader Class

    -

    Imports (126). Used by: pyweb.py (157)

    +

    Imports (124). Used by: pyweb.py (155)

    -

    add an expression command to the current chunk (127) =

    +

    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
    +    # **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 ('breakpoint', 'compile', 'eval', 'exec', 'execfile', 'globals', 'help', 'input', 'memoryview', 'open', 'print', 'super', '__import__')
    +        if name not in dangerous
         ))
         globals = dict(
             __builtins__=safe,
    @@ -5127,7 +5159,8 @@ 

    The WebReader Class

    theWebReader=self, theFile=self.theWeb.web_path, thisApplication=sys.argv[0], - __version__=__version__, + __version__=__version__, # Legacy compatibility. Deprecated. + version=__version__, ) # Evaluate result = str(eval(expression, globals)) @@ -5139,7 +5172,7 @@

    The WebReader Class

    -

    add an expression command to the current chunk (127). Used by: minor commands... (123)

    +

    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 @@ -5149,19 +5182,19 @@

    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 (128) =

    +

    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 (128). Used by: minor commands... (123)

    +

    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 (129) +=

    +

    WebReader handle a command string (127) +=

     def expect(self, tokens: Iterable[str]) -> str | None:
         try:
    @@ -5180,19 +5213,19 @@ 

    The WebReader Class

    -

    WebReader handle a command string (129). Used by: WebReader class... (116)

    +

    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 (130) =

    +

    WebReader location in the input stream (128) =

     def location(self) -> tuple[str, int]:
         return (str(self.filePath), self.tokenizer.lineNumber+1)
     
    -

    WebReader location in the input stream (130). Used by: WebReader class... (116)

    +

    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 @@ -5202,21 +5235,22 @@

    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.

    -

    Imports (131) +=

    +

    Imports (129) +=

     from typing import TextIO
     
    -

    Imports (131). Used by: pyweb.py (157)

    +

    Imports (129). Used by: pyweb.py (155)

    -

    WebReader load the web (132) =

    +

    WebReader load the web (130) =

     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.
    +    # 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
    @@ -5241,22 +5275,25 @@ 

    The WebReader Class

    if self.handleCommand(token): continue else: - self.logger.warning('Unknown @-command in input: %r', token) + 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 (132). Used by: WebReader class... (116)

    +

    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 (133) =

    +

    WebReader command literals (131) =

     # Structural ("major") commands
     self.cmdo = self.command+'o'
    @@ -5282,7 +5319,7 @@ 

    The WebReader Class

    -

    WebReader command literals (133). Used by: WebReader class... (116)

    +

    WebReader command literals (131). Used by: WebReader class... (116)

    @@ -5317,16 +5354,16 @@

    The Tokenizer Class

    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 (134) +=

    +

    Imports (132) +=

     import re
     from collections.abc import Iterator, Iterable
     
    -

    Imports (134). Used by: pyweb.py (157)

    +

    Imports (132). Used by: pyweb.py (155)

    -

    Tokenizer class - breaks input into tokens (135) =

    +

    Tokenizer class - breaks input into tokens (133) =

     class Tokenizer(Iterator[str]):
         def __init__(self, stream: TextIO, command_char: str='@') -> None:
    @@ -5334,16 +5371,18 @@ 

    The Tokenizer Class

    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
    -

    Tokenizer class - breaks input into tokens (135). Used by: Base Class Definitions (1)

    +

    Tokenizer class - breaks input into tokens (133). Used by: Base Class Definitions (1)

    @@ -5365,13 +5404,13 @@

    The Option Parser Class

    To handle this, we have a separate lexical scanner and parser for these two commands.

    -

    Imports (136) +=

    +

    Imports (134) +=

     import shlex
     
    -

    Imports (136). Used by: pyweb.py (157)

    +

    Imports (134). Used by: pyweb.py (155)

    Here's how we can define an option.

    @@ -5384,15 +5423,15 @@ 

    The Option Parser Class

    )

    The idea is to parallel argparse.add_argument() syntax.

    -

    Option Parser class - locates optional values on commands (137) =

    +

    Option Parser class - locates optional values on commands (135) =

     class ParseError(Exception): pass
     
    -

    Option Parser class - locates optional values on commands (137). Used by: Base Class Definitions (1)

    +

    Option Parser class - locates optional values on commands (135). Used by: Base Class Definitions (1)

    -

    Option Parser class - locates optional values on commands (138) +=

    +

    Option Parser class - locates optional values on commands (136) +=

     class OptionDef:
         def __init__(self, name: str, **kw: Any) -> None:
    @@ -5401,12 +5440,12 @@ 

    The Option Parser Class

    -

    Option Parser class - locates optional values on commands (138). 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 (139) +=

    +

    Option Parser class - locates optional values on commands (137) +=

     class OptionParser:
         def __init__(self, *arg_defs: Any) -> None:
    @@ -5433,7 +5472,7 @@ 

    The Option Parser Class

    try: 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: @@ -5460,7 +5499,7 @@

    The Option Parser Class

    -

    Option Parser class - locates optional values on commands (139). 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.

    @@ -5470,7 +5509,7 @@

    The Option Parser Class

     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
             trailers.pop(0)
    @@ -5490,12 +5529,12 @@ 

    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
    +import pyweb, os, runpy, sys, pathlib, contextlib
    +log = pathlib.Path("source.log")
     pyweb.tangle("source.w")
    -with open("source.log", "w") as target:
    -    sys.stdout = target
    -    runpy.run_path('source.py')
    -    sys.stdout = sys.__stdout__
    +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 @@ -5511,17 +5550,17 @@

    Action Class Hierarchy

    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 actions of the application (140) =

    +

    Action class hierarchy - used to describe actions of the application (138) =

    -→Action superclass has common features of all actions (141)
    -→ActionSequence subclass that holds a sequence of other actions (144)
    -→WeaveAction subclass initiates the weave action (148)
    -→TangleAction subclass initiates the tangle action (151)
    -→LoadAction subclass loads the document web (154)
    +→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 actions of the application (140). 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

    @@ -5561,7 +5600,7 @@

    Action Class

    !start:
    The time at which the action started.
    -

    Action superclass has common features of all actions (141) =

    +

    Action superclass has common features of all actions (139) =

     class Action:
         """An action performed by pyWeb."""
    @@ -5575,17 +5614,18 @@ 

    Action Class

    def __str__(self) -> str: return f"{self.name!s} [{self.web!s}]" - →Action call method actually does the real work (142) - →Action final summary of what was done (143) + →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 (141). Used by: Action class hierarchy... (140)

    +

    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 (142) =

    +

    Action call method actually does the real work (140) =

     def __call__(self) -> None:
         self.logger.info("Starting %s", self.name)
    @@ -5593,11 +5633,11 @@ 

    Action Class

    -

    Action call method actually does the real work (142). Used by: Action superclass... (141)

    +

    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 (143) =

    +

    Action final summary of what was done (141) =

     def duration(self) -> float:
         """Return duration of the action."""
    @@ -5608,7 +5648,7 @@ 

    Action Class

    -

    Action final summary of what was done (143). Used by: Action superclass... (141)

    +

    Action final summary of what was done (141). Used by: Action superclass... (139)

    @@ -5622,7 +5662,7 @@

    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 (144) =

    +

    ActionSequence subclass that holds a sequence of other actions (142) =

     class ActionSequence(Action):
         """An action composed of a sequence of other actions."""
    @@ -5634,19 +5674,21 @@ 

    ActionSequence Class

    def __str__(self) -> str: return "; ".join([str(x) for x in self.opSequence]) - →ActionSequence call method delegates the sequence of ations (145) - →ActionSequence append adds a new action to the sequence (146) - →ActionSequence summary summarizes each step (147) + →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 (144). Used by: Action class hierarchy... (140)

    +

    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 (145) =

    +

    ActionSequence call method delegates the sequence of ations (143) =

     def __call__(self) -> None:
         super().__call__()
    @@ -5657,29 +5699,29 @@ 

    ActionSequence Class

    -

    ActionSequence call method delegates the sequence of ations (145). Used by: ActionSequence subclass... (144)

    +

    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 (146) =

    +

    ActionSequence append adds a new action to the sequence (144) =

     def append(self, anAction: Action) -> None:
         self.opSequence.append(anAction)
     
    -

    ActionSequence append adds a new action to the sequence (146). Used by: ActionSequence subclass... (144)

    +

    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 (147) =

    +

    ActionSequence summary summarizes each step (145) =

     def summary(self) -> str:
         return ", ".join([o.summary() for o in self.opSequence])
     
    -

    ActionSequence summary summarizes each step (147). Used by: ActionSequence subclass... (144)

    +

    ActionSequence summary summarizes each step (145). Used by: ActionSequence subclass... (142)

    @@ -5692,7 +5734,7 @@

    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 (148) =

    +

    WeaveAction subclass initiates the weave action (146) =

     class WeaveAction(Action):
         """Weave the final document."""
    @@ -5702,19 +5744,20 @@ 

    WeaveAction Class

    def __str__(self) -> str: return f"{self.name!s} [{self.web!s}, {self.options.theWeaver!s}]" - →WeaveAction call method to pick the language (149) - →WeaveAction summary of language choice (150) + →WeaveAction call method to pick the language (147) + + →WeaveAction summary of language choice (148)
    -

    WeaveAction subclass initiates the weave action (148). Used by: Action class hierarchy... (140)

    +

    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 (149) =

    +

    WeaveAction call method to pick the language (147) =

     def __call__(self) -> None:
         super().__call__()
    @@ -5723,6 +5766,7 @@ 

    WeaveAction Class

    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") @@ -5732,11 +5776,11 @@

    WeaveAction Class

    -

    WeaveAction call method to pick the language (149). Used by: WeaveAction subclass... (148)

    +

    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 (150) =

    +

    WeaveAction summary of language choice (148) =

     def summary(self) -> str:
         if self.options.theWeaver and self.options.theWeaver.linesWritten > 0:
    @@ -5747,7 +5791,7 @@ 

    WeaveAction Class

    -

    WeaveAction summary of language choice (150). Used by: WeaveAction subclass... (148)

    +

    WeaveAction summary of language choice (148). Used by: WeaveAction subclass... (146)

    @@ -5759,28 +5803,30 @@

    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 (151) =

    +

    TangleAction subclass initiates the tangle action (149) =

     class TangleAction(Action):
         """Tangle source files."""
         def __init__(self) -> None:
             super().__init__("Tangle")
     
    -    →TangleAction call method does tangling of the output files (152)
    -    →TangleAction summary method provides total lines tangled (153)
    +    →TangleAction call method does tangling of the output files (150)
    +
    +    →TangleAction summary method provides total lines tangled (151)
     
    -

    TangleAction subclass initiates the tangle action (151). Used by: Action class hierarchy... (140)

    +

    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 (152) =

    +

    TangleAction call method does tangling of the output files (150) =

     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:
    @@ -5789,11 +5835,11 @@ 

    TangleAction Class

    -

    TangleAction call method does tangling of the output files (152). Used by: TangleAction subclass... (151)

    +

    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 (153) =

    +

    TangleAction summary method provides total lines tangled (151) =

     def summary(self) -> str:
         if self.options.theTangler and self.options.theTangler.linesWritten > 0:
    @@ -5804,7 +5850,7 @@ 

    TangleAction Class

    -

    TangleAction summary method provides total lines tangled (153). Used by: TangleAction subclass... (151)

    +

    TangleAction summary method provides total lines tangled (151). Used by: TangleAction subclass... (149)

    @@ -5815,7 +5861,7 @@

    LoadAction Class

    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 (154) =

    +

    LoadAction subclass loads the document web (152) =

     class LoadAction(Action):
         """Load the source web."""
    @@ -5823,12 +5869,14 @@ 

    LoadAction Class

    super().__init__("Load") def __str__(self) -> str: return f"Load [{self.webReader!s}, {self.web!s}]" - →LoadAction call method loads the input files (155) - →LoadAction summary provides lines read (156) + + →LoadAction call method loads the input files (153) + + →LoadAction summary provides lines read (154)
    -

    LoadAction subclass loads the document web (154). Used by: Action class hierarchy... (140)

    +

    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.

    @@ -5845,7 +5893,7 @@

    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 (155) =

    +

    LoadAction call method loads the input files (153) =

     def __call__(self) -> None:
         super().__call__()
    @@ -5872,11 +5920,11 @@ 

    LoadAction Class

    -

    LoadAction call method loads the input files (155). Used by: LoadAction subclass... (154)

    +

    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 (156) =

    +

    LoadAction summary provides lines read (154) =

     def summary(self) -> str:
         return (
    @@ -5885,25 +5933,25 @@ 

    LoadAction Class

    -

    LoadAction summary provides lines read (156). Used by: LoadAction subclass... (154)

    +

    LoadAction summary provides lines read (154). Used by: LoadAction subclass... (152)

    pyWeb Module File

    The pyWeb application file is shown below:

    -

    pyweb.py (157) =

    +

    pyweb.py (155) =

    -→Overheads (159), →(160), →(161)
    -→Imports (3), →(12), →(48), →(58), →(98), →(126), →(131), →(134), →(136), →(158), →(162), →(168)
    +→Overheads (157), →(158), →(159)
    +→Imports (3), →(12), →(48), →(58), →(98), →(124), →(129), →(132), →(134), →(156), →(160), →(166)
     →Base Class Definitions (1)
    -→Application Class (163), →(164)
    -→Logging Setup (169), →(170)
    -→Interface Functions (171)
    +→Application Class (161), →(162)
    +→Logging Setup (167), →(168)
    +→Interface Functions (169)
     
    -

    pyweb.py (157).

    +

    pyweb.py (155).

    The Overheads are described below, they include things like:

      @@ -5933,7 +5981,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 (158) +=

    +

    Imports (156) +=

     import os
     import time
    @@ -5942,7 +5990,7 @@ 

    Python Library Imports

    -

    Imports (158). Used by: pyweb.py (157)

    +

    Imports (156). Used by: pyweb.py (155)

    Note that os.path, time, datetime and platform` are provided in the expression context.

    @@ -5955,69 +6003,49 @@

    Overheads

    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 (159) =

    +

    Overheads (157) =

     #!/usr/bin/env python
     
    -

    Overheads (159). Used by: pyweb.py (157)

    +

    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 (160) +=

    +

    Overheads (158) +=

     """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 (160). Used by: pyweb.py (157)

    +

    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 (161) +=

    +

    Overheads (159) +=

     __version__ = """3.1"""
     
     ### DO NOT EDIT THIS FILE!
    -### It was created by /Users/slott/Documents/Projects/PyWebTool-3/pyweb/pyweb.py, __version__='3.0'.
    +### It was created by bootstrap/pyweb.py, __version__='3.0'.
     ### From source pyweb.w modified Fri Jun 10 10:48:04 2022.
     ### In working directory '/Users/slott/Documents/Projects/py-web-tool'.
     
    -

    Overheads (161). Used by: pyweb.py (157)

    +

    Overheads (159). Used by: pyweb.py (155)

    @@ -6056,27 +6084,27 @@

    The Application Class

    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 (162) +=

    +

    Imports (160) +=

     import argparse
     
    -

    Imports (162). Used by: pyweb.py (157)

    +

    Imports (160). Used by: pyweb.py (155)

    -

    Application Class (163) =

    +

    Application Class (161) =

     class Application:
         def __init__(self) -> None:
             self.logger = logging.getLogger(self.__class__.__qualname__)
    -        →Application default options (165)
    +        →Application default options (163)
     
    -    →Application parse command line (166)
    -    →Application class process all files (167)
    +    →Application parse command line (164)
    +    →Application class process all files (165)
     
    -

    Application Class (163). Used by: pyweb.py (157)

    +

    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. @@ -6152,7 +6180,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 (164) +=

    +

    Application Class (162) +=

     # Global list of available weaver classes.
     weavers = {
    @@ -6164,11 +6192,11 @@ 

    The Application Class

    -

    Application Class (164). Used by: pyweb.py (157)

    +

    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 (165) =

    +

    Application default options (163) =

     self.defaults = argparse.Namespace(
         verbosity=logging.INFO,
    @@ -6178,8 +6206,9 @@ 

    The Application Class

    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() @@ -6193,14 +6222,14 @@

    The Application Class

    -

    Application default options (165). Used by: Application Class... (163)

    +

    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 (166) =

    +

    Application parse command line (164) =

     def parseArgs(self, argv: list[str]) -> argparse.Namespace:
         p = argparse.ArgumentParser()
    @@ -6209,10 +6238,12 @@ 

    The Application Class

    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("-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) @@ -6222,13 +6253,15 @@

    The Application Class

    """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()] except KeyError: @@ -6239,6 +6272,7 @@

    The Application Class

    raise TypeError(f"{weaver_class!r} not a subclass of Weaver") config.theWeaver = weaver_class() + # Tangler config.theTangler = TanglerMake() if config.permit: @@ -6253,7 +6287,7 @@

    The Application Class

    -

    Application parse command line (166). Used by: Application Class... (163)

    +

    Application parse command line (164). Used by: Application Class... (161)

    The process() function uses the current Application settings to process each file as follows:

    @@ -6273,7 +6307,7 @@

    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 (167) =

    +

    Application class process all files (165) =

     def process(self, config: argparse.Namespace) -> None:
         root = logging.getLogger()
    @@ -6284,9 +6318,9 @@ 

    The Application Class

    self.logger.debug("Command character %r", config.command) if config.skip: - if config.skip.lower().startswith('w'): # not weaving == tangling + if config.skip.lower().startswith('w'): # not weaving == tangling self.theAction = self.doTangle - elif config.skip.lower().startswith('t'): # not tangling == weaving + elif config.skip.lower().startswith('t'): # not tangling == weaving self.theAction = self.doWeave else: raise Exception(f"Unknown -x option {config.skip!r}") @@ -6304,7 +6338,7 @@

    The Application Class

    -

    Application class process all files (167). Used by: Application Class... (163)

    +

    Application class process all files (165). Used by: Application Class... (161)

    @@ -6312,45 +6346,47 @@

    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 (168) +=

    +

    Imports (166) +=

     import logging
     import logging.config
     
    -

    Imports (168). Used by: pyweb.py (157)

    +

    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 (169) =

    +

    Logging Setup (167) =

     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
     
    -

    Logging Setup (169). Used by: pyweb.py (157)

    +

    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 (170) +=

    +

    Logging Setup (168) +=

     log_config = {
         'version': 1,
    @@ -6382,7 +6418,7 @@ 

    Logging Setup

    -

    Logging Setup (170). Used by: pyweb.py (157)

    +

    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.

    @@ -6396,7 +6432,7 @@

    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 (171) =

    +

    Interface Functions (169) =

     def main(argv: list[str] = sys.argv[1:]) -> None:
         a = Application()
    @@ -6409,7 +6445,7 @@ 

    The Main Function

    -

    Interface Functions (171). Used by: pyweb.py (157)

    +

    Interface Functions (169). Used by: pyweb.py (155)

    This can be extended by doing something like the following.

      @@ -6428,27 +6464,26 @@

      The Main Function

      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 +

    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.

    +
    @@ -6513,30 +6554,33 @@

    wea

    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 (173) =

    +

    weave.py (171) =

    -→weave.py overheads for correct operation of a script (174)
    -→weave.py custom weaver definition to customize the Weaver being used (175)
    -→weaver.py processing: load and weave the document (176)
    +→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 (173).

    +

    weave.py (171).

    -

    weave.py overheads for correct operation of a script (174) =

    +

    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 (174). Used by: weave.py (173)

    +

    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 (175) =

    +

    weave.py custom weaver definition to customize the Weaver being used (173) =

     class MyHTML(pyweb.HTML):
         """HTML formatting templates."""
    @@ -6583,42 +6627,46 @@ 

    wea

    -

    weave.py custom weaver definition to customize the Weaver being used (175). Used by: weave.py (173)

    +

    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 (176) =

    +

    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",
    +        options = argparse.Namespace(
    +            source_path=source,
    +            output=source.parent,
                 verbosity=logging.INFO,
                 command='@',
    -            theWeaver=MyHTML(),
                 permitList=[],
                 tangler_line_numbers=False,
                 reference_style=pyweb.SimpleReference(),
    -            theTangler=pyweb.TanglerMake(),
    +            theWeaver=MyHTML(),
                 webReader=pyweb.WebReader(),
    -            )
    +        )
     
    -    w = pyweb.Web()
    +        w = pyweb.Web()
     
    -    for action in LoadAction(), WeaveAction():
    +        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"))
     
    -

    weaver.py processing: load and weave the document (176). Used by: weave.py (173)

    +

    weaver.py processing: load and weave the document (174). Used by: weave.py (171)

    The setup.py, requirements-dev.txt and MANIFEST.in files

    In order to support a pleasant installation, the setup.py file is helpful.

    -

    setup.py (177) =

    +

    setup.py (175) =

     #!/usr/bin/env python3
     """Setup for pyWeb."""
    @@ -6627,54 +6675,54 @@ 

    The author_email='s_lott@yahoo.com', + author_email='slott56@gmail.com', url='http://slott-softwarearchitect.blogspot.com/', py_modules=['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', ] )

    -

    setup.py (177).

    +

    setup.py (175).

    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 (178) =

    +

    MANIFEST.in (176) =

     include *.w *.css *.html *.conf *.rst
    -include test/*.w test/*.css test/*.html test/*.conf test/*.py
    +include tests/*.w tests/*.css tests/*.html tests/*.conf tests/*.py
     include jedit/*.xml
     
    -

    MANIFEST.in (178).

    +

    MANIFEST.in (176).

    In order to install dependencies, the following file is also used.

    -

    requirements-dev.txt (179) =

    +

    requirements-dev.txt (177) =

     docutils==0.18.1
     tox==3.25.0
     mypy==0.910
    -pytest == 7.1.2
    +pytest==7.1.2
     
    -

    requirements-dev.txt (179).

    +

    requirements-dev.txt (177).

    The README file

    Here's the README file.

    -

    README (180) =

    +

    README (178) =

     pyWeb 3.1: In Python, Yet Another Literate Programming Tool
     
    @@ -6752,26 +6800,26 @@ 

    The
    -

    README (180).

    +

    README (178).

    @@ -6780,7 +6828,7 @@

    The HTML Support 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 (181) =

    +

    docutils.conf (179) =

     # docutils.conf
     
    @@ -6791,11 +6839,11 @@ 

    The HTML Support Files

    -

    docutils.conf (181).

    +

    docutils.conf (179).

    page-layout.css This tweaks one CSS to be sure that the resulting HTML pages are easier to read.

    -

    page-layout.css (182) =

    +

    page-layout.css (180) =

     /* Page layout tweaks */
     div.document { width: 7in; }
    @@ -6817,13 +6865,13 @@ 

    The HTML Support Files

    -

    page-layout.css (182).

    +

    page-layout.css (180).

    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 (183) =

    +

    .nojekyll (181) =

    -

    System Message: ERROR/3 (pyweb.rst, line 8633)

    +

    System Message: ERROR/3 (pyweb.rst, line 8661)

    Content block expected for the "parsed-literal" directive; none found.

     ..  parsed-literal::
    @@ -6831,27 +6879,32 @@ 

    The HTML Support Files

    +
    -

    .nojekyll (183).

    +

    .nojekyll (181).

    Here's an index.html to redirect GitHub to the pyweb.html file.

    -

    index.html (184) =

    -
    -<?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>
    +

    index.html (182) =

    +
    +<!doctype html>
    +<html lang="en">
    +  <head>
    +    <meta charset="utf-8">
    +    <meta name="viewport" content="width=device-width, initial-scale=1">
    +    <meta http-equiv="refresh" content="0;url=pyweb.html" />
    +    <title>Redirect</title>
    +  </head>
    +  <body>
    +    <p>Sorry, you should have been redirected <a href="pyweb.html">pyweb.html</a>.</p>
    +  </body>
     </html>
     
    -

    index.html (184).

    +

    index.html (182).

    @@ -6859,41 +6912,50 @@

    Tox and Makefile

    It's simpler to have a Makefile to automate testing, particularly when making changes to py-web-tool.

    Note that there are tabs in this file. We bootstrap the next version from the 3.0 version.

    -

    Makefile (185) =

    +

    Makefile (183) =

     # Makefile for py-web-tool.
     # Requires a pyweb-3.0.py (untouched) to bootstrap the current version.
     
    -SOURCE = pyweb.w intro.w overview.w impl.w tests.w additional.w todo.w done.w \
    -    test/pyweb_test.w test/intro.w test/unit.w test/func.w test/combined.w
    +SOURCE_PYLPWEB = pyweb.w intro.w overview.w impl.w tests.w additional.w todo.w done.w
    +TEST_PYLPWEB = tests/pyweb_test.w tests/intro.w tests/unit.w tests/func.w tests/scripts.w
     
    -.PHONY : test build
    +.PHONY : test doc build
     
     # Note the bootstrapping new version from version 3.0 as baseline.
     # Handy to keep this *outside* the project's Git repository.
    -PYWEB_BOOTSTRAP=/Users/slott/Documents/Projects/PyWebTool-3/pyweb/pyweb.py
    +PYLPWEB_BOOTSTRAP=bootstrap/pyweb.py
    +
    +test : $(SOURCE_PYLPWEB) $(TEST_PYLPWEB)
    +    python3 $(PYLPWEB_BOOTSTRAP) -xw pyweb.w
    +    python3 pyweb.py tests/pyweb_test.w -o tests
    +    PYTHONPATH=${PWD} pytest
    +    python3 pyweb.py tests/pyweb_test.w -xt -o tests
    +    rst2html.py tests/pyweb_test.rst tests/pyweb_test.html
    +    mypy --strict --show-error-codes pyweb.py tangle.py weave.py
     
    -test : $(SOURCE)
    -    python3 $(PYWEB_BOOTSTRAP) -xw pyweb.w
    -    cd test && python3 ../pyweb.py pyweb_test.w
    -    cd test && PYTHONPATH=.. python3 test.py
    -    cd test && rst2html.py pyweb_test.rst pyweb_test.html
    -    mypy --strict --show-error-codes pyweb.py
    +doc : pyweb.html
     
    -build : pyweb.py pyweb.html
    +build : pyweb.py tangle.py weave.py pyweb.html
     
    -pyweb.py pyweb.rst : $(SOURCE)
    -    python3 $(PYWEB_BOOTSTRAP) pyweb.w
    +pyweb.py pyweb.rst : $(SOURCE_PYLPWEB)
    +    python3 $(PYLPWEB_BOOTSTRAP) pyweb.w
    +
    +tests/pyweb_test.rst : pyweb.py $(TEST_PYLPWEB)
    +    python3 pyweb.py tests/pyweb_test.w -o tests
     
     pyweb.html : pyweb.rst
         rst2html.py $< $@
    +
    +tests/pyweb_test.html : tests/pyweb_test.rst
    +    rst2html.py $< $@
     
    -

    Makefile (185).

    +

    Makefile (183).

    TODO: Finish tox.ini or pyproject.toml.

    -

    pyproject.toml (186) =

    +

    pyproject.toml (184) =

     [build-system]
     requires = ["setuptools >= 61.2.0", "wheel >= 0.37.1", "pytest == 7.1.2", "mypy == 0.910"]
    @@ -6908,19 +6970,22 @@ 

    Tox and Makefile

    deps = pytest == 7.1.2 mypy == 0.910 +setenv = + PYLPWEB_BOOTSTRAP = bootstrap/pyweb.py + PYTHONPATH = {toxinidir} commands_pre = - python3 pyweb-3.0.py pyweb.w - python3 pyweb.py -o test test/pyweb_test.w + python3 {env:PYLPWEB_BOOTSTRAP} pyweb.w + python3 pyweb.py -o tests tests/pyweb_test.w commands = - python3 test/test.py - mypy --strict pyweb.py + pytest + mypy --strict --show-error-codes pyweb.py tangle.py weave.py """
    -

    pyproject.toml (186).

    +

    pyproject.toml (184).

    - +
    @@ -6928,23 +6993,23 @@

    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 (187) =

    +

    jedit/pyweb.xml (185) =

     <?xml version="1.0"?>
     <!DOCTYPE MODE SYSTEM "xmode.dtd">
     
     <MODE>
    -    →props for JEdit mode (188)
    -    →rules for JEdit PyWeb and RST (189)
    -    →rules for JEdit PyWeb XML-Like Constructs (190)
    +    →props for JEdit mode (186)
    +    →rules for JEdit PyWeb and RST (187)
    +    →rules for JEdit PyWeb XML-Like Constructs (188)
     </MODE>
     
    -

    jedit/pyweb.xml (187).

    +

    jedit/pyweb.xml (185).

    Here are some properties to define RST constructs to JEdit

    -

    props for JEdit mode (188) =

    +

    props for JEdit mode (186) =

     <PROPS>
         <PROPERTY NAME="lineComment" VALUE=".. "/>
    @@ -6958,10 +7023,10 @@ 

    JEdit Configuration

    -

    props for JEdit mode (188). Used by: jedit/pyweb.xml (187)

    +

    props for JEdit mode (186). Used by: jedit/pyweb.xml (185)

    Here are some rules to define PyWeb and RST constructs to JEdit.

    -

    rules for JEdit PyWeb and RST (189) =

    +

    rules for JEdit PyWeb and RST (187) =

     <RULES IGNORE_CASE="FALSE" HIGHLIGHT_DIGITS="FALSE">
     
    @@ -7097,11 +7162,11 @@ 

    JEdit Configuration

    -

    rules for JEdit PyWeb and RST (189). Used by: jedit/pyweb.xml (187)

    +

    rules for JEdit PyWeb and RST (187). Used by: jedit/pyweb.xml (185)

    Here are some additional rules to define PyWeb constructs to JEdit that look like XML.

    -

    rules for JEdit PyWeb XML-Like Constructs (190) =

    +

    rules for JEdit PyWeb XML-Like Constructs (188) =

     <RULES SET="CODE" DEFAULT="KEYWORD1">
         <SPAN TYPE="MARKUP">
    @@ -7112,7 +7177,7 @@ 

    JEdit Configuration

    -

    rules for JEdit PyWeb XML-Like Constructs (190). Used by: jedit/pyweb.xml (187)

    +

    rules for JEdit PyWeb XML-Like Constructs (188). Used by: jedit/pyweb.xml (185)

    Additionally, you'll want to update the JEdit catalog.

    @@ -7126,7 +7191,7 @@ 

    JEdit Configuration

    </MODES>
    - +

    Python 3.10 Migration

    @@ -7134,11 +7199,20 @@

    Python 3.10 Migration

  • [x] Add type hints.
  • [x] Replace all .format() with f-strings.
  • [x] Replace filename strings (and os.path) with pathlib.Path.
  • -
  • [ ] Add abc to formalize Abstract Base Classes.
  • -
  • [ ] Introduce match statements for some of the elif blocks.
  • -
  • [ ] Introduce pytest instead of building a test runner.
  • -
  • [ ] pyproject.toml. This requires `-o dir option to write output to a directory of choice; which requires pathlib.
  • -
  • [ ] Replace various mock classes with unittest.mock.Mock objects and appropriate extended testing.
  • +
  • [x] Add abc to formalize Abstract Base Classes.
  • +
  • [x] Use match statements for some of the elif blocks.
  • +
  • [x] Introduce pytest instead of building a test runner from runner.w.
  • +
  • [x] Add -o dir option to write output to a directory of choice. Requires pathlib.
  • +
  • [x] Finish pyproject.toml. Requires -o dir option.
  • +
  • [x] Add bootstrap directory.
  • +
  • [x] Test cases for weave.py and tangle.py
  • +
  • [x] Replace various mock classes with unittest.mock.Mock objects and appropriate testing.
  • +
  • [ ] Separate tests, examples, and src from each other.
  • +
  • +
    [ ] Rename the module from pyweb to pylpweb to avoid namespace squatting issues.
    +
    Rename the project from py-web-tool to py-lpweb-tool.
    +
    +
  • @@ -7209,7 +7283,7 @@

    Other Thoughts

    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.

    - +
    @@ -7217,10 +7291,14 @@

    Change Log

    Changes for 3.1

    • Change to Python 3.10.
    • -
    • Add type hints, f-strings, pathlib.
    • -
    • Replace some complex elif blocks with match statements
    • -
    • Remove the Jedit configuration file as an output.
    • +
    • 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.
    • +
    • Add -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

    Changes for 3.0

    Source pyweb.w modified Fri Jun 10 10:48:04 2022.

    pyweb.__version__ '3.0'.

    diff --git a/pyweb.py b/pyweb.py index 806c1bc..47b7606 100644 --- a/pyweb.py +++ b/pyweb.py @@ -70,6 +70,13 @@ def __init__(self, fromLine: int = 0) -> None: 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 @@ -244,15 +251,16 @@ def tangle(self, aWeb: "Web", aTangler: "Tangler") -> None: class Chunk: """Anonymous piece of input file: will be output through the weaver only.""" - web : weakref.ReferenceType["Web"] - previous_command : "Command" + 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.commands: list["Command"] = [] # The list of children of this chunk self.user_id_list: list[str] = [] - self.name: str = '' + self.name: str = "" self.fullName: str = "" self.seq: int = 0 self.referencedBy: list[Chunk] = [] # Chunks which reference this chunk. Ideally just one. @@ -263,6 +271,12 @@ 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: @@ -273,16 +287,12 @@ def append(self, command: Command) -> None: def appendText(self, text: str, lineNumber: int = 0) -> None: - """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)) + """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)) @@ -1312,7 +1322,8 @@ def references(self, aChunk: Chunk) -> str: if len(references) != 0: refList = [ self.ref_item_template.substitute(seq=s, fullName=n) - for n,s in references ] + for n, s in references + ] return self.ref_template.substitute(refList=self.ref_separator.join(refList)) else: return "" @@ -1646,7 +1657,7 @@ def doClose(self) -> None: except OSError as e: pass # Doesn't exist. (Could check for errno.ENOENT) self.checkPath() - self.filePath.hardlink_to(self.tempname) # type: ignore [attr-defined] + self.filePath.hardlink_to(self.tempname) os.remove(self.tempname) self.logger.info("Wrote %d lines to %s", self.linesWritten, self.filePath) @@ -1901,7 +1912,7 @@ def parseArgs(self, argv: list[str]) -> argparse.Namespace: 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("-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") diff --git a/pyweb.rst b/pyweb.rst index 61bca46..c8e1b5e 100644 --- a/pyweb.rst +++ b/pyweb.rst @@ -1936,7 +1936,8 @@ Each code chunk includes the places where the chunk is referenced. if len(references) != 0: refList = [ self.ref\_item\_template.substitute(seq=s, fullName=n) - for n,s in references ] + for n, s in references + ] return self.ref\_template.substitute(refList=self.ref\_separator.join(refList)) else: return "" @@ -3048,7 +3049,7 @@ and time) if nothing has changed. except OSError as e: pass # Doesn't exist. (Could check for errno.ENOENT) self.checkPath() - self.filePath.hardlink\_to(self.tempname) # type: ignore [attr-defined] + self.filePath.hardlink\_to(self.tempname) os.remove(self.tempname) self.logger.info("Wrote %d lines to %s", self.linesWritten, self.filePath) @@ -3249,15 +3250,16 @@ The ``Chunk`` constructor initializes the following instance variables: class Chunk: """Anonymous piece of input file: will be output through the weaver only.""" - web : weakref.ReferenceType["Web"] - previous\_command : "Command" + 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.commands: list["Command"] = [] # The list of children of this chunk self.user\_id\_list: list[str] = [] - self.name: str = '' + self.name: str = "" self.fullName: str = "" self.seq: int = 0 self.referencedBy: list[Chunk] = [] # Chunks which reference this chunk. Ideally just one. @@ -3268,6 +3270,12 @@ The ``Chunk`` constructor initializes the following instance variables: 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`_) @@ -3330,16 +3338,12 @@ be a separate ``TextCommand`` because it will wind up indented. def appendText(self, text: str, lineNumber: int = 0) -> None: - """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)) + """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)) .. @@ -4238,6 +4242,13 @@ the command began, in ``lineNumber``. 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`_) @@ -6311,7 +6322,8 @@ An ``os.getcwd()`` could be changed to ``os.path.realpath('.')``. theWebReader=self, theFile=self.theWeb.web\_path, thisApplication=sys.argv[0], - \_\_version\_\_=\_\_version\_\_, + \_\_version\_\_=\_\_version\_\_, # Legacy compatibility. Deprecated. + version=\_\_version\_\_, ) # Evaluate result = str(eval(expression, globals)) @@ -7834,11 +7846,12 @@ instances. 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("-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) @@ -8132,26 +8145,26 @@ weaver via the command-line option ``-w myweaver``. 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. .. py-web-tool/additional.w @@ -8196,32 +8209,37 @@ Note the general flow of this top-level script. #!/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")) .. @@ -8246,7 +8264,9 @@ A customized weaver generally has three parts. :class: code |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`_) .. @@ -8264,10 +8284,11 @@ A customized weaver generally has three parts. #!/usr/bin/env python3 """Sample weave.py script.""" - import pyweb - import logging import argparse + import logging import string + from pathlib import Path + import pyweb .. @@ -8340,28 +8361,32 @@ A customized weaver generally has three parts. :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")) .. @@ -8420,7 +8445,7 @@ We use a simple inclusion to augment the default manifest rules. :class: code include \*.w \*.css \*.html \*.conf \*.rst - include test/\*.w test/\*.css test/\*.html test/\*.conf test/\*.py + include tests/\*.w tests/\*.css tests/\*.html tests/\*.conf tests/\*.py include jedit/\*.xml .. @@ -8538,22 +8563,22 @@ Here's the README file. 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. 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. + \`\`test\_loader.py\`\`, \`\`test\_unit.py\`\`, and \`\`test\_scripts.py\`\`. + Use \*\*pytest\*\* to run all the tests :: - cd test - python3 -m pyweb pyweb\_test.w - PYTHONPATH=.. python3 test.py - rst2html.py pyweb\_test.rst pyweb\_test.html - mypy --strict pyweb.py + python3 bootstrap/pyweb.py -xw pyweb.w + python3 pyweb.py tests/pyweb\_test.w -o tests + PYTHONPATH=${PWD} pytest + rst2html.py tests/pyweb\_test.rst tests/pyweb\_test.html + mypy --strict pyweb.py weave.py tangle.py @@ -8637,6 +8662,7 @@ bug in ``NamedChunk.tangle()`` that prevents handling zero-length text. :class: code + .. @@ -8653,13 +8679,17 @@ Here's an ``index.html`` to redirect GitHub to the ``pyweb.html`` file. .. parsed-literal:: :class: code - - - - Redirect - - - Sorry, you should have been redirected pyweb.html. + + + + + + + Redirect + + +

    Sorry, you should have been redirected pyweb.html.

    + .. @@ -8688,9 +8718,9 @@ Note that there are tabs in this file. We bootstrap the next version from the 3. # Requires a pyweb-3.0.py (untouched) to bootstrap the current version. SOURCE\_PYLPWEB = pyweb.w intro.w overview.w impl.w tests.w additional.w todo.w done.w - TEST\_PYLPWEB = test/pyweb\_test.w test/intro.w test/unit.w test/func.w test/runner.w + TEST\_PYLPWEB = tests/pyweb\_test.w tests/intro.w tests/unit.w tests/func.w tests/scripts.w - .PHONY : test doc weave build + .PHONY : test doc build # Note the bootstrapping new version from version 3.0 as baseline. # Handy to keep this \*outside\* the project's Git repository. @@ -8698,13 +8728,12 @@ Note that there are tabs in this file. We bootstrap the next version from the 3. test : $(SOURCE\_PYLPWEB) $(TEST\_PYLPWEB) python3 $(PYLPWEB\_BOOTSTRAP) -xw pyweb.w - python3 pyweb.py test/pyweb\_test.w -o test + python3 pyweb.py tests/pyweb\_test.w -o tests PYTHONPATH=${PWD} pytest - rst2html.py test/pyweb\_test.rst test/pyweb\_test.html + python3 pyweb.py tests/pyweb\_test.w -xt -o tests + rst2html.py tests/pyweb\_test.rst tests/pyweb\_test.html mypy --strict --show-error-codes pyweb.py tangle.py weave.py - weave : pyweb.py tangle.py weave.py - doc : pyweb.html build : pyweb.py tangle.py weave.py pyweb.html @@ -8712,8 +8741,15 @@ Note that there are tabs in this file. We bootstrap the next version from the 3. pyweb.py pyweb.rst : $(SOURCE\_PYLPWEB) python3 $(PYLPWEB\_BOOTSTRAP) pyweb.w + tests/pyweb\_test.rst : pyweb.py $(TEST\_PYLPWEB) + python3 pyweb.py tests/pyweb\_test.w -o tests + pyweb.html : pyweb.rst rst2html.py $< $@ + + tests/pyweb\_test.html : tests/pyweb\_test.rst + rst2html.py $< $@ + .. @@ -8749,7 +8785,7 @@ Note that there are tabs in this file. We bootstrap the next version from the 3. PYTHONPATH = {toxinidir} commands\_pre = python3 {env:PYLPWEB\_BOOTSTRAP} pyweb.w - python3 pyweb.py -o test test/pyweb\_test.w + python3 pyweb.py -o tests tests/pyweb\_test.w commands = pytest mypy --strict --show-error-codes pyweb.py tangle.py weave.py @@ -9031,15 +9067,15 @@ Python 3.10 Migration #. [x] Add ``bootstrap`` directory. -#. [ ] Test cases for ``weave.py`` and ``tangle.py`` +#. [x] Test cases for ``weave.py`` and ``tangle.py`` -#. [ ] Rename the module from ``pyweb`` to ``pylpweb`` to avoid namespace squatting issues. - Rename the project from ``py-web-tool`` to ``py-lpweb-tool``. - -#. [ ] Replace various mock classes with ``unittest.mock.Mock`` objects and appropriate extended testing. +#. [x] Replace various mock classes with ``unittest.mock.Mock`` objects and appropriate testing. #. [ ] Separate ``tests``, ``examples``, and ``src`` from each other. +#. [ ] Rename the module from ``pyweb`` to ``pylpweb`` to avoid namespace squatting issues. + Rename the project from ``py-web-tool`` to ``py-lpweb-tool``. + To Do ======= @@ -9130,6 +9166,12 @@ Changes for 3.1 - Add ``-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 + Changes for 3.0 - Move to GitHub @@ -9610,7 +9652,7 @@ User Identifiers :CodeCommand: `65`_ [`83`_] :Command: - `53`_ `54`_ `57`_ `65`_ `75`_ [`79`_] `82`_ `84`_ `88`_ `165`_ + `53`_ `54`_ `55`_ `57`_ `65`_ `75`_ [`79`_] `82`_ `84`_ `88`_ `165`_ :Emitter: [`4`_] `5`_ `13`_ `44`_ :Error: @@ -9632,7 +9674,7 @@ User Identifiers :OutputChunk: [`71`_] `118`_ :Path: - [`3`_] `4`_ `5`_ `53`_ `97`_ `114`_ `116`_ `120`_ `130`_ `163`_ `164`_ + [`3`_] `4`_ `5`_ `53`_ `97`_ `114`_ `116`_ `120`_ `130`_ `163`_ `164`_ `170`_ `172`_ `174`_ :ReferenceCommand: [`88`_] `123`_ :TangleAction: @@ -9640,7 +9682,7 @@ User Identifiers :Tangler: `4`_ [`44`_] `49`_ `63`_ `64`_ `69`_ `70`_ `74`_ `77`_ `81`_ `82`_ `83`_ `84`_ `92`_ `114`_ `164`_ :TanglerMake: - [`49`_] `164`_ `168`_ `170`_ `174`_ + [`49`_] `164`_ `168`_ `170`_ :TextCommand: `55`_ `57`_ `69`_ `75`_ [`82`_] `83`_ :Tokenizer: @@ -9662,7 +9704,7 @@ User Identifiers :__exit__: [`5`_] `167`_ :__version__: - `125`_ [`159`_] + `125`_ [`159`_] `164`_ :_gatherUserId: [`110`_] :_updateUserId: @@ -9750,7 +9792,7 @@ User Identifiers :logging.config: [`166`_] `167`_ :main: - [`169`_] + [`169`_] `170`_ `174`_ :makeContent: `55`_ [`57`_] `65`_ `75`_ :multi_reference: @@ -9802,7 +9844,7 @@ User Identifiers :startswith: `59`_ [`80`_] `82`_ `104`_ `113`_ `130`_ `137`_ `165`_ :string: - [`12`_] `17`_ `18`_ `19`_ `20`_ `21`_ `22`_ `25`_ `26`_ `29`_ `31`_ `34`_ `35`_ `36`_ `37`_ `38`_ `40`_ `41`_ `42`_ `43`_ `172`_ `173`_ + [`12`_] `17`_ `18`_ `19`_ `20`_ `21`_ `22`_ `25`_ `26`_ `29`_ `31`_ `34`_ `35`_ `36`_ `37`_ `38`_ `40`_ `41`_ `42`_ `43`_ `55`_ `172`_ `173`_ :summary: `141`_ `145`_ `148`_ `151`_ [`154`_] `165`_ `170`_ `174`_ :sys: @@ -9849,7 +9891,7 @@ User Identifiers .. class:: small - Created by bootstrap/pyweb.py at Sat Jun 11 08:20:45 2022. + Created by bootstrap/pyweb.py at Sun Jun 12 19:19:13 2022. Source pyweb.w modified Fri Jun 10 10:48:04 2022. diff --git a/tangle.py b/tangle.py index 3841c54..da25538 100644 --- a/tangle.py +++ b/tangle.py @@ -1,28 +1,33 @@ #!/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")) diff --git a/test/combined.rst b/test/combined.rst deleted file mode 100644 index 5037227..0000000 --- a/test/combined.rst +++ /dev/null @@ -1,3081 +0,0 @@ -############################################ -pyWeb Literate Programming 2.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:pyweb slott$ cd test - MacBookPro-SLott:test slott$ python ../pyweb.py 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. - 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$ - - -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( 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 ): - -- 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. - - -.. _`1`: -.. rubric:: test_unit.py (1) = -.. parsed-literal:: - :class: code - - |srarr|\ Unit Test overheads: imports, etc. (`45`_) - |srarr|\ Unit Test of Emitter class hierarchy (`2`_) - |srarr|\ Unit Test of Chunk class hierarchy (`11`_) - |srarr|\ Unit Test of Command class hierarchy (`22`_) - |srarr|\ Unit Test of Reference class hierarchy (`31`_) - |srarr|\ Unit Test of Web class (`32`_) - |srarr|\ Unit Test of WebReader class (`38`_) - |srarr|\ Unit Test of Action class hierarchy (`39`_) - |srarr|\ Unit Test of Application class (`44`_) - |srarr|\ Unit Test main (`46`_) - -.. - - .. 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, fileName ): - self.theFile= io.StringIO() - def doClose( self ): - self.theFile.flush() - - 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.theFile.getvalue() ) - def test\_emitter\_should\_codeBlock( self ): - self.emitter.open( "test.tmp" ) - self.emitter.codeBlock( "Some" ) - self.emitter.codeBlock( " Code" ) - self.emitter.close() - self.assertEquals( "Some Code\\n", self.emitter.theFile.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.theFile.getvalue() ) - -.. - - .. class:: small - - |loz| *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. - - -.. _`4`: -.. rubric:: Unit Test Mock Chunk class (4) = -.. parsed-literal:: - :class: code - - - 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:: small - - |loz| *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. - - -.. _`5`: -.. rubric:: Unit Test of Weaver subclass of Emitter (5) = -.. parsed-literal:: - :class: code - - - 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: - pass #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( "Container (\`123\`\_)", result ) - result= self.weaver.referenceTo( "Chunk", 314 ) - self.assertEquals( r"\|srarr\|\\ Chunk (\`314\`\_)", result ) - - def test\_weaver\_should\_codeBegin( self ): - self.weaver.open( self.filename ) - self.weaver.setIndent() - 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() - with open( "testweaver.rst", "r" ) as result: - txt= result.read() - self.assertEquals( "\\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: Container (\`123\`\_)\\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 :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 ): - self.weaver.open( self.filename ) - self.weaver.xrefHead( ) - self.weaver.xrefLine( "Chunk", [ ("Container", 123) ] ) - self.weaver.xrefFoot( ) - #self.weaver.fileEnd( self.aFileChunk ) # Why? - self.weaver.close() - with open( "testweaver.rst", "r" ) as result: - txt= result.read() - self.assertEquals( "\\n:Chunk:\\n \|srarr\|\\\\ (\`('Container', 123)\`\_)\\n\\n", txt ) - - def test\_weaver\_should\_xref\_def( self ): - self.weaver.open( self.filename ) - 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() - with open( "testweaver.rst", "r" ) as result: - txt= result.read() - self.assertEquals( "\\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`_); test_unit.py (`1`_) - - -Note that the XREF data structure seems to have changed without appropriate -unit test support. During version 2.3 (6 Mar 2014) development, this -unit test seemed to have failed. - -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 ): - 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:: small - - |loz| *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. - - -.. _`7`: -.. rubric:: Unit Test of HTML subclass of Emitter (7) = -.. parsed-literal:: - :class: code - - - 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:: small - - |loz| *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 ``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`_); test_unit.py (`1`_) - - -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 ): - 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:: small - - |loz| *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. - - - - -.. _`10`: -.. rubric:: Unit Test of TanglerMake subclass of Emitter (10) = -.. parsed-literal:: - :class: code - - - 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:: small - - |loz| *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. - - - -.. _`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 OutputChunk subclass (`20`_) - |srarr|\ Unit Test of NamedDocumentChunk subclass (`21`_) - -.. - - .. 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 - - - class MockCommand( object ): - def \_\_init\_\_( self ): - self.lineNumber= 314 - def startswith( self, text ): - return False - -.. - - .. class:: small - - |loz| *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. - - -.. _`13`: -.. rubric:: Unit Test of Chunk superclass (13) += -.. parsed-literal:: - :class: code - - - 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:: small - - |loz| *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. - - -.. _`14`: -.. rubric:: Unit Test of Chunk superclass (14) += -.. parsed-literal:: - :class: code - - - 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:: small - - |loz| *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. - - -.. _`15`: -.. rubric:: Unit Test of Chunk superclass (15) += -.. parsed-literal:: - :class: code - - - class TestChunk( unittest.TestCase ): - def setUp( self ): - 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`_); test_unit.py (`1`_) - - -Can we build a Chunk? - - -.. _`16`: -.. rubric:: Unit Test of Chunk construction (16) = -.. parsed-literal:: - :class: code - - - 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) ) - -.. - - .. class:: small - - |loz| *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? - - -.. _`17`: -.. rubric:: Unit Test of Chunk interrogation (17) = -.. parsed-literal:: - :class: code - - - 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 ) - -.. - - .. class:: small - - |loz| *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? - - -.. _`18`: -.. rubric:: Unit Test of Chunk emission (18) = -.. parsed-literal:: - :class: code - - - 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 as e: - self.assertEquals( "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`_); 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. - - -.. _`19`: -.. rubric:: Unit Test of NamedChunk subclass (19) = -.. parsed-literal:: - :class: code - - - 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:: small - - |loz| *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. - - -.. _`20`: -.. rubric:: Unit Test of OutputChunk subclass (20) = -.. parsed-literal:: - :class: code - - - 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:: small - - |loz| *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. - - **TODO** Test ``NamedDocumentChunk``. - - -.. _`21`: -.. rubric:: Unit Test of NamedDocumentChunk subclass (21) = -.. parsed-literal:: - :class: code - - # TODO Test This -.. - - .. class:: small - - |loz| *Unit Test of NamedDocumentChunk subclass (21)*. Used by: Unit Test of Chunk class hierarchy (`11`_); test_unit.py (`1`_) - - -Command Tests ---------------- - - -.. _`22`: -.. rubric:: Unit Test of Command class hierarchy (22) = -.. parsed-literal:: - :class: code - - - |srarr|\ Unit Test of Command superclass (`23`_) - |srarr|\ Unit Test of TextCommand class to contain a document text block (`24`_) - |srarr|\ Unit Test of CodeCommand class to contain a program source code block (`25`_) - |srarr|\ Unit Test of XrefCommand superclass for all cross-reference commands (`26`_) - |srarr|\ Unit Test of FileXrefCommand class for an output file cross-reference (`27`_) - |srarr|\ Unit Test of MacroXrefCommand class for a named chunk cross-reference (`28`_) - |srarr|\ Unit Test of UserIdXrefCommand class for a user identifier cross-reference (`29`_) - |srarr|\ Unit Test of ReferenceCommand class for chunk references (`30`_) - -.. - - .. class:: small - - |loz| *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. - - -.. _`23`: -.. rubric:: Unit Test of Command superclass (23) = -.. parsed-literal:: - :class: code - - # No Tests -.. - - .. class:: small - - |loz| *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. - - -.. _`24`: -.. rubric:: Unit Test of TextCommand class to contain a document text block (24) = -.. parsed-literal:: - :class: code - - - 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:: small - - |loz| *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. - - -.. _`25`: -.. rubric:: Unit Test of CodeCommand class to contain a program source code block (25) = -.. parsed-literal:: - :class: code - - - 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:: small - - |loz| *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. - - -.. _`26`: -.. rubric:: Unit Test of XrefCommand superclass for all cross-reference commands (26) = -.. parsed-literal:: - :class: code - - # No Tests -.. - - .. class:: small - - |loz| *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 ``@o`` -locations. - - -.. _`27`: -.. rubric:: Unit Test of FileXrefCommand class for an output file cross-reference (27) = -.. parsed-literal:: - :class: code - - - 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:: small - - |loz| *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. - - -.. _`28`: -.. rubric:: Unit Test of MacroXrefCommand class for a named chunk cross-reference (28) = -.. parsed-literal:: - :class: code - - - 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:: small - - |loz| *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. - - -.. _`29`: -.. rubric:: Unit Test of UserIdXrefCommand class for a user identifier cross-reference (29) = -.. parsed-literal:: - :class: code - - - 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:: small - - |loz| *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. - - -.. _`30`: -.. rubric:: Unit Test of ReferenceCommand class for chunk references (30) = -.. parsed-literal:: - :class: code - - - 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:: small - - |loz| *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. - - - -.. _`31`: -.. rubric:: Unit Test of Reference class hierarchy (31) = -.. parsed-literal:: - :class: code - - - 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:: small - - |loz| *Unit Test of Reference class hierarchy (31)*. Used by: test_unit.py (`1`_) - - -Web Tests ------------ - -This is more difficult to create mocks for. - - -.. _`32`: -.. rubric:: Unit Test of Web class (32) = -.. parsed-literal:: - :class: code - - - class TestWebConstruction( unittest.TestCase ): - def setUp( self ): - self.web= pyweb.Web() - |srarr|\ Unit Test Web class construction methods (`33`_) - - class TestWebProcessing( unittest.TestCase ): - def setUp( self ): - self.web= pyweb.Web() - 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 (`34`_) - |srarr|\ Unit Test Web class chunk cross-reference (`35`_) - |srarr|\ Unit Test Web class tangle (`36`_) - |srarr|\ Unit Test Web class weave (`37`_) - -.. - - .. class:: small - - |loz| *Unit Test of Web class (32)*. Used by: test_unit.py (`1`_) - - - -.. _`33`: -.. rubric:: Unit Test Web class construction methods (33) = -.. parsed-literal:: - :class: code - - - 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:: small - - |loz| *Unit Test Web class construction methods (33)*. Used by: Unit Test of Web class (`32`_); test_unit.py (`1`_) - - - -.. _`34`: -.. rubric:: Unit Test Web class name resolution methods (34) = -.. parsed-literal:: - :class: code - - - 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 as e: - self.assertTrue( e.args[0].startswith("Cannot resolve 'A File'") ) - -.. - - .. class:: small - - |loz| *Unit Test Web class name resolution methods (34)*. Used by: Unit Test of Web class (`32`_); test_unit.py (`1`_) - - - -.. _`35`: -.. rubric:: Unit Test Web class chunk cross-reference (35) = -.. parsed-literal:: - :class: code - - - 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 ) - -.. - - .. class:: small - - |loz| *Unit Test Web class chunk cross-reference (35)*. Used by: Unit Test of Web class (`32`_); test_unit.py (`1`_) - - - -.. _`36`: -.. rubric:: Unit Test Web class tangle (36) = -.. parsed-literal:: - :class: code - - - 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 ) - -.. - - .. class:: small - - |loz| *Unit Test Web class tangle (36)*. Used by: Unit Test of Web class (`32`_); test_unit.py (`1`_) - - - -.. _`37`: -.. rubric:: Unit Test Web class weave (37) = -.. parsed-literal:: - :class: code - - - 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:: small - - |loz| *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. - - -.. _`38`: -.. rubric:: Unit Test of WebReader class (38) = -.. parsed-literal:: - :class: code - - # Tested via functional tests -.. - - .. class:: small - - |loz| *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. - - -.. _`39`: -.. rubric:: Unit Test of Action class hierarchy (39) = -.. parsed-literal:: - :class: code - - - |srarr|\ Unit test of Action Sequence class (`40`_) - |srarr|\ Unit test of LoadAction class (`43`_) - |srarr|\ Unit test of TangleAction class (`42`_) - |srarr|\ Unit test of WeaverAction class (`41`_) - -.. - - .. class:: small - - |loz| *Unit Test of Action class hierarchy (39)*. Used by: test_unit.py (`1`_) - - - -.. _`40`: -.. rubric:: Unit test of Action Sequence class (40) = -.. parsed-literal:: - :class: code - - - 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 source( self, filename, file ): - self.webFileName= filename - 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 - self.action.options= argparse.Namespace() - 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:: small - - |loz| *Unit test of Action Sequence class (40)*. Used by: Unit Test of Action class hierarchy (`39`_); test_unit.py (`1`_) - - - -.. _`41`: -.. rubric:: Unit test of WeaverAction class (41) = -.. parsed-literal:: - :class: code - - - class TestWeaveAction( unittest.TestCase ): - def setUp( self ): - self.web= MockWeb() - self.action= pyweb.WeaveAction( ) - self.weaver= MockWeaver() - self.action.web= self.web - self.action.options= argparse.Namespace( theWeaver=self.weaver ) - def test\_should\_execute\_weaving( self ): - self.action() - self.assertTrue( self.web.wove is self.weaver ) - -.. - - .. class:: small - - |loz| *Unit test of WeaverAction class (41)*. Used by: Unit Test of Action class hierarchy (`39`_); test_unit.py (`1`_) - - - -.. _`42`: -.. rubric:: Unit test of TangleAction class (42) = -.. parsed-literal:: - :class: code - - - class TestTangleAction( unittest.TestCase ): - def setUp( self ): - self.web= MockWeb() - self.action= pyweb.TangleAction( ) - self.tangler= MockTangler() - self.action.web= self.web - self.action.options= argparse.Namespace( - theTangler= self.tangler ) - def test\_should\_execute\_tangling( self ): - self.action() - self.assertTrue( self.web.tangled is self.tangler ) - -.. - - .. class:: small - - |loz| *Unit test of TangleAction class (42)*. Used by: Unit Test of Action class hierarchy (`39`_); test_unit.py (`1`_) - - - -.. _`43`: -.. rubric:: Unit test of LoadAction class (43) = -.. parsed-literal:: - :class: code - - - class TestLoadAction( unittest.TestCase ): - def setUp( self ): - self.web= MockWeb() - self.action= pyweb.LoadAction( ) - self.webReader= MockWebReader() - self.action.webReader= self.webReader - self.action.web= self.web - self.action.options= argparse.Namespace( webReader= self.webReader, webFileName="TestLoadAction.w" ) - with open("TestLoadAction.w","w") as web: - pass - def tearDown( self ): - try: - os.remove("TestLoadAction.w") - except IOError: - pass - def test\_should\_execute\_loading( self ): - self.action() - self.assertEquals( 1, self.webReader.count ) - -.. - - .. class:: small - - |loz| *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. - - -.. _`44`: -.. rubric:: Unit Test of Application class (44) = -.. parsed-literal:: - :class: code - - # TODO Test Application class -.. - - .. class:: small - - |loz| *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. - - -.. _`45`: -.. rubric:: Unit Test overheads: imports, etc. (45) = -.. parsed-literal:: - :class: code - - from \_\_future\_\_ import print\_function - """Unit tests.""" - import pyweb - import unittest - import logging - import io - import string - import os - import time - import re - import argparse - -.. - - .. class:: small - - |loz| *Unit Test overheads: imports, etc. (45)*. Used by: test_unit.py (`1`_) - - - -.. _`46`: -.. rubric:: Unit Test main (46) = -.. parsed-literal:: - :class: code - - - if \_\_name\_\_ == "\_\_main\_\_": - import sys - logging.basicConfig( stream=sys.stdout, level= logging.WARN ) - unittest.main() - -.. - - .. class:: small - - |loz| *Unit Test main (46)*. 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. - - -.. _`47`: -.. rubric:: test_loader.py (47) = -.. parsed-literal:: - :class: code - - |srarr|\ Load Test overheads: imports, etc. (`53`_) - |srarr|\ Load Test superclass to refactor common setup (`48`_) - |srarr|\ Load Test error handling with a few common syntax errors (`49`_) - |srarr|\ Load Test include processing with syntax errors (`51`_) - |srarr|\ Load Test main program (`54`_) - -.. - - .. class:: small - - |loz| *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. - - -.. _`48`: -.. rubric:: Load Test superclass to refactor common setup (48) = -.. parsed-literal:: - :class: code - - - class ParseTestcase( unittest.TestCase ): - text= "" - file\_name= "" - def setUp( self ): - source= io.StringIO( self.text ) - self.web= pyweb.Web() - self.rdr= pyweb.WebReader() - self.rdr.source( self.file\_name, source ).web( self.web ) - -.. - - .. class:: small - - |loz| *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. - - -.. _`49`: -.. rubric:: Load Test error handling with a few common syntax errors (49) = -.. parsed-literal:: - :class: code - - - |srarr|\ 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 as e: - self.assertEquals( "At ('test1.w', 8): expected ('@{',), found '@o'", e.args[0] ) - -.. - - .. class:: small - - |loz| *Load Test error handling with a few common syntax errors (49)*. Used by: test_loader.py (`47`_) - - - -.. _`50`: -.. rubric:: Sample Document 1 with correct and incorrect syntax (50) = -.. 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 (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. - - -.. _`51`: -.. rubric:: Load Test include processing with syntax errors (51) = -.. parsed-literal:: - :class: code - - - |srarr|\ 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 as e: - self.assertEquals( "At ('test8\_inc.tmp', 4): end of input, ('@{', '@[') not found", e.args[0] ) - def tearDown( self ): - os.remove( 'test8\_inc.tmp' ) - super( Test\_IncludeParseErrors, self ).tearDown() - -.. - - .. class:: small - - |loz| *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``. - - -.. _`52`: -.. rubric:: Sample Document 8 and the file it includes (52) = -.. 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 (52)*. Used by: Load Test include processing with syntax errors (`51`_); test_loader.py (`47`_) - - -

    The overheads for a Python unittest.

    - - -.. _`53`: -.. rubric:: Load Test overheads: imports, etc. (53) = -.. parsed-literal:: - :class: code - - from \_\_future\_\_ import print\_function - """Loader and parsing tests.""" - import pyweb - import unittest - import logging - import os - import io - -.. - - .. class:: small - - |loz| *Load Test overheads: imports, etc. (53)*. Used by: test_loader.py (`47`_) - - -A main program that configures logging and then runs the test. - - -.. _`54`: -.. rubric:: Load Test main program (54) = -.. parsed-literal:: - :class: code - - - if \_\_name\_\_ == "\_\_main\_\_": - import sys - logging.basicConfig( stream=sys.stdout, level= logging.WARN ) - unittest.main() - -.. - - .. class:: small - - |loz| *Load Test main program (54)*. Used by: test_loader.py (`47`_) - - -Tests for Tangling ------------------- - -We need to be able to tangle a web. - - -.. _`55`: -.. rubric:: test_tangler.py (55) = -.. parsed-literal:: - :class: code - - |srarr|\ Tangle Test overheads: imports, etc. (`69`_) - |srarr|\ Tangle Test superclass to refactor common setup (`56`_) - |srarr|\ Tangle Test semantic error 2 (`57`_) - |srarr|\ Tangle Test semantic error 3 (`59`_) - |srarr|\ Tangle Test semantic error 4 (`61`_) - |srarr|\ Tangle Test semantic error 5 (`63`_) - |srarr|\ Tangle Test semantic error 6 (`65`_) - |srarr|\ Tangle Test include error 7 (`67`_) - |srarr|\ Tangle Test main program (`70`_) - -.. - - .. class:: small - - |loz| *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. - - - -.. _`56`: -.. rubric:: Tangle Test superclass to refactor common setup (56) = -.. parsed-literal:: - :class: code - - - class TangleTestcase( unittest.TestCase ): - text= "" - file\_name= "" - error= "" - def setUp( self ): - source= io.StringIO( self.text ) - self.web= pyweb.Web() - 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 as 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 - -.. - - .. class:: small - - |loz| *Tangle Test superclass to refactor common setup (56)*. Used by: test_tangler.py (`55`_) - - - -.. _`57`: -.. rubric:: Tangle Test semantic error 2 (57) = -.. parsed-literal:: - :class: code - - - |srarr|\ 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." ) - -.. - - .. class:: small - - |loz| *Tangle Test semantic error 2 (57)*. Used by: test_tangler.py (`55`_) - - - -.. _`58`: -.. rubric:: Sample Document 2 (58) = -.. 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 (58)*. Used by: Tangle Test semantic error 2 (`57`_); test_tangler.py (`55`_) - - - -.. _`59`: -.. rubric:: Tangle Test semantic error 3 (59) = -.. parsed-literal:: - :class: code - - - |srarr|\ 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." ) - -.. - - .. class:: small - - |loz| *Tangle Test semantic error 3 (59)*. Used by: test_tangler.py (`55`_) - - - -.. _`60`: -.. rubric:: Sample Document 3 (60) = -.. 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 (60)*. Used by: Tangle Test semantic error 3 (`59`_); test_tangler.py (`55`_) - - - - -.. _`61`: -.. rubric:: Tangle Test semantic error 4 (61) = -.. parsed-literal:: - :class: code - - - |srarr|\ 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...'" ) - -.. - - .. class:: small - - |loz| *Tangle Test semantic error 4 (61)*. Used by: test_tangler.py (`55`_) - - - -.. _`62`: -.. rubric:: Sample Document 4 (62) = -.. 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 (62)*. Used by: Tangle Test semantic error 4 (`61`_); test_tangler.py (`55`_) - - - -.. _`63`: -.. rubric:: Tangle Test semantic error 5 (63) = -.. parsed-literal:: - :class: code - - - |srarr|\ 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 ['part1a', 'part1b']" ) - -.. - - .. class:: small - - |loz| *Tangle Test semantic error 5 (63)*. Used by: test_tangler.py (`55`_) - - - -.. _`64`: -.. rubric:: Sample Document 5 (64) = -.. 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 (64)*. Used by: Tangle Test semantic error 5 (`63`_); test_tangler.py (`55`_) - - - -.. _`65`: -.. rubric:: Tangle Test semantic error 6 (65) = -.. parsed-literal:: - :class: code - - - |srarr|\ 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() ) ) - -.. - - .. class:: small - - |loz| *Tangle Test semantic error 6 (65)*. Used by: test_tangler.py (`55`_) - - - -.. _`66`: -.. rubric:: Sample Document 6 (66) = -.. 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 (66)*. Used by: Tangle Test semantic error 6 (`65`_); test_tangler.py (`55`_) - - - -.. _`67`: -.. rubric:: Tangle Test include error 7 (67) = -.. parsed-literal:: - :class: code - - - |srarr|\ 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() - -.. - - .. class:: small - - |loz| *Tangle Test include error 7 (67)*. Used by: test_tangler.py (`55`_) - - - -.. _`68`: -.. rubric:: Sample Document 7 and it's included file (68) = -.. 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 (68)*. Used by: Tangle Test include error 7 (`67`_); test_tangler.py (`55`_) - - - -.. _`69`: -.. rubric:: Tangle Test overheads: imports, etc. (69) = -.. parsed-literal:: - :class: code - - from \_\_future\_\_ import print\_function - """Tangler tests exercise various semantic features.""" - import pyweb - import unittest - import logging - import os - import io - -.. - - .. class:: small - - |loz| *Tangle Test overheads: imports, etc. (69)*. Used by: test_tangler.py (`55`_) - - - -.. _`70`: -.. rubric:: Tangle Test main program (70) = -.. 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 (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. - - -.. _`71`: -.. rubric:: test_weaver.py (71) = -.. parsed-literal:: - :class: code - - |srarr|\ Weave Test overheads: imports, etc. (`78`_) - |srarr|\ Weave Test superclass to refactor common setup (`72`_) - |srarr|\ Weave Test references and definitions (`73`_) - |srarr|\ Weave Test evaluation of expressions (`76`_) - |srarr|\ Weave Test main program (`79`_) - -.. - - .. class:: small - - |loz| *test_weaver.py (71)*. - - -Weaving test cases have a common setup shown in this superclass. - - -.. _`72`: -.. rubric:: Weave Test superclass to refactor common setup (72) = -.. parsed-literal:: - :class: code - - - class WeaveTestcase( unittest.TestCase ): - text= "" - file\_name= "" - error= "" - def setUp( self ): - source= io.StringIO( self.text ) - self.web= pyweb.Web() - 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 as 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 - -.. - - .. class:: small - - |loz| *Weave Test superclass to refactor common setup (72)*. Used by: test_weaver.py (`71`_) - - - -.. _`73`: -.. rubric:: Weave Test references and definitions (73) = -.. parsed-literal:: - :class: code - - - |srarr|\ Sample Document 0 (`74`_) - |srarr|\ 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() - self.maxDiff= None - self.assertEqual( test0\_expected, actual ) - - -.. - - .. class:: small - - |loz| *Weave Test references and definitions (73)*. Used by: test_weaver.py (`71`_) - - - -.. _`74`: -.. rubric:: Sample Document 0 (74) = -.. 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 (74)*. Used by: Weave Test references and definitions (`73`_); test_weaver.py (`71`_) - - - -.. _`75`: -.. rubric:: Expected Output 0 (75) = -.. 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 (75)*. Used by: Weave Test references and definitions (`73`_); test_weaver.py (`71`_) - - -Note that this really requires a mocked ``time`` module in order -to properly provide a consistent output from ``time.asctime()``. - - -.. _`76`: -.. rubric:: Weave Test evaluation of expressions (76) = -.. parsed-literal:: - :class: code - - - |srarr|\ 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)\\n", actual[2] ) - self.assertEquals( 'Version = 2.3\\n', actual[3] ) - self.assertEquals( 'CWD = %s\\n' % os.getcwd(), actual[4] ) - -.. - - .. class:: small - - |loz| *Weave Test evaluation of expressions (76)*. Used by: test_weaver.py (`71`_) - - - -.. _`77`: -.. rubric:: Sample Document 9 (77) = -.. 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 (77)*. Used by: Weave Test evaluation of expressions (`76`_); test_weaver.py (`71`_) - - - -.. _`78`: -.. rubric:: Weave Test overheads: imports, etc. (78) = -.. parsed-literal:: - :class: code - - from \_\_future\_\_ import print\_function - """Weaver tests exercise various weaving features.""" - import pyweb - import unittest - import logging - import os - import string - import io - -.. - - .. class:: small - - |loz| *Weave Test overheads: imports, etc. (78)*. Used by: test_weaver.py (`71`_) - - - -.. _`79`: -.. rubric:: Weave Test main program (79) = -.. parsed-literal:: - :class: code - - - if \_\_name\_\_ == "\_\_main\_\_": - import sys - logging.basicConfig( stream=sys.stdout, level= logging.WARN ) - unittest.main() - -.. - - .. class:: small - - |loz| *Weave Test main program (79)*. Used by: test_weaver.py (`71`_) - - - -Combined Test Script -===================== - -.. test/combined.w - -The combined test script runs all tests in all test modules. - - -.. _`80`: -.. rubric:: test.py (80) = -.. parsed-literal:: - :class: code - - |srarr|\ Combined Test overheads, imports, etc. (`81`_) - |srarr|\ Combined Test suite which imports all other test modules (`82`_) - |srarr|\ Combined Test main script (`83`_) - -.. - - .. class:: small - - |loz| *test.py (80)*. - - -The overheads import unittest and logging, because those are essential -infrastructure. Additionally, each of the test modules is also imported. - - -.. _`81`: -.. rubric:: Combined Test overheads, imports, etc. (81) = -.. parsed-literal:: - :class: code - - """Combined tests.""" - import unittest - import test\_loader - import test\_tangler - import test\_weaver - import test\_unit - import logging - -.. - - .. class:: small - - |loz| *Combined Test overheads, imports, etc. (81)*. Used by: test.py (`80`_) - - -The test suite is built from each of the individual test modules. - - -.. _`82`: -.. rubric:: Combined Test suite which imports all other test modules (82) = -.. parsed-literal:: - :class: code - - - def suite(): - s= unittest.TestSuite() - for m in ( test\_loader, test\_tangler, test\_weaver, test\_unit ): - s.addTests( unittest.defaultTestLoader.loadTestsFromModule( m ) ) - return s - -.. - - .. class:: small - - |loz| *Combined Test suite which imports all other test modules (82)*. Used by: test.py (`80`_) - - -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. - - - -.. _`83`: -.. rubric:: Combined Test main script (83) = -.. parsed-literal:: - :class: code - - - 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) ) - -.. - - .. class:: small - - |loz| *Combined Test main script (83)*. Used by: test.py (`80`_) - - -Additional 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. - - -.. _`84`: -.. rubric:: docutils.conf (84) = -.. 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 (84)*. - - -``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. - - -.. _`85`: -.. rubric:: page-layout.css (85) = -.. 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 (85)*. - - -Indices -======= - -Files ------ - - -:docutils.conf: - |srarr|\ (`84`_) -:page-layout.css: - |srarr|\ (`85`_) -:test.py: - |srarr|\ (`80`_) -:test_loader.py: - |srarr|\ (`47`_) -:test_tangler.py: - |srarr|\ (`55`_) -:test_unit.py: - |srarr|\ (`1`_) -:test_weaver.py: - |srarr|\ (`71`_) - - - -Macros ------- - - -:Combined Test main script: - |srarr|\ (`83`_) -:Combined Test overheads, imports, etc.: - |srarr|\ (`81`_) -:Combined Test suite which imports all other test modules: - |srarr|\ (`82`_) -:Expected Output 0: - |srarr|\ (`75`_) -:Load Test error handling with a few common syntax errors: - |srarr|\ (`49`_) -:Load Test include processing with syntax errors: - |srarr|\ (`51`_) -:Load Test main program: - |srarr|\ (`54`_) -:Load Test overheads: imports, etc.: - |srarr|\ (`53`_) -:Load Test superclass to refactor common setup: - |srarr|\ (`48`_) -:Sample Document 0: - |srarr|\ (`74`_) -:Sample Document 1 with correct and incorrect syntax: - |srarr|\ (`50`_) -:Sample Document 2: - |srarr|\ (`58`_) -:Sample Document 3: - |srarr|\ (`60`_) -:Sample Document 4: - |srarr|\ (`62`_) -:Sample Document 5: - |srarr|\ (`64`_) -:Sample Document 6: - |srarr|\ (`66`_) -:Sample Document 7 and it's included file: - |srarr|\ (`68`_) -:Sample Document 8 and the file it includes: - |srarr|\ (`52`_) -:Sample Document 9: - |srarr|\ (`77`_) -:Tangle Test include error 7: - |srarr|\ (`67`_) -:Tangle Test main program: - |srarr|\ (`70`_) -:Tangle Test overheads: imports, etc.: - |srarr|\ (`69`_) -:Tangle Test semantic error 2: - |srarr|\ (`57`_) -:Tangle Test semantic error 3: - |srarr|\ (`59`_) -:Tangle Test semantic error 4: - |srarr|\ (`61`_) -:Tangle Test semantic error 5: - |srarr|\ (`63`_) -:Tangle Test semantic error 6: - |srarr|\ (`65`_) -:Tangle Test superclass to refactor common setup: - |srarr|\ (`56`_) -:Unit Test Mock Chunk class: - |srarr|\ (`4`_) -:Unit Test Web class chunk cross-reference: - |srarr|\ (`35`_) -:Unit Test Web class construction methods: - |srarr|\ (`33`_) -:Unit Test Web class name resolution methods: - |srarr|\ (`34`_) -:Unit Test Web class tangle: - |srarr|\ (`36`_) -:Unit Test Web class weave: - |srarr|\ (`37`_) -:Unit Test main: - |srarr|\ (`46`_) -:Unit Test of Action class hierarchy: - |srarr|\ (`39`_) -:Unit Test of Application class: - |srarr|\ (`44`_) -: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|\ (`25`_) -:Unit Test of Command class hierarchy: - |srarr|\ (`22`_) -:Unit Test of Command superclass: - |srarr|\ (`23`_) -: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|\ (`27`_) -: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|\ (`28`_) -:Unit Test of NamedChunk subclass: - |srarr|\ (`19`_) -:Unit Test of NamedDocumentChunk subclass: - |srarr|\ (`21`_) -:Unit Test of OutputChunk subclass: - |srarr|\ (`20`_) -:Unit Test of Reference class hierarchy: - |srarr|\ (`31`_) -:Unit Test of ReferenceCommand class for chunk references: - |srarr|\ (`30`_) -: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|\ (`24`_) -:Unit Test of UserIdXrefCommand class for a user identifier cross-reference: - |srarr|\ (`29`_) -:Unit Test of Weaver subclass of Emitter: - |srarr|\ (`5`_) -:Unit Test of Web class: - |srarr|\ (`32`_) -:Unit Test of WebReader class: - |srarr|\ (`38`_) -:Unit Test of XrefCommand superclass for all cross-reference commands: - |srarr|\ (`26`_) -:Unit Test overheads: imports, etc.: - |srarr|\ (`45`_) -:Unit test of Action Sequence class: - |srarr|\ (`40`_) -:Unit test of LoadAction class: - |srarr|\ (`43`_) -:Unit test of TangleAction class: - |srarr|\ (`42`_) -:Unit test of WeaverAction class: - |srarr|\ (`41`_) -:Weave Test evaluation of expressions: - |srarr|\ (`76`_) -:Weave Test main program: - |srarr|\ (`79`_) -:Weave Test overheads: imports, etc.: - |srarr|\ (`78`_) -:Weave Test references and definitions: - |srarr|\ (`73`_) -:Weave Test superclass to refactor common setup: - |srarr|\ (`72`_) - - - -User Identifiers ----------------- - -(None) - - ----------- - -.. class:: small - - - Created by ../pyweb.py at Tue Mar 11 10:12:14 2014. - - pyweb.__version__ '2.3'. - - Source combined.w modified Fri Mar 7 09:51:12 2014. - - Working directory '/Users/slott/Documents/Projects/pyWeb-2.3/pyweb/test'. - diff --git a/test/test_loader.py b/test/test_loader.py deleted file mode 100644 index aabb179..0000000 --- a/test/test_loader.py +++ /dev/null @@ -1,111 +0,0 @@ - -import logging.handlers -from pathlib import Path - -"""Loader and parsing tests.""" -import io -import logging -import os -from pathlib import Path -import string -import types -import unittest - -import pyweb - - -class ParseTestcase(unittest.TestCase): - text = "" - file_path: Path - def setUp(self) -> None: - self.source = io.StringIO(self.text) - self.web = pyweb.Web() - self.rdr = pyweb.WebReader() - - - -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_path = Path("test1.w") - def setUp(self) -> None: - super().setUp() - self.logger = logging.getLogger("WebReader") - self.buffer = logging.handlers.BufferingHandler(12) - self.buffer.setLevel(logging.WARN) - self.logger.addHandler(self.buffer) - self.logger.setLevel(logging.WARN) - def test_error_should_count_1(self) -> None: - self.rdr.load(self.web, self.file_path, self.source) - self.assertEqual(3, self.rdr.errors) - messages = [r.message for r in self.buffer.buffer] - self.assertEqual( - ["At ('test1.w', 8): expected ('@{',), found '@o'", - "Extra '@{' (possibly missing chunk name) near ('test1.w', 9)", - "Extra '@{' (possibly missing chunk name) near ('test1.w', 9)"], - messages - ) - def tearDown(self) -> None: - self.logger.setLevel(logging.CRITICAL) - self.logger.removeHandler(self.buffer) - super().tearDown() - - - - -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_path = Path("test8.w") - def setUp(self) -> None: - super().setUp() - Path('test8_inc.tmp').write_text(test8_inc_w) - self.logger = logging.getLogger("WebReader") - self.buffer = logging.handlers.BufferingHandler(12) - self.buffer.setLevel(logging.WARN) - self.logger.addHandler(self.buffer) - self.logger.setLevel(logging.WARN) - def test_error_should_count_2(self) -> None: - self.rdr.load(self.web, self.file_path, self.source) - self.assertEqual(1, self.rdr.errors) - messages = [r.message for r in self.buffer.buffer] - self.assertEqual( - ["At ('test8_inc.tmp', 4): end of input, ('@{', '@[') not found", - "Errors in included file 'test8_inc.tmp', output is incomplete."], - messages - ) - def tearDown(self) -> None: - self.logger.setLevel(logging.CRITICAL) - self.logger.removeHandler(self.buffer) - Path('test8_inc.tmp').unlink() - super().tearDown() - - -if __name__ == "__main__": - import sys - logging.basicConfig(stream=sys.stdout, level=logging.WARN) - unittest.main() - diff --git a/tests.w b/tests.w index ac181af..1368519 100644 --- a/tests.w +++ b/tests.w @@ -3,23 +3,23 @@ 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. diff --git a/test/docutils.conf b/tests/docutils.conf similarity index 100% rename from test/docutils.conf rename to tests/docutils.conf diff --git a/test/func.w b/tests/func.w similarity index 88% rename from test/func.w rename to tests/func.w index a777416..0090fde 100644 --- a/test/func.w +++ b/tests/func.w @@ -20,9 +20,13 @@ We need to be able to load a web from one or more source files. @o test_loader.py @{@ + @ + @ + @ + @ @} @@ -35,8 +39,9 @@ input object to the ``WebReader`` instance. @d Load Test superclass... @{ class ParseTestcase(unittest.TestCase): - text = "" - file_path: Path + text: ClassVar[str] + file_path: ClassVar[Path] + def setUp(self) -> None: self.source = io.StringIO(self.text) self.web = pyweb.Web() @@ -51,6 +56,7 @@ find an expected next token. @{ import logging.handlers from pathlib import Path +from typing import ClassVar @} @d Load Test error handling... @@ -60,28 +66,17 @@ from pathlib import Path class Test_ParseErrors(ParseTestcase): text = test1_w file_path = Path("test1.w") - def setUp(self) -> None: - super().setUp() - self.logger = logging.getLogger("WebReader") - self.buffer = logging.handlers.BufferingHandler(12) - self.buffer.setLevel(logging.WARN) - self.logger.addHandler(self.buffer) - self.logger.setLevel(logging.WARN) def test_error_should_count_1(self) -> None: - self.rdr.load(self.web, self.file_path, self.source) + 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) - messages = [r.message for r in self.buffer.buffer] - self.assertEqual( - ["At ('test1.w', 8): expected ('@@{',), found '@@o'", - "Extra '@@{' (possibly missing chunk name) near ('test1.w', 9)", - "Extra '@@{' (possibly missing chunk name) near ('test1.w', 9)"], - messages + 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)" + ] ) - def tearDown(self) -> None: - self.logger.setLevel(logging.CRITICAL) - self.logger.removeHandler(self.buffer) - super().tearDown() - @} @d Sample Document 1... @@ -104,7 +99,8 @@ 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. +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... @{ @@ -116,25 +112,19 @@ class Test_IncludeParseErrors(ParseTestcase): def setUp(self) -> None: super().setUp() Path('test8_inc.tmp').write_text(test8_inc_w) - self.logger = logging.getLogger("WebReader") - self.buffer = logging.handlers.BufferingHandler(12) - self.buffer.setLevel(logging.WARN) - self.logger.addHandler(self.buffer) - self.logger.setLevel(logging.WARN) def test_error_should_count_2(self) -> None: - self.rdr.load(self.web, self.file_path, self.source) + 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) - messages = [r.message for r in self.buffer.buffer] - self.assertEqual( - ["At ('test8_inc.tmp', 4): end of input, ('@@{', '@@[') not found", - "Errors in included file 'test8_inc.tmp', output is incomplete."], - messages - ) + 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: - self.logger.setLevel(logging.CRITICAL) - self.logger.removeHandler(self.buffer) - Path('test8_inc.tmp').unlink() super().tearDown() + Path('test8_inc.tmp').unlink() @} The sample document must reference the correct name that will @@ -165,6 +155,7 @@ import logging import os from pathlib import Path import string +import sys import types import unittest @@ -176,7 +167,6 @@ 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() @} @@ -207,14 +197,16 @@ exceptions raised. @d Tangle Test superclass... @{ class TangleTestcase(unittest.TestCase): - text = "" - error = "" - file_path: Path + 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) @@ -223,6 +215,7 @@ class TangleTestcase(unittest.TestCase): 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() @@ -395,6 +388,7 @@ import io import logging import os from pathlib import Path +from typing import ClassVar import unittest import pyweb @@ -426,9 +420,10 @@ Weaving test cases have a common setup shown in this superclass. @d Weave Test superclass... @{ class WeaveTestcase(unittest.TestCase): - text = "" - error = "" - file_path: Path + text: ClassVar[str] + error: ClassVar[str] + file_path: ClassVar[Path] + def setUp(self) -> None: self.source = io.StringIO(self.text) self.web = pyweb.Web() @@ -526,9 +521,14 @@ 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( ) @@ -537,7 +537,7 @@ class TestEvaluations(WeaveTestcase): actual = self.file_path.with_suffix(".html").read_text().splitlines() #print(actual) self.assertEqual("An anonymous chunk.", actual[0]) - self.assertTrue(actual[1].startswith("Time =")) + 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]) @@ -561,6 +561,8 @@ import logging import os from pathlib import Path import string +import sys +from typing import ClassVar import unittest import pyweb @@ -569,7 +571,6 @@ import pyweb @d Weave Test main program... @{ if __name__ == "__main__": - import sys logging.basicConfig(stream=sys.stderr, level=logging.WARN) unittest.main() @} diff --git a/test/intro.w b/tests/intro.w similarity index 100% rename from test/intro.w rename to tests/intro.w diff --git a/test/page-layout.css b/tests/page-layout.css similarity index 100% rename from test/page-layout.css rename to tests/page-layout.css 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/test/pyweb_test.html b/tests/pyweb_test.html similarity index 78% rename from test/pyweb_test.html rename to tests/pyweb_test.html index 56ed07a..a4a3874 100644 --- a/test/pyweb_test.html +++ b/tests/pyweb_test.html @@ -417,12 +417,19 @@

    Yet Another Lite
  • Tests for Weaving
  • -
  • Combined Test Runner
  • -
  • Additional Files
  • -
  • Indices @@ -555,7 +562,7 @@

    Unit Testing

    This gives us the following outline for unit testing.

    test_unit.py (1) =

    -→Unit Test overheads: imports, etc. (48)
    +→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)
    @@ -564,7 +571,7 @@ 

    Unit Testing

    →Unit Test of WebReader class (39), →(40), →(41) →Unit Test of Action class hierarchy (42) →Unit Test of Application class (47) -→Unit Test main (49) +→Unit Test main (50)
    @@ -638,28 +645,42 @@

    Emitter Tests

    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.

    +

    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) =

    -class MockChunk:
    -    def __init__(self, name: str, seq: int, lineNumber: int) -> None:
    -        self.name = name
    -        self.fullName = name
    -        self.seq = seq
    -        self.lineNumber = lineNumber
    -        self.initial = True
    -        self.commands = []
    -        self.referencedBy = []
    -    def __repr__(self) -> str:
    -        return f"({self.name!r}, {self.seq!r})"
    -    def references(self, aWeaver: pyweb.Weaver) -> list[str]:
    -        return [(c.name, c.seq) for c in self.referencedBy]
    -    def reference_indent(self, aWeb: "Web", aTangler: "Tangler", amount: int) -> None:
    -        aTangler.addIndent(amount)
    -    def reference_dedent(self, aWeb: "Web", aTangler: "Tangler") -> None:
    -        aTangler.clrIndent()
    -    def tangle(self, aWeb: "Web", aTangler: "Tangler") -> None:
    -        aTangler.write(self.name)
    +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
    +)
     
    @@ -674,9 +695,11 @@

    Emitter Tests

    self.weaver.reference_style = pyweb.SimpleReference() self.filepath = Path("testweaver") self.aFileChunk = MockChunk("File", 123, 456) - self.aFileChunk.referencedBy = [ ] + 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() @@ -690,6 +713,8 @@

    Emitter Tests

    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) @@ -752,6 +777,8 @@

    Emitter Tests

    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() @@ -762,9 +789,23 @@

    Emitter Tests

    result = self.weaver.quote("\\end{Verbatim}") self.assertEqual("\\end\\,{Verbatim}", result) result = self.weaver.references(self.aChunk) - self.assertEqual("\n \\footnotesize\n Used by:\n \\begin{list}{}{}\n \n \\item Code example File (123) (Sect. \\ref{pyweb123}, p. \\pageref{pyweb123})\n\n \\end{list}\n \\normalsize\n", result) + 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)])
    @@ -782,13 +823,14 @@

    Emitter Tests

    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) @@ -796,14 +838,16 @@

    Emitter Tests

    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 just a template change.

    +

    The unique feature of the HTMLShort class is a template change.

    -To Do Test HTMLShort.
    +TODO: Test HTMLShort.

    Unit Test of HTMLShort subclass of Emitter (8) =

     # TODO: Finish this
    @@ -853,9 +897,6 @@ 

    Emitter Tests

    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 either -need to wait for a full second to elapse or we need to mock the various -os and filecmp features used by TanglerMake.

    Unit Test of TanglerMake subclass of Emitter (10) =

     class TestTanglerMake(unittest.TestCase):
    @@ -863,7 +904,7 @@ 

    Emitter Tests

    self.tangler = pyweb.TanglerMake() self.filepath = Path("testtangler.code") self.aChunk = MockChunk("Chunk", 314, 278) - #self.aChunk.references_list = [ ("Container", 123) ] + #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")) @@ -871,7 +912,6 @@

    Emitter Tests

    self.tangler.close() self.time_original = self.filepath.stat().st_mtime self.original = self.filepath.stat() - #time.sleep(0.75) # Alternative to assure timestamps must be different def tearDown(self) -> None: try: @@ -921,15 +961,19 @@

    Chunk Tests

    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.

    +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:
    -    def __init__(self) -> None:
    -        self.lineNumber = 314
    -    def startswith(self, text: str) -> bool:
    -        return False
    +MockCommand = Mock(
    +    name="Command class",
    +    side_effect=lambda: Mock(
    +        name="Command instance",
    +        # text="",  # Only used for TextCommand.
    +        lineNumber=314,
    +        startswith=Mock(return_value=False)
    +    )
    +)
     
    @@ -938,100 +982,76 @@

    Chunk Tests

    A MockWeb can contain a Chunk.

    Unit Test of Chunk superclass (13) +=

    -class MockWeb:
    -    def __init__(self) -> None:
    -        self.chunks = []
    -        self.wove = None
    -        self.tangled = None
    -    def add(self, aChunk: pyweb.Chunk) -> None:
    -        self.chunks.append(aChunk)
    -    def addNamed(self, aChunk: pyweb.Chunk) -> None:
    -        self.chunks.append(aChunk)
    -    def addOutput(self, aChunk: pyweb.Chunk) -> None:
    -        self.chunks.append(aChunk)
    -    def fullNameFor(self, name: str) -> str:
    -        return name
    -    def fileXref(self) -> dict[str, list[int]]:
    -        return {'file': [1,2,3]}
    -    def chunkXref(self) -> dict[str, list[int]]:
    -        return {'chunk': [4,5,6]}
    -    def userNamesXref(self) -> dict[str, list[int]]:
    -        return {'name': (7, [8,9,10])}
    -    def getchunk(self, name: str) -> list[pyweb.Chunk]:
    -        return [MockChunk(name, 1, 314)]
    -    def createUsedBy(self) -> None:
    -        pass
    -    def weaveChunk(self, name, weaver) -> None:
    -        weaver.write(name)
    -    def weave(self, weaver) -> None:
    -        self.wove = weaver
    -    def tangle(self, tangler) -> None:
    -        self.tangled = tangler
    +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 MockTangle can process a Chunk.

    +

    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) +=

    -class MockWeaver:
    -    def __init__(self) -> None:
    -        self.begin_chunk = []
    -        self.end_chunk = []
    -        self.written = []
    -        self.code_indent = None
    -    def quote(self, text: str) -> str:
    -        return text.replace("&", "&amp;") # token quoting
    -    def docBegin(self, aChunk: pyweb.Chunk) -> None:
    -        self.begin_chunk.append(aChunk)
    -    def write(self, text: str) -> None:
    -        self.written.append(text)
    -    def docEnd(self, aChunk: pyweb.Chunk) -> None:
    -        self.end_chunk.append(aChunk)
    -    def codeBegin(self, aChunk: pyweb.Chunk) -> None:
    -        self.begin_chunk.append(aChunk)
    -    def codeBlock(self, text: str) -> None:
    -        self.written.append(text)
    -    def codeEnd(self, aChunk: pyweb.Chunk) -> None:
    -        self.end_chunk.append(aChunk)
    -    def fileBegin(self, aChunk: pyweb.Chunk) -> None:
    -        self.begin_chunk.append(aChunk)
    -    def fileEnd(self, aChunk: pyweb.Chunk) -> None:
    -        self.end_chunk.append(aChunk)
    -    def addIndent(self, increment=0):
    -        pass
    -    def setIndent(self, fixed: int | None=None, command: str | None=None) -> None:
    -        self.indent = fixed
    -    def addIndent(self, increment: int = 0) -> None:
    -        self.indent = increment
    -    def clrIndent(self) -> None:
    -        pass
    -    def xrefHead(self) -> None:
    -        pass
    -    def xrefLine(self, name: str, refList: list[int]) -> None:
    -        self.written.append(f"{name} {refList}")
    -    def xrefDefLine(self, name: str, defn: int, refList: list[int]) -> None:
    -        self.written.append(f"{name} {defn} {refList}")
    -    def xrefFoot(self) -> None:
    -        pass
    -    def referenceTo(self, name: str, seq: int) -> None:
    -        pass
    -    def open(self, aFile: str) -> "MockWeaver":
    -        return self
    -    def close(self) -> None:
    -        pass
    -    def __enter__(self) -> "MockWeaver":
    -        return self
    -    def __exit__(self, *args: Any) -> bool:
    -        return False
    -
    -class MockTangler(MockWeaver):
    -    def __init__(self) -> None:
    -        super().__init__()
    -        self.context = [0]
    -    def addIndent(self, amount: int) -> None:
    -        pass
    +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
    +)
     
    @@ -1043,8 +1063,11 @@

    Chunk Tests

    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)
    @@ -1057,28 +1080,32 @@

    Chunk Tests

    def test_append_command_should_work(self) -> None: cmd1 = MockCommand() self.theChunk.append(cmd1) - self.assertEqual(1, len(self.theChunk.commands) ) + 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(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.assertEqual(1, len(self.theChunk.commands)) self.theChunk.appendText("&more text") - self.assertEqual(1, len(self.theChunk.commands) ) + 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) ) + self.assertEqual(2, len(self.theChunk.commands)) + assert cmd1.chunk == self.theChunk -def test_append_to_web_should_work(self) -> None: +def test_append_chunk_to_web_should_work(self) -> None: web = MockWeb() self.theChunk.webAdd(web) - self.assertEqual(1, len(web.chunks)) + self.assertEqual(web.add.mock_calls, [call(self.theChunk)])
    @@ -1110,6 +1137,7 @@

    Chunk Tests

    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") @@ -1129,16 +1157,14 @@

    Chunk Tests

    Can we emit a Chunk with a weaver or tangler?

    Unit Test of Chunk emission (18) =

    -def test_weave_should_work(self) -> None:
    +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(1, len(wvr.begin_chunk))
    -    self.assertTrue(wvr.begin_chunk[0] is self.theChunk)
    -    self.assertEqual(1, len(wvr.end_chunk))
    -    self.assertTrue(wvr.end_chunk[0] is self.theChunk)
    -    self.assertEqual("this chunk has very & many words", "".join( wvr.written))
    +    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()
    @@ -1171,30 +1197,27 @@ 

    Chunk Tests

    self.assertEqual("index", self.theChunk.getUserIDRefs()[0]) self.assertEqual("terms", self.theChunk.getUserIDRefs()[1]) - def test_append_to_web_should_work(self) -> None: + def test_append_named_chunk_to_web_should_work(self) -> None: web = MockWeb() self.theChunk.webAdd(web) - self.assertEqual(1, len(web.chunks)) + 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(1, len(wvr.begin_chunk)) - self.assertTrue(wvr.begin_chunk[0] is self.theChunk) - self.assertEqual(1, len(wvr.end_chunk)) - self.assertTrue(wvr.end_chunk[0] is self.theChunk) - self.assertEqual("the words &amp; text of this Chunk", "".join( wvr.written)) + 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(1, len(tnglr.begin_chunk)) - self.assertTrue(tnglr.begin_chunk[0] is self.theChunk) - self.assertEqual(1, len(tnglr.end_chunk)) - self.assertTrue(tnglr.end_chunk[0] is self.theChunk) - self.assertEqual("the words & text of this Chunk", "".join( tnglr.written)) + 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)])
    @@ -1204,7 +1227,7 @@

    Chunk Tests

     class TestNamedChunk_Noindent(unittest.TestCase):
         def setUp(self) -> None:
    -        self.theChunk = pyweb.NamedChunk_Noindent("Some Name...")
    +        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")
    @@ -1212,11 +1235,13 @@ 

    Chunk Tests

    tnglr = MockTangler() web = MockWeb() self.theChunk.tangle(web, tnglr) - self.assertEqual(1, len(tnglr.begin_chunk)) - self.assertTrue(tnglr.begin_chunk[0] is self.theChunk) - self.assertEqual(1, len(tnglr.end_chunk)) - self.assertTrue(tnglr.end_chunk[0] is self.theChunk) - self.assertEqual("the words & text of this Chunk", "".join( tnglr.written)) + + self.assertEqual(tnglr.mock_calls, [ + call.codeBegin(self.theChunk), + call.codeBlock('the words & text of this Chunk'), + call.codeEnd(self.theChunk) + ] + )
    @@ -1234,30 +1259,33 @@

    Chunk Tests

    self.theChunk.append(cmd) self.theChunk.setUserIDRefs("index terms") - def test_append_to_web_should_work(self) -> None: + def test_append_output_chunk_to_web_should_work(self) -> None: web = MockWeb() self.theChunk.webAdd(web) - self.assertEqual(1, len(web.chunks)) + 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(1, len(wvr.begin_chunk)) - self.assertTrue(wvr.begin_chunk[0] is self.theChunk) - self.assertEqual(1, len(wvr.end_chunk)) - self.assertTrue(wvr.end_chunk[0] is self.theChunk) - self.assertEqual("the words &amp; text of this Chunk", "".join( wvr.written)) + 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(1, len(tnglr.begin_chunk)) - self.assertTrue(tnglr.begin_chunk[0] is self.theChunk) - self.assertEqual(1, len(tnglr.end_chunk)) - self.assertTrue(tnglr.end_chunk[0] is self.theChunk) - self.assertEqual("the words & text of this Chunk", "".join( tnglr.written)) + self.assertEqual(tnglr.mock_calls, [ + call.codeBegin(self.theChunk), + call.codeBlock('the words & text of this Chunk'), + call.codeEnd(self.theChunk) + ] + )
    @@ -1318,16 +1346,18 @@

    Command Tests

    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("Some text & words in the document\n ", "".join( wvr.written)) + 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("Some text & words in the document\n ", "".join( tnglr.written)) + self.assertEqual(tnglr.write.mock_calls, [call('Some text & words in the document\n ')])
    @@ -1339,16 +1369,18 @@

    Command Tests

    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("Some text &amp; words in the document\n ", "".join( wvr.written)) + 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("Some text & words in the document\n ", "".join( tnglr.written)) + self.assertEqual(tnglr.codeBlock.mock_calls, [call('Some text & words in the document\n ')])
    @@ -1370,11 +1402,13 @@

    Command Tests

    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("file [1, 2, 3]", "".join( wvr.written)) + 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() @@ -1395,11 +1429,13 @@

    Command Tests

    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("chunk [4, 5, 6]", "".join( wvr.written)) + 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() @@ -1420,11 +1456,13 @@

    Command Tests

    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("name 7 [8, 9, 10]", "".join( wvr.written)) + 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() @@ -1450,17 +1488,19 @@

    Command Tests

    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("Some Name", "".join( wvr.written)) + 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("Some Name", "".join( tnglr.written)) + self.assertEqual(tnglr.write.mock_calls, [call('Some Name')])
    @@ -1602,11 +1642,13 @@

    Web Tests

    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)) @@ -1615,6 +1657,7 @@

    Web Tests

    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)) @@ -1638,8 +1681,12 @@

    Web Tests

    def test_valid_web_should_tangle(self) -> None: tangler = MockTangler() self.web.tangle(tangler) - self.assertEqual(3, len(tangler.written)) - self.assertEqual(['some code', 'some user2a code', 'some user1 code'], tangler.written) + self.assertEqual(tangler.codeBlock.mock_calls, [ + call('some code'), + call('some user2a code'), + call('some user1 code'), + ] + )
    @@ -1650,9 +1697,16 @@

    Web Tests

    def test_valid_web_should_weave(self) -> None: weaver = MockWeaver() self.web.weave(weaver) - self.assertEqual(6, len(weaver.written)) - expected = ['some text', 'some code', None, 'some user2a code', None, 'some user1 code'] - self.assertEqual(expected, weaver.written) + 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'), + ] + )
    @@ -1745,46 +1799,21 @@

    Action Tests

    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 MockAction:
    -    def __init__(self) -> None:
    -        self.count = 0
    -    def __call__(self) -> None:
    -        self.count += 1
    -
    -class MockWebReader:
    -    def __init__(self) -> None:
    -        self.count = 0
    -        self.theWeb = None
    -        self.errors = 0
    -    def web(self, aWeb: "Web") -> None:
    -        """Deprecated"""
    -        warnings.warn("deprecated", DeprecationWarning)
    -        self.theWeb = aWeb
    -        return self
    -    def source(self, filename: str, file: TextIO) -> str:
    -        """Deprecated"""
    -        warnings.warn("deprecated", DeprecationWarning)
    -        self.webFileName = filename
    -    def load(self, aWeb: pyweb.Web, filename: str, source: TextIO | None = None) -> None:
    -        self.theWeb = aWeb
    -        self.webFileName = filename
    -        self.count += 1
    -
     class TestActionSequence(unittest.TestCase):
         def setUp(self) -> None:
             self.web = MockWeb()
    -        self.a1 = MockAction()
    -        self.a2 = MockAction()
    +        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()
    -        for c in self.action.opSequence:
    -            self.assertEqual(1, c.count)
    -            self.assertTrue(self.web is c.web)
    +        self.assertEqual(self.a1.call_count, 1)
    +        self.assertEqual(self.a2.call_count, 1)
     
    @@ -1805,7 +1834,7 @@

    Action Tests

    ) def test_should_execute_weaving(self) -> None: self.action() - self.assertTrue(self.web.wove is self.weaver) + self.assertEqual(self.web.weave.mock_calls, [call(self.weaver)])
    @@ -1826,23 +1855,28 @@

    Action Tests

    ) def test_should_execute_tangling(self) -> None: self.action() - self.assertTrue(self.web.tangled is self.tangler) + 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 = MockWebReader()
    +        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=Path("TestLoadAction.w"),
    +            source_path=self.source_path,
                 command="@",
                 permitList = [],
                 output=Path.cwd(),
    @@ -1855,7 +1889,11 @@ 

    Action Tests

    pass def test_should_execute_loading(self) -> None: self.action() - self.assertEqual(1, self.webReader.count) + # 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
    @@ -1866,6 +1904,7 @@

    Action Tests

    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
    @@ -1888,9 +1927,12 @@ 

    Overheads and Main ScriptOverheads and Main Script

    Unit Test overheads: imports, etc. (48). Used by: test_unit.py (1)

    -

    Unit Test main (49) =

    +

    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__":
    -    import sys
         logging.basicConfig(stream=sys.stdout, level=logging.WARN)
         unittest.main()
     
    -

    Unit Test main (49). Used by: test_unit.py (1)

    +

    Unit Test main (50). Used by: test_unit.py (1)

    We run the default unittest.main() to execute the entire suite of tests.

    @@ -1926,27 +1978,32 @@

    Functional Testing

    Tests for Loading

    We need to be able to load a web from one or more source files.

    -

    test_loader.py (50) =

    +

    test_loader.py (51) =

    -→Load Test overheads: imports, etc. (52), →(57)
    -→Load Test superclass to refactor common setup (51)
    -→Load Test error handling with a few common syntax errors (53)
    -→Load Test include processing with syntax errors (55)
    -→Load Test main program (58)
    +→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 (50).

    +

    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 (51) =

    +

    Load Test superclass to refactor common setup (52) =

     class ParseTestcase(unittest.TestCase):
    -    text = ""
    -    file_path: Path
    +    text: ClassVar[str]
    +    file_path: ClassVar[Path]
    +
         def setUp(self) -> None:
             self.source = io.StringIO(self.text)
             self.web = pyweb.Web()
    @@ -1954,54 +2011,45 @@ 

    Tests for Loading

    -

    Load Test superclass to refactor common setup (51). Used by: test_loader.py (50)

    +

    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. (52) =

    +

    Load Test overheads: imports, etc. (53) =

     import logging.handlers
     from pathlib import Path
    +from typing import ClassVar
     
    -

    Load Test overheads: imports, etc. (52). Used by: test_loader.py (50)

    +

    Load Test overheads: imports, etc. (53). Used by: test_loader.py (51)

    -

    Load Test error handling with a few common syntax errors (53) =

    +

    Load Test error handling with a few common syntax errors (54) =

    -→Sample Document 1 with correct and incorrect syntax (54)
    +→Sample Document 1 with correct and incorrect syntax (55)
     
     class Test_ParseErrors(ParseTestcase):
         text = test1_w
         file_path = Path("test1.w")
    -    def setUp(self) -> None:
    -        super().setUp()
    -        self.logger = logging.getLogger("WebReader")
    -        self.buffer = logging.handlers.BufferingHandler(12)
    -        self.buffer.setLevel(logging.WARN)
    -        self.logger.addHandler(self.buffer)
    -        self.logger.setLevel(logging.WARN)
         def test_error_should_count_1(self) -> None:
    -        self.rdr.load(self.web, self.file_path, self.source)
    +        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)
    -        messages = [r.message for r in self.buffer.buffer]
    -        self.assertEqual(
    -            ["At ('test1.w', 8): expected ('@{',), found '@o'",
    -            "Extra '@{' (possibly missing chunk name) near ('test1.w', 9)",
    -            "Extra '@{' (possibly missing chunk name) near ('test1.w', 9)"],
    -            messages
    +        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)"
    +            ]
             )
    -    def tearDown(self) -> None:
    -        self.logger.setLevel(logging.CRITICAL)
    -        self.logger.removeHandler(self.buffer)
    -        super().tearDown()
     
    -

    Load Test error handling with a few common syntax errors (53). Used by: test_loader.py (50)

    +

    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 (54) =

    +

    Sample Document 1 with correct and incorrect syntax (55) =

     test1_w = """Some anonymous chunk
     @o test1.tmp
    @@ -2016,17 +2064,18 @@ 

    Tests for Loading

    -

    Sample Document 1 with correct and incorrect syntax (54). Used by: Load Test error handling... (53)

    +

    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.

    -

    Load Test include processing with syntax errors (55) =

    +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 (56)
    +→Sample Document 8 and the file it includes (57)
     
     class Test_IncludeParseErrors(ParseTestcase):
         text = test8_w
    @@ -2034,33 +2083,27 @@ 

    Tests for Loading

    def setUp(self) -> None: super().setUp() Path('test8_inc.tmp').write_text(test8_inc_w) - self.logger = logging.getLogger("WebReader") - self.buffer = logging.handlers.BufferingHandler(12) - self.buffer.setLevel(logging.WARN) - self.logger.addHandler(self.buffer) - self.logger.setLevel(logging.WARN) def test_error_should_count_2(self) -> None: - self.rdr.load(self.web, self.file_path, self.source) + 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) - messages = [r.message for r in self.buffer.buffer] - self.assertEqual( - ["At ('test8_inc.tmp', 4): end of input, ('@{', '@[') not found", - "Errors in included file 'test8_inc.tmp', output is incomplete."], - messages + 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: - self.logger.setLevel(logging.CRITICAL) - self.logger.removeHandler(self.buffer) - Path('test8_inc.tmp').unlink() super().tearDown() + Path('test8_inc.tmp').unlink()
    -

    Load Test include processing with syntax errors (55). Used by: test_loader.py (50)

    +

    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 (56) =

    +

    Sample Document 8 and the file it includes (57) =

     test8_w = """Some anonymous chunk.
     @d title @[the title of this document, defined with @@[ and @@]@]
    @@ -2076,10 +2119,10 @@ 

    Tests for Loading

    -

    Sample Document 8 and the file it includes (56). Used by: Load Test include... (55)

    +

    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. (57) +=

    +

    Load Test overheads: imports, etc. (58) +=

     """Loader and parsing tests."""
     import io
    @@ -2087,6 +2130,7 @@ 

    Tests for Loading

    import os from pathlib import Path import string +import sys import types import unittest @@ -2094,55 +2138,56 @@

    Tests for Loading

    -

    Load Test overheads: imports, etc. (57). Used by: test_loader.py (50)

    +

    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 (58) =

    +

    Load Test main program (59) =

     if __name__ == "__main__":
    -    import sys
         logging.basicConfig(stream=sys.stdout, level=logging.WARN)
         unittest.main()
     
    -

    Load Test main program (58). Used by: test_loader.py (50)

    +

    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 (59) =

    +

    test_tangler.py (60) =

    -→Tangle Test overheads: imports, etc. (73)
    -→Tangle Test superclass to refactor common setup (60)
    -→Tangle Test semantic error 2 (61)
    -→Tangle Test semantic error 3 (63)
    -→Tangle Test semantic error 4 (65)
    -→Tangle Test semantic error 5 (67)
    -→Tangle Test semantic error 6 (69)
    -→Tangle Test include error 7 (71)
    -→Tangle Test main program (74)
    +→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 (59).

    +

    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 (60) =

    +

    Tangle Test superclass to refactor common setup (61) =

     class TangleTestcase(unittest.TestCase):
    -    text = ""
    -    error = ""
    -    file_path: Path
    +    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)
    @@ -2151,6 +2196,7 @@ 

    Tests for Tangling

    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() @@ -2159,11 +2205,11 @@

    Tests for Tangling

    -

    Tangle Test superclass to refactor common setup (60). Used by: test_tangler.py (59)

    +

    Tangle Test superclass to refactor common setup (61). Used by: test_tangler.py (60)

    -

    Tangle Test semantic error 2 (61) =

    +

    Tangle Test semantic error 2 (62) =

    -→Sample Document 2 (62)
    +→Sample Document 2 (63)
     
     class Test_SemanticError_2(TangleTestcase):
         text = test2_w
    @@ -2173,9 +2219,9 @@ 

    Tests for Tangling

    -

    Tangle Test semantic error 2 (61). Used by: test_tangler.py (59)

    +

    Tangle Test semantic error 2 (62). Used by: test_tangler.py (60)

    -

    Sample Document 2 (62) =

    +

    Sample Document 2 (63) =

     test2_w = """Some anonymous chunk
     @o test2.tmp
    @@ -2188,11 +2234,11 @@ 

    Tests for Tangling

    -

    Sample Document 2 (62). Used by: Tangle Test semantic error 2... (61)

    +

    Sample Document 2 (63). Used by: Tangle Test semantic error 2... (62)

    -

    Tangle Test semantic error 3 (63) =

    +

    Tangle Test semantic error 3 (64) =

    -→Sample Document 3 (64)
    +→Sample Document 3 (65)
     
     class Test_SemanticError_3(TangleTestcase):
         text = test3_w
    @@ -2202,9 +2248,9 @@ 

    Tests for Tangling

    -

    Tangle Test semantic error 3 (63). Used by: test_tangler.py (59)

    +

    Tangle Test semantic error 3 (64). Used by: test_tangler.py (60)

    -

    Sample Document 3 (64) =

    +

    Sample Document 3 (65) =

     test3_w = """Some anonymous chunk
     @o test3.tmp
    @@ -2218,11 +2264,11 @@ 

    Tests for Tangling

    -

    Sample Document 3 (64). Used by: Tangle Test semantic error 3... (63)

    +

    Sample Document 3 (65). Used by: Tangle Test semantic error 3... (64)

    -

    Tangle Test semantic error 4 (65) =

    +

    Tangle Test semantic error 4 (66) =

    -→Sample Document 4 (66)
    +→Sample Document 4 (67)
     
     class Test_SemanticError_4(TangleTestcase):
         text = test4_w
    @@ -2232,9 +2278,9 @@ 

    Tests for Tangling

    -

    Tangle Test semantic error 4 (65). Used by: test_tangler.py (59)

    +

    Tangle Test semantic error 4 (66). Used by: test_tangler.py (60)

    -

    Sample Document 4 (66) =

    +

    Sample Document 4 (67) =

     test4_w = """Some anonymous chunk
     @o test4.tmp
    @@ -2248,11 +2294,11 @@ 

    Tests for Tangling

    -

    Sample Document 4 (66). Used by: Tangle Test semantic error 4... (65)

    +

    Sample Document 4 (67). Used by: Tangle Test semantic error 4... (66)

    -

    Tangle Test semantic error 5 (67) =

    +

    Tangle Test semantic error 5 (68) =

    -→Sample Document 5 (68)
    +→Sample Document 5 (69)
     
     class Test_SemanticError_5(TangleTestcase):
         text = test5_w
    @@ -2262,9 +2308,9 @@ 

    Tests for Tangling

    -

    Tangle Test semantic error 5 (67). Used by: test_tangler.py (59)

    +

    Tangle Test semantic error 5 (68). Used by: test_tangler.py (60)

    -

    Sample Document 5 (68) =

    +

    Sample Document 5 (69) =

     test5_w = """
     Some anonymous chunk
    @@ -2280,11 +2326,11 @@ 

    Tests for Tangling

    -

    Sample Document 5 (68). Used by: Tangle Test semantic error 5... (67)

    +

    Sample Document 5 (69). Used by: Tangle Test semantic error 5... (68)

    -

    Tangle Test semantic error 6 (69) =

    +

    Tangle Test semantic error 6 (70) =

    -→Sample Document 6 (70)
    +→Sample Document 6 (71)
     
     class Test_SemanticError_6(TangleTestcase):
         text = test6_w
    @@ -2299,9 +2345,9 @@ 

    Tests for Tangling

    -

    Tangle Test semantic error 6 (69). Used by: test_tangler.py (59)

    +

    Tangle Test semantic error 6 (70). Used by: test_tangler.py (60)

    -

    Sample Document 6 (70) =

    +

    Sample Document 6 (71) =

     test6_w = """Some anonymous chunk
     @o test6.tmp
    @@ -2317,11 +2363,11 @@ 

    Tests for Tangling

    -

    Sample Document 6 (70). Used by: Tangle Test semantic error 6... (69)

    +

    Sample Document 6 (71). Used by: Tangle Test semantic error 6... (70)

    -

    Tangle Test include error 7 (71) =

    +

    Tangle Test include error 7 (72) =

    -→Sample Document 7 and it's included file (72)
    +→Sample Document 7 and it's included file (73)
     
     class Test_IncludeError_7(TangleTestcase):
         text = test7_w
    @@ -2341,9 +2387,9 @@ 

    Tests for Tangling

    -

    Tangle Test include error 7 (71). Used by: test_tangler.py (59)

    +

    Tangle Test include error 7 (72). Used by: test_tangler.py (60)

    -

    Sample Document 7 and it's included file (72) =

    +

    Sample Document 7 and it's included file (73) =

     test7_w = """
     Some anonymous chunk.
    @@ -2358,24 +2404,25 @@ 

    Tests for Tangling

    -

    Sample Document 7 and it's included file (72). Used by: Tangle Test include error 7... (71)

    +

    Sample Document 7 and it's included file (73). Used by: Tangle Test include error 7... (72)

    -

    Tangle Test overheads: imports, etc. (73) =

    +

    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. (73). Used by: test_tangler.py (59)

    +

    Tangle Test overheads: imports, etc. (74). Used by: test_tangler.py (60)

    -

    Tangle Test main program (74) =

    +

    Tangle Test main program (75) =

     if __name__ == "__main__":
         import sys
    @@ -2384,31 +2431,32 @@ 

    Tests for Tangling

    -

    Tangle Test main program (74). Used by: test_tangler.py (59)

    +

    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 (75) =

    +

    test_weaver.py (76) =

    -→Weave Test overheads: imports, etc. (82)
    -→Weave Test superclass to refactor common setup (76)
    -→Weave Test references and definitions (77)
    -→Weave Test evaluation of expressions (80)
    -→Weave Test main program (83)
    +→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 (75).

    +

    test_weaver.py (76).

    Weaving test cases have a common setup shown in this superclass.

    -

    Weave Test superclass to refactor common setup (76) =

    +

    Weave Test superclass to refactor common setup (77) =

     class WeaveTestcase(unittest.TestCase):
    -    text = ""
    -    error = ""
    -    file_path: Path
    +    text: ClassVar[str]
    +    error: ClassVar[str]
    +    file_path: ClassVar[Path]
    +
         def setUp(self) -> None:
             self.source = io.StringIO(self.text)
             self.web = pyweb.Web()
    @@ -2422,12 +2470,12 @@ 

    Tests for Weaving

    -

    Weave Test superclass to refactor common setup (76). Used by: test_weaver.py (75)

    +

    Weave Test superclass to refactor common setup (77). Used by: test_weaver.py (76)

    -

    Weave Test references and definitions (77) =

    +

    Weave Test references and definitions (78) =

    -→Sample Document 0 (78)
    -→Expected Output 0 (79)
    +→Sample Document 0 (79)
    +→Expected Output 0 (80)
     
     class Test_RefDefWeave(WeaveTestcase):
         text = test0_w
    @@ -2446,9 +2494,9 @@ 

    Tests for Weaving

    -

    Weave Test references and definitions (77). Used by: test_weaver.py (75)

    +

    Weave Test references and definitions (78). Used by: test_weaver.py (76)

    -

    Sample Document 0 (78) =

    +

    Sample Document 0 (79) =

     test0_w = """<html>
     <head>
    @@ -2474,9 +2522,9 @@ 

    Tests for Weaving

    -

    Sample Document 0 (78). Used by: Weave Test references... (77)

    +

    Sample Document 0 (79). Used by: Weave Test references... (78)

    -

    Expected Output 0 (79) =

    +

    Expected Output 0 (80) =

     test0_expected = """<html>
     <head>
    @@ -2511,17 +2559,22 @@ 

    Tests for Weaving

    -

    Expected Output 0 (79). Used by: Weave Test references... (77)

    +

    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 (80) =

    +

    Weave Test evaluation of expressions (81) =

    -→Sample Document 9 (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( )
    @@ -2530,16 +2583,16 @@ 

    Tests for Weaving

    actual = self.file_path.with_suffix(".html").read_text().splitlines() #print(actual) self.assertEqual("An anonymous chunk.", actual[0]) - self.assertTrue(actual[1].startswith("Time =")) + 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 (80). Used by: test_weaver.py (75)

    +

    Weave Test evaluation of expressions (81). Used by: test_weaver.py (76)

    -

    Sample Document 9 (81) =

    +

    Sample Document 9 (82) =

     test9_w= """An anonymous chunk.
     Time = @(time.asctime()@)
    @@ -2550,9 +2603,9 @@ 

    Tests for Weaving

    -

    Sample Document 9 (81). Used by: Weave Test evaluation... (80)

    +

    Sample Document 9 (82). Used by: Weave Test evaluation... (81)

    -

    Weave Test overheads: imports, etc. (82) =

    +

    Weave Test overheads: imports, etc. (83) =

     """Weaver tests exercise various weaving features."""
     import io
    @@ -2560,141 +2613,276 @@ 

    Tests for Weaving

    import os from pathlib import Path import string +import sys +from typing import ClassVar import unittest import pyweb
    -

    Weave Test overheads: imports, etc. (82). Used by: test_weaver.py (75)

    +

    Weave Test overheads: imports, etc. (83). Used by: test_weaver.py (76)

    -

    Weave Test main program (83) =

    +

    Weave Test main program (84) =

     if __name__ == "__main__":
    -    import sys
         logging.basicConfig(stream=sys.stderr, level=logging.WARN)
         unittest.main()
     
    -

    Weave Test main program (83). Used by: test_weaver.py (75)

    +

    Weave Test main program (84). Used by: test_weaver.py (76)

    -
    -

    Combined Test Runner

    - -

    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.

    -

    runner.py (84) =

    +
    +

    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) =

    -→Combined Test overheads, imports, etc. (85)
    -→Combined Test suite which imports all other test modules (86)
    -→Combined Test command line options (87)
    -→Combined Test main script (88)
    +→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)
     
    -

    runner.py (84).

    +

    test_scripts.py (85).

    -

    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. (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) =

    -"""Combined tests."""
    -import argparse
    -import unittest
    -import test_loader
    -import test_tangler
    -import test_weaver
    -import test_unit
    -import logging
    -import sys
    +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()
     
    -

    Combined Test overheads, imports, etc. (85). Used by: runner.py (84)

    +

    Test of weave.py (88). Used by: test_scripts.py (85)

    -

    The test suite is built from each of the individual test modules.

    -

    Combined Test suite which imports all other test modules (86) =

    +
    +
    +

    Tangle Script Test

    +

    We check the tangle output to be sure it's what we expected.

    +

    Test of tangle.py (89) =

    -def suite():
    -    s = unittest.TestSuite()
    -    for m in (test_loader, test_tangler, test_weaver, test_unit):
    -        s.addTests(unittest.defaultTestLoader.loadTestsFromModule(m))
    -    return s
    +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()
     
    -

    Combined Test suite which imports all other test modules (86). Used by: runner.py (84)

    +

    Test of tangle.py (89). Used by: test_scripts.py (85)

    -

    In order to debug failing tests, we accept some command-line -parameters to the combined testing script.

    -

    Combined Test command line options (87) =

    +
    +
    +

    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) =

    -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
    -
    - -
    -

    Combined Test command line options (87). Used by: runner.py (84)

    -
    -

    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.

    -

    Combined Test main script (88) =

    +"""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__":
    -    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))
    +    logging.basicConfig(stream=sys.stdout, level=logging.WARN)
    +    unittest.main()
     
    -

    Combined Test main script (88). Used by: runner.py (84)

    +

    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.

    +

    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 (89) =

    +

    docutils.conf (92) =

     # docutils.conf
     
    @@ -2705,12 +2893,12 @@ 

    Additional Files

    -

    docutils.conf (89).

    +

    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 (90) =

    +

    page-layout.css (93) =

     /* Page layout tweaks */
     div.document { width: 7in; }
    @@ -2732,130 +2920,138 @@ 

    Additional Files

    -

    page-layout.css (90).

    +

    page-layout.css (93).

    -

    Indices

    +

    Indices

    -

    Files

    +

    Files

    - + - + - + - + + - + - +
    docutils.conf:→(89)
    docutils.conf:→(92)
    page-layout.css:
     →(90)
     →(93)
    runner.py:→(84)
    test_loader.py:→(51)
    test_loader.py:→(50)
    test_scripts.py:
     →(85)
    test_tangler.py:
     →(59)
     →(60)
    test_unit.py:→(1)
    test_weaver.py:→(75)
    test_weaver.py:→(76)
    -

    Macros

    +

    Macros

    - - - - - - - - - - - - - + - + - + - + +→(53) →(58) - + - + - + - + - + - + - + - + - + - + - + + + + + + + + + + + + + - + - + +→(74) - + - + - + - + - + - + + + + + + + @@ -2875,7 +3071,7 @@

    Macros

    - + @@ -2972,7 +3168,7 @@

    Macros

    +→(48) →(49) @@ -2987,31 +3183,31 @@

    Macros

    - + - + +→(83) - + - +
    Combined Test command line options:
     →(87)
    Combined Test main script:
     →(88)
    Combined Test overheads, imports, etc.:
     →(85)
    Combined Test suite which imports all other test modules:
     →(86)
    Expected Output 0:
     →(79)
     →(80)
    Load Test error handling with a few common syntax errors:
     →(53)
     →(54)
    Load Test include processing with syntax errors:
     →(55)
     →(56)
    Load Test main program:
     →(58)
     →(59)
    Load Test overheads:
     imports, etc.: -→(52) →(57)
    Load Test superclass to refactor common setup:
     →(51)
     →(52)
    Sample Document 0:
     →(78)
     →(79)
    Sample Document 1 with correct and incorrect syntax:
     →(54)
     →(55)
    Sample Document 2:
     →(62)
     →(63)
    Sample Document 3:
     →(64)
     →(65)
    Sample Document 4:
     →(66)
     →(67)
    Sample Document 5:
     →(68)
     →(69)
    Sample Document 6:
     →(70)
     →(71)
    Sample Document 7 and it's included file:
     →(72)
     →(73)
    Sample Document 8 and the file it includes:
     →(56)
     →(57)
    Sample Document 9:
     →(81)
     →(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:
     →(71)
     →(72)
    Tangle Test main program:
     →(74)
     →(75)
    Tangle Test overheads:
     imports, etc.: -→(73)
    Tangle Test semantic error 2:
     →(61)
     →(62)
    Tangle Test semantic error 3:
     →(63)
     →(64)
    Tangle Test semantic error 4:
     →(65)
     →(66)
    Tangle Test semantic error 5:
     →(67)
     →(68)
    Tangle Test semantic error 6:
     →(69)
     →(70)
    Tangle Test superclass to refactor common setup:
     →(60)
     →(61)
    Test of tangle.py:
     →(89)
    Test of weave.py:
     →(88)
    Unit Test Mock Chunk class:
     →(4)
    Unit Test Web class weave:
     →(38)
    Unit Test main:→(49)
    Unit Test main:→(50)
    Unit Test of Action class hierarchy:
     →(42)
    Unit Test overheads:
     imports, etc.: -→(48)
    Unit test of Action Sequence class:
     →(43)
     →(44)
    Weave Test evaluation of expressions:
     →(80)
     →(81)
    Weave Test main program:
     →(83)
     →(84)
    Weave Test overheads:
     imports, etc.: -→(82)
    Weave Test references and definitions:
     →(77)
     →(78)
    Weave Test superclass to refactor common setup:
     →(76)
     →(77)
    -

    User Identifiers

    +

    User Identifiers

    (None)


    -Created by pyweb.py at Sat Jun 11 08:26:49 2022.
    -

    Source test/pyweb_test.w modified Fri Jun 10 17:07:24 2022.

    +Created by pyweb.py at Sun Jun 12 19:07:28 2022.
    +

    Source tests/pyweb_test.w modified Sat Jun 11 08:30:06 2022.

    pyweb.__version__ '3.1'.

    Working directory '/Users/slott/Documents/Projects/py-web-tool'.

    diff --git a/test/pyweb_test.rst b/tests/pyweb_test.rst similarity index 74% rename from test/pyweb_test.rst rename to tests/pyweb_test.rst index 4187655..e9f61c6 100644 --- a/test/pyweb_test.rst +++ b/tests/pyweb_test.rst @@ -179,7 +179,7 @@ This gives us the following outline for unit testing. .. parsed-literal:: :class: code - |srarr|\ Unit Test overheads: imports, etc. (`48`_) + |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`_) @@ -188,7 +188,7 @@ This gives us the following outline for unit testing. |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 (`49`_) + |srarr|\ Unit Test main (`50`_) .. @@ -285,7 +285,19 @@ emitter is Tangler-like. |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. +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`: @@ -294,25 +306,30 @@ A Mock Chunk is a Chunk-like object that we can use to test Weavers. :class: code - class MockChunk: - def \_\_init\_\_(self, name: str, seq: int, lineNumber: int) -> None: - self.name = name - self.fullName = name - self.seq = seq - self.lineNumber = lineNumber - self.initial = True - self.commands = [] - self.referencedBy = [] - def \_\_repr\_\_(self) -> str: - return f"({self.name!r}, {self.seq!r})" - def references(self, aWeaver: pyweb.Weaver) -> list[str]: - return [(c.name, c.seq) for c in self.referencedBy] - def reference\_indent(self, aWeb: "Web", aTangler: "Tangler", amount: int) -> None: - aTangler.addIndent(amount) - def reference\_dedent(self, aWeb: "Web", aTangler: "Tangler") -> None: - aTangler.clrIndent() - def tangle(self, aWeb: "Web", aTangler: "Tangler") -> None: - aTangler.write(self.name) + 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 + ) .. @@ -336,9 +353,11 @@ The default Weaver is an Emitter that uses templates to produce RST markup. self.weaver.reference\_style = pyweb.SimpleReference() self.filepath = Path("testweaver") self.aFileChunk = MockChunk("File", 123, 456) - self.aFileChunk.referencedBy = [ ] + 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() @@ -352,6 +371,8 @@ The default Weaver is an Emitter that uses templates to produce RST markup. 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) @@ -424,6 +445,8 @@ We'll examine a few features of the LaTeX templates. 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() @@ -434,9 +457,23 @@ We'll examine a few features of the LaTeX templates. result = self.weaver.quote("\\\\end{Verbatim}") self.assertEqual("\\\\end\\\\,{Verbatim}", result) result = self.weaver.references(self.aChunk) - self.assertEqual("\\n \\\\footnotesize\\n Used by:\\n \\\\begin{list}{}{}\\n \\n \\\\item Code example File (123) (Sect. \\\\ref{pyweb123}, p. \\\\pageref{pyweb123})\\n\\n \\\\end{list}\\n \\\\normalsize\\n", result) + 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)]) .. @@ -463,12 +500,13 @@ We'll examine a few features of the HTML templates. 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") @@ -477,6 +515,8 @@ We'll examine a few features of the HTML templates. 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)]) .. @@ -486,9 +526,9 @@ We'll examine a few features of the HTML templates. |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 just a template change. +The unique feature of the ``HTMLShort`` class is a template change. - **To Do** Test ``HTMLShort``. + **TODO:** Test ``HTMLShort``. .. _`8`: @@ -555,9 +595,6 @@ 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 either -need to wait for a full second to elapse or we need to mock the various -``os`` and ``filecmp`` features used by ``TanglerMake``. @@ -573,7 +610,7 @@ need to wait for a full second to elapse or we need to mock the various self.tangler = pyweb.TanglerMake() self.filepath = Path("testtangler.code") self.aChunk = MockChunk("Chunk", 314, 278) - #self.aChunk.references\_list = [ ("Container", 123) ] + #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")) @@ -581,7 +618,6 @@ need to wait for a full second to elapse or we need to mock the various self.tangler.close() self.time\_original = self.filepath.stat().st\_mtime self.original = self.filepath.stat() - #time.sleep(0.75) # Alternative to assure timestamps must be different def tearDown(self) -> None: try: @@ -644,7 +680,7 @@ of chunks that are used to produce the documentation and the source files. 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. +mock objects for all of these relationships in which a Chunk participates. A MockCommand can be attached to a Chunk. @@ -655,11 +691,15 @@ A MockCommand can be attached to a Chunk. :class: code - class MockCommand: - def \_\_init\_\_(self) -> None: - self.lineNumber = 314 - def startswith(self, text: str) -> bool: - return False + MockCommand = Mock( + name="Command class", + side\_effect=lambda: Mock( + name="Command instance", + # text="", # Only used for TextCommand. + lineNumber=314, + startswith=Mock(return\_value=False) + ) + ) .. @@ -677,35 +717,30 @@ A MockWeb can contain a Chunk. :class: code - class MockWeb: - def \_\_init\_\_(self) -> None: - self.chunks = [] - self.wove = None - self.tangled = None - def add(self, aChunk: pyweb.Chunk) -> None: - self.chunks.append(aChunk) - def addNamed(self, aChunk: pyweb.Chunk) -> None: - self.chunks.append(aChunk) - def addOutput(self, aChunk: pyweb.Chunk) -> None: - self.chunks.append(aChunk) - def fullNameFor(self, name: str) -> str: - return name - def fileXref(self) -> dict[str, list[int]]: - return {'file': [1,2,3]} - def chunkXref(self) -> dict[str, list[int]]: - return {'chunk': [4,5,6]} - def userNamesXref(self) -> dict[str, list[int]]: - return {'name': (7, [8,9,10])} - def getchunk(self, name: str) -> list[pyweb.Chunk]: - return [MockChunk(name, 1, 314)] - def createUsedBy(self) -> None: - pass - def weaveChunk(self, name, weaver) -> None: - weaver.write(name) - def weave(self, weaver) -> None: - self.wove = weaver - def tangle(self, tangler) -> None: - self.tangled = tangler + + 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 + ) .. @@ -714,7 +749,14 @@ A MockWeb can contain a Chunk. |loz| *Unit Test of Chunk superclass (13)*. Used by: Unit Test of Chunk class hierarchy... (`11`_) -A MockWeaver or MockTangle can process a Chunk. +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`: @@ -723,63 +765,41 @@ A MockWeaver or MockTangle can process a Chunk. :class: code - class MockWeaver: - def \_\_init\_\_(self) -> None: - self.begin\_chunk = [] - self.end\_chunk = [] - self.written = [] - self.code\_indent = None - def quote(self, text: str) -> str: - return text.replace("&", "&") # token quoting - def docBegin(self, aChunk: pyweb.Chunk) -> None: - self.begin\_chunk.append(aChunk) - def write(self, text: str) -> None: - self.written.append(text) - def docEnd(self, aChunk: pyweb.Chunk) -> None: - self.end\_chunk.append(aChunk) - def codeBegin(self, aChunk: pyweb.Chunk) -> None: - self.begin\_chunk.append(aChunk) - def codeBlock(self, text: str) -> None: - self.written.append(text) - def codeEnd(self, aChunk: pyweb.Chunk) -> None: - self.end\_chunk.append(aChunk) - def fileBegin(self, aChunk: pyweb.Chunk) -> None: - self.begin\_chunk.append(aChunk) - def fileEnd(self, aChunk: pyweb.Chunk) -> None: - self.end\_chunk.append(aChunk) - def addIndent(self, increment=0): - pass - def setIndent(self, fixed: int \| None=None, command: str \| None=None) -> None: - self.indent = fixed - def addIndent(self, increment: int = 0) -> None: - self.indent = increment - def clrIndent(self) -> None: - pass - def xrefHead(self) -> None: - pass - def xrefLine(self, name: str, refList: list[int]) -> None: - self.written.append(f"{name} {refList}") - def xrefDefLine(self, name: str, defn: int, refList: list[int]) -> None: - self.written.append(f"{name} {defn} {refList}") - def xrefFoot(self) -> None: - pass - def referenceTo(self, name: str, seq: int) -> None: - pass - def open(self, aFile: str) -> "MockWeaver": - return self - def close(self) -> None: - pass - def \_\_enter\_\_(self) -> "MockWeaver": - return self - def \_\_exit\_\_(self, \*args: Any) -> bool: - return False - - class MockTangler(MockWeaver): - def \_\_init\_\_(self) -> None: - super().\_\_init\_\_() - self.context = [0] - def addIndent(self, amount: int) -> None: - pass + 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 + ) + .. @@ -800,8 +820,11 @@ A Chunk is built, interrogated and then emitted. 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`_) .. @@ -823,28 +846,32 @@ Can we build a Chunk? def test\_append\_command\_should\_work(self) -> None: cmd1 = MockCommand() self.theChunk.append(cmd1) - self.assertEqual(1, len(self.theChunk.commands) ) + 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(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.assertEqual(1, len(self.theChunk.commands)) self.theChunk.appendText("&more text") - self.assertEqual(1, len(self.theChunk.commands) ) + 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) ) - - def test\_append\_to\_web\_should\_work(self) -> None: + 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(1, len(web.chunks)) + self.assertEqual(web.add.mock\_calls, [call(self.theChunk)]) .. @@ -885,6 +912,7 @@ Can we interrogate a Chunk? 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") @@ -913,16 +941,14 @@ Can we emit a Chunk with a weaver or tangler? :class: code - def test\_weave\_should\_work(self) -> None: + 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(1, len(wvr.begin\_chunk)) - self.assertTrue(wvr.begin\_chunk[0] is self.theChunk) - self.assertEqual(1, len(wvr.end\_chunk)) - self.assertTrue(wvr.end\_chunk[0] is self.theChunk) - self.assertEqual("this chunk has very & many words", "".join( wvr.written)) + 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() @@ -964,30 +990,27 @@ and tangled differently than anonymous chunks. self.assertEqual("index", self.theChunk.getUserIDRefs()[0]) self.assertEqual("terms", self.theChunk.getUserIDRefs()[1]) - def test\_append\_to\_web\_should\_work(self) -> None: + def test\_append\_named\_chunk\_to\_web\_should\_work(self) -> None: web = MockWeb() self.theChunk.webAdd(web) - self.assertEqual(1, len(web.chunks)) - + 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(1, len(wvr.begin\_chunk)) - self.assertTrue(wvr.begin\_chunk[0] is self.theChunk) - self.assertEqual(1, len(wvr.end\_chunk)) - self.assertTrue(wvr.end\_chunk[0] is self.theChunk) - self.assertEqual("the words & text of this Chunk", "".join( wvr.written)) + 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(1, len(tnglr.begin\_chunk)) - self.assertTrue(tnglr.begin\_chunk[0] is self.theChunk) - self.assertEqual(1, len(tnglr.end\_chunk)) - self.assertTrue(tnglr.end\_chunk[0] is self.theChunk) - self.assertEqual("the words & text of this Chunk", "".join( tnglr.written)) + 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)]) .. @@ -1005,7 +1028,7 @@ and tangled differently than anonymous chunks. class TestNamedChunk\_Noindent(unittest.TestCase): def setUp(self) -> None: - self.theChunk = pyweb.NamedChunk\_Noindent("Some Name...") + 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") @@ -1013,11 +1036,13 @@ and tangled differently than anonymous chunks. tnglr = MockTangler() web = MockWeb() self.theChunk.tangle(web, tnglr) - self.assertEqual(1, len(tnglr.begin\_chunk)) - self.assertTrue(tnglr.begin\_chunk[0] is self.theChunk) - self.assertEqual(1, len(tnglr.end\_chunk)) - self.assertTrue(tnglr.end\_chunk[0] is self.theChunk) - self.assertEqual("the words & text of this Chunk", "".join( tnglr.written)) + + self.assertEqual(tnglr.mock\_calls, [ + call.codeBegin(self.theChunk), + call.codeBlock('the words & text of this Chunk'), + call.codeEnd(self.theChunk) + ] + ) .. @@ -1045,30 +1070,33 @@ and tangled differently than anonymous chunks. self.theChunk.append(cmd) self.theChunk.setUserIDRefs("index terms") - def test\_append\_to\_web\_should\_work(self) -> None: + def test\_append\_output\_chunk\_to\_web\_should\_work(self) -> None: web = MockWeb() self.theChunk.webAdd(web) - self.assertEqual(1, len(web.chunks)) - + 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(1, len(wvr.begin\_chunk)) - self.assertTrue(wvr.begin\_chunk[0] is self.theChunk) - self.assertEqual(1, len(wvr.end\_chunk)) - self.assertTrue(wvr.end\_chunk[0] is self.theChunk) - self.assertEqual("the words & text of this Chunk", "".join( wvr.written)) - + 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(1, len(tnglr.begin\_chunk)) - self.assertTrue(tnglr.begin\_chunk[0] is self.theChunk) - self.assertEqual(1, len(tnglr.end\_chunk)) - self.assertTrue(tnglr.end\_chunk[0] is self.theChunk) - self.assertEqual("the words & text of this Chunk", "".join( tnglr.written)) + self.assertEqual(tnglr.mock\_calls, [ + call.codeBegin(self.theChunk), + call.codeBlock('the words & text of this Chunk'), + call.codeEnd(self.theChunk) + ] + ) .. @@ -1160,16 +1188,18 @@ A TextCommand object must be constructed, interrogated and emitted. 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("Some text & words in the document\\n ", "".join( wvr.written)) + 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("Some text & words in the document\\n ", "".join( tnglr.written)) + self.assertEqual(tnglr.write.mock\_calls, [call('Some text & words in the document\\n ')]) .. @@ -1190,16 +1220,18 @@ A CodeCommand object is a TextCommand with different processing for being emitte 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("Some text & words in the document\\n ", "".join( wvr.written)) + 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("Some text & words in the document\\n ", "".join( tnglr.written)) + self.assertEqual(tnglr.codeBlock.mock\_calls, [call('Some text & words in the document\\n ')]) .. @@ -1237,11 +1269,13 @@ locations. 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("file [1, 2, 3]", "".join( wvr.written)) + 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() @@ -1271,11 +1305,13 @@ locations. 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("chunk [4, 5, 6]", "".join( wvr.written)) + 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() @@ -1305,11 +1341,13 @@ names. 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("name 7 [8, 9, 10]", "".join( wvr.written)) + 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() @@ -1344,17 +1382,20 @@ They can't be simply tangled. 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("Some Name", "".join( wvr.written)) + 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("Some Name", "".join( tnglr.written)) + self.assertEqual(tnglr.write.mock\_calls, [call('Some Name')]) + .. @@ -1540,11 +1581,13 @@ This is more difficult to create mocks for. 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)) @@ -1553,6 +1596,7 @@ This is more difficult to create mocks for. 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)) @@ -1584,8 +1628,12 @@ This is more difficult to create mocks for. def test\_valid\_web\_should\_tangle(self) -> None: tangler = MockTangler() self.web.tangle(tangler) - self.assertEqual(3, len(tangler.written)) - self.assertEqual(['some code', 'some user2a code', 'some user1 code'], tangler.written) + self.assertEqual(tangler.codeBlock.mock\_calls, [ + call('some code'), + call('some user2a code'), + call('some user1 code'), + ] + ) .. @@ -1604,9 +1652,16 @@ This is more difficult to create mocks for. def test\_valid\_web\_should\_weave(self) -> None: weaver = MockWeaver() self.web.weave(weaver) - self.assertEqual(6, len(weaver.written)) - expected = ['some text', 'some code', None, 'some user2a code', None, 'some user1 code'] - self.assertEqual(expected, weaver.written) + 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'), + ] + ) .. @@ -1740,6 +1795,8 @@ load, tangle, weave. |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) = @@ -1747,44 +1804,18 @@ load, tangle, weave. :class: code - class MockAction: - def \_\_init\_\_(self) -> None: - self.count = 0 - def \_\_call\_\_(self) -> None: - self.count += 1 - - class MockWebReader: - def \_\_init\_\_(self) -> None: - self.count = 0 - self.theWeb = None - self.errors = 0 - def web(self, aWeb: "Web") -> None: - """Deprecated""" - warnings.warn("deprecated", DeprecationWarning) - self.theWeb = aWeb - return self - def source(self, filename: str, file: TextIO) -> str: - """Deprecated""" - warnings.warn("deprecated", DeprecationWarning) - self.webFileName = filename - def load(self, aWeb: pyweb.Web, filename: str, source: TextIO \| None = None) -> None: - self.theWeb = aWeb - self.webFileName = filename - self.count += 1 - class TestActionSequence(unittest.TestCase): def setUp(self) -> None: self.web = MockWeb() - self.a1 = MockAction() - self.a2 = MockAction() + 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() - for c in self.action.opSequence: - self.assertEqual(1, c.count) - self.assertTrue(self.web is c.web) + self.assertEqual(self.a1.call\_count, 1) + self.assertEqual(self.a2.call\_count, 1) .. @@ -1813,7 +1844,7 @@ load, tangle, weave. ) def test\_should\_execute\_weaving(self) -> None: self.action() - self.assertTrue(self.web.wove is self.weaver) + self.assertEqual(self.web.weave.mock\_calls, [call(self.weaver)]) .. @@ -1842,7 +1873,7 @@ load, tangle, weave. ) def test\_should\_execute\_tangling(self) -> None: self.action() - self.assertTrue(self.web.tangled is self.tangler) + self.assertEqual(self.web.tangle.mock\_calls, [call(self.tangler)]) .. @@ -1851,6 +1882,8 @@ load, tangle, weave. |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) = @@ -1862,11 +1895,15 @@ load, tangle, weave. def setUp(self) -> None: self.web = MockWeb() self.action = pyweb.LoadAction() - self.webReader = MockWebReader() + 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=Path("TestLoadAction.w"), + source\_path=self.source\_path, command="@", permitList = [], output=Path.cwd(), @@ -1879,7 +1916,11 @@ load, tangle, weave. pass def test\_should\_execute\_loading(self) -> None: self.action() - self.assertEqual(1, self.webReader.count) + # 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 .. @@ -1894,6 +1935,8 @@ 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) = @@ -1927,9 +1970,12 @@ The boilerplate code for unit testing is the following. 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 @@ -1941,15 +1987,34 @@ The boilerplate code for unit testing is the following. |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 main (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\_\_": - import sys logging.basicConfig(stream=sys.stdout, level=logging.WARN) unittest.main() @@ -1957,7 +2022,7 @@ The boilerplate code for unit testing is the following. .. class:: small - |loz| *Unit Test main (49)*. Used by: test_unit.py (`1`_) + |loz| *Unit Test main (50)*. Used by: test_unit.py (`1`_) We run the default ``unittest.main()`` to execute the entire suite of tests. @@ -1984,22 +2049,26 @@ Tests for Loading We need to be able to load a web from one or more source files. -.. _`50`: -.. rubric:: test_loader.py (50) = +.. _`51`: +.. rubric:: test_loader.py (51) = .. parsed-literal:: :class: code - |srarr|\ Load Test overheads: imports, etc. (`52`_), |srarr|\ (`57`_) - |srarr|\ Load Test superclass to refactor common setup (`51`_) - |srarr|\ Load Test error handling with a few common syntax errors (`53`_) - |srarr|\ Load Test include processing with syntax errors (`55`_) - |srarr|\ Load Test main program (`58`_) + |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 (50)*. + |loz| *test_loader.py (51)*. Parsing test cases have a common setup shown in this superclass. @@ -2009,15 +2078,16 @@ By using some class-level variables ``text``, input object to the ``WebReader`` instance. -.. _`51`: -.. rubric:: Load Test superclass to refactor common setup (51) = +.. _`52`: +.. rubric:: Load Test superclass to refactor common setup (52) = .. parsed-literal:: :class: code class ParseTestcase(unittest.TestCase): - text = "" - file\_path: Path + text: ClassVar[str] + file\_path: ClassVar[Path] + def setUp(self) -> None: self.source = io.StringIO(self.text) self.web = pyweb.Web() @@ -2027,7 +2097,7 @@ input object to the ``WebReader`` instance. .. class:: small - |loz| *Load Test superclass to refactor common setup (51)*. Used by: test_loader.py (`50`_) + |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. @@ -2035,67 +2105,57 @@ We'll cover most of the cases with a quick check for a failure to find an expected next token. -.. _`52`: -.. rubric:: Load Test overheads: imports, etc. (52) = +.. _`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. (52)*. Used by: test_loader.py (`50`_) + |loz| *Load Test overheads: imports, etc. (53)*. Used by: test_loader.py (`51`_) -.. _`53`: -.. rubric:: Load Test error handling with a few common syntax errors (53) = +.. _`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 (`54`_) + |srarr|\ Sample Document 1 with correct and incorrect syntax (`55`_) class Test\_ParseErrors(ParseTestcase): text = test1\_w file\_path = Path("test1.w") - def setUp(self) -> None: - super().setUp() - self.logger = logging.getLogger("WebReader") - self.buffer = logging.handlers.BufferingHandler(12) - self.buffer.setLevel(logging.WARN) - self.logger.addHandler(self.buffer) - self.logger.setLevel(logging.WARN) def test\_error\_should\_count\_1(self) -> None: - self.rdr.load(self.web, self.file\_path, self.source) + 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) - messages = [r.message for r in self.buffer.buffer] - self.assertEqual( - ["At ('test1.w', 8): expected ('@{',), found '@o'", - "Extra '@{' (possibly missing chunk name) near ('test1.w', 9)", - "Extra '@{' (possibly missing chunk name) near ('test1.w', 9)"], - messages + 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)" + ] ) - def tearDown(self) -> None: - self.logger.setLevel(logging.CRITICAL) - self.logger.removeHandler(self.buffer) - super().tearDown() - .. .. class:: small - |loz| *Load Test error handling with a few common syntax errors (53)*. Used by: test_loader.py (`50`_) + |loz| *Load Test error handling with a few common syntax errors (54)*. Used by: test_loader.py (`51`_) -.. _`54`: -.. rubric:: Sample Document 1 with correct and incorrect syntax (54) = +.. _`55`: +.. rubric:: Sample Document 1 with correct and incorrect syntax (55) = .. parsed-literal:: :class: code @@ -2115,7 +2175,7 @@ find an expected next token. .. class:: small - |loz| *Sample Document 1 with correct and incorrect syntax (54)*. Used by: Load Test error handling... (`53`_) + |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 @@ -2124,16 +2184,17 @@ 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. +create a temporary file. It's hard to mock the include processing, +since it's a nested instance of the tokenizer. -.. _`55`: -.. rubric:: Load Test include processing with syntax errors (55) = +.. _`56`: +.. rubric:: Load Test include processing with syntax errors (56) = .. parsed-literal:: :class: code - |srarr|\ Sample Document 8 and the file it includes (`56`_) + |srarr|\ Sample Document 8 and the file it includes (`57`_) class Test\_IncludeParseErrors(ParseTestcase): text = test8\_w @@ -2141,39 +2202,33 @@ create a temporary file. It's hard to mock the include processing. def setUp(self) -> None: super().setUp() Path('test8\_inc.tmp').write\_text(test8\_inc\_w) - self.logger = logging.getLogger("WebReader") - self.buffer = logging.handlers.BufferingHandler(12) - self.buffer.setLevel(logging.WARN) - self.logger.addHandler(self.buffer) - self.logger.setLevel(logging.WARN) def test\_error\_should\_count\_2(self) -> None: - self.rdr.load(self.web, self.file\_path, self.source) + 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) - messages = [r.message for r in self.buffer.buffer] - self.assertEqual( - ["At ('test8\_inc.tmp', 4): end of input, ('@{', '@[') not found", - "Errors in included file 'test8\_inc.tmp', output is incomplete."], - messages - ) + 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: - self.logger.setLevel(logging.CRITICAL) - self.logger.removeHandler(self.buffer) - Path('test8\_inc.tmp').unlink() super().tearDown() + Path('test8\_inc.tmp').unlink() .. .. class:: small - |loz| *Load Test include processing with syntax errors (55)*. Used by: test_loader.py (`50`_) + |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``. -.. _`56`: -.. rubric:: Sample Document 8 and the file it includes (56) = +.. _`57`: +.. rubric:: Sample Document 8 and the file it includes (57) = .. parsed-literal:: :class: code @@ -2194,14 +2249,14 @@ be given to the included document by ``setUp``. .. class:: small - |loz| *Sample Document 8 and the file it includes (56)*. Used by: Load Test include... (`55`_) + |loz| *Sample Document 8 and the file it includes (57)*. Used by: Load Test include... (`56`_)

    The overheads for a Python unittest.

    -.. _`57`: -.. rubric:: Load Test overheads: imports, etc. (57) += +.. _`58`: +.. rubric:: Load Test overheads: imports, etc. (58) += .. parsed-literal:: :class: code @@ -2212,6 +2267,7 @@ be given to the included document by ``setUp``. import os from pathlib import Path import string + import sys import types import unittest @@ -2221,20 +2277,19 @@ be given to the included document by ``setUp``. .. class:: small - |loz| *Load Test overheads: imports, etc. (57)*. Used by: test_loader.py (`50`_) + |loz| *Load Test overheads: imports, etc. (58)*. Used by: test_loader.py (`51`_) A main program that configures logging and then runs the test. -.. _`58`: -.. rubric:: Load Test main program (58) = +.. _`59`: +.. rubric:: Load Test main program (59) = .. parsed-literal:: :class: code if \_\_name\_\_ == "\_\_main\_\_": - import sys logging.basicConfig(stream=sys.stdout, level=logging.WARN) unittest.main() @@ -2242,7 +2297,7 @@ A main program that configures logging and then runs the test. .. class:: small - |loz| *Load Test main program (58)*. Used by: test_loader.py (`50`_) + |loz| *Load Test main program (59)*. Used by: test_loader.py (`51`_) Tests for Tangling @@ -2251,26 +2306,26 @@ Tests for Tangling We need to be able to tangle a web. -.. _`59`: -.. rubric:: test_tangler.py (59) = +.. _`60`: +.. rubric:: test_tangler.py (60) = .. parsed-literal:: :class: code - |srarr|\ Tangle Test overheads: imports, etc. (`73`_) - |srarr|\ Tangle Test superclass to refactor common setup (`60`_) - |srarr|\ Tangle Test semantic error 2 (`61`_) - |srarr|\ Tangle Test semantic error 3 (`63`_) - |srarr|\ Tangle Test semantic error 4 (`65`_) - |srarr|\ Tangle Test semantic error 5 (`67`_) - |srarr|\ Tangle Test semantic error 6 (`69`_) - |srarr|\ Tangle Test include error 7 (`71`_) - |srarr|\ Tangle Test main program (`74`_) + |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 (59)*. + |loz| *test_tangler.py (60)*. Tangling test cases have a common setup and teardown shown in this superclass. @@ -2280,21 +2335,23 @@ exceptions raised. -.. _`60`: -.. rubric:: Tangle Test superclass to refactor common setup (60) = +.. _`61`: +.. rubric:: Tangle Test superclass to refactor common setup (61) = .. parsed-literal:: :class: code class TangleTestcase(unittest.TestCase): - text = "" - error = "" - file\_path: Path + 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) @@ -2303,6 +2360,7 @@ exceptions raised. 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() @@ -2313,17 +2371,17 @@ exceptions raised. .. class:: small - |loz| *Tangle Test superclass to refactor common setup (60)*. Used by: test_tangler.py (`59`_) + |loz| *Tangle Test superclass to refactor common setup (61)*. Used by: test_tangler.py (`60`_) -.. _`61`: -.. rubric:: Tangle Test semantic error 2 (61) = +.. _`62`: +.. rubric:: Tangle Test semantic error 2 (62) = .. parsed-literal:: :class: code - |srarr|\ Sample Document 2 (`62`_) + |srarr|\ Sample Document 2 (`63`_) class Test\_SemanticError\_2(TangleTestcase): text = test2\_w @@ -2335,12 +2393,12 @@ exceptions raised. .. class:: small - |loz| *Tangle Test semantic error 2 (61)*. Used by: test_tangler.py (`59`_) + |loz| *Tangle Test semantic error 2 (62)*. Used by: test_tangler.py (`60`_) -.. _`62`: -.. rubric:: Sample Document 2 (62) = +.. _`63`: +.. rubric:: Sample Document 2 (63) = .. parsed-literal:: :class: code @@ -2358,17 +2416,17 @@ exceptions raised. .. class:: small - |loz| *Sample Document 2 (62)*. Used by: Tangle Test semantic error 2... (`61`_) + |loz| *Sample Document 2 (63)*. Used by: Tangle Test semantic error 2... (`62`_) -.. _`63`: -.. rubric:: Tangle Test semantic error 3 (63) = +.. _`64`: +.. rubric:: Tangle Test semantic error 3 (64) = .. parsed-literal:: :class: code - |srarr|\ Sample Document 3 (`64`_) + |srarr|\ Sample Document 3 (`65`_) class Test\_SemanticError\_3(TangleTestcase): text = test3\_w @@ -2380,12 +2438,12 @@ exceptions raised. .. class:: small - |loz| *Tangle Test semantic error 3 (63)*. Used by: test_tangler.py (`59`_) + |loz| *Tangle Test semantic error 3 (64)*. Used by: test_tangler.py (`60`_) -.. _`64`: -.. rubric:: Sample Document 3 (64) = +.. _`65`: +.. rubric:: Sample Document 3 (65) = .. parsed-literal:: :class: code @@ -2404,18 +2462,18 @@ exceptions raised. .. class:: small - |loz| *Sample Document 3 (64)*. Used by: Tangle Test semantic error 3... (`63`_) + |loz| *Sample Document 3 (65)*. Used by: Tangle Test semantic error 3... (`64`_) -.. _`65`: -.. rubric:: Tangle Test semantic error 4 (65) = +.. _`66`: +.. rubric:: Tangle Test semantic error 4 (66) = .. parsed-literal:: :class: code - |srarr|\ Sample Document 4 (`66`_) + |srarr|\ Sample Document 4 (`67`_) class Test\_SemanticError\_4(TangleTestcase): text = test4\_w @@ -2427,12 +2485,12 @@ exceptions raised. .. class:: small - |loz| *Tangle Test semantic error 4 (65)*. Used by: test_tangler.py (`59`_) + |loz| *Tangle Test semantic error 4 (66)*. Used by: test_tangler.py (`60`_) -.. _`66`: -.. rubric:: Sample Document 4 (66) = +.. _`67`: +.. rubric:: Sample Document 4 (67) = .. parsed-literal:: :class: code @@ -2451,17 +2509,17 @@ exceptions raised. .. class:: small - |loz| *Sample Document 4 (66)*. Used by: Tangle Test semantic error 4... (`65`_) + |loz| *Sample Document 4 (67)*. Used by: Tangle Test semantic error 4... (`66`_) -.. _`67`: -.. rubric:: Tangle Test semantic error 5 (67) = +.. _`68`: +.. rubric:: Tangle Test semantic error 5 (68) = .. parsed-literal:: :class: code - |srarr|\ Sample Document 5 (`68`_) + |srarr|\ Sample Document 5 (`69`_) class Test\_SemanticError\_5(TangleTestcase): text = test5\_w @@ -2473,12 +2531,12 @@ exceptions raised. .. class:: small - |loz| *Tangle Test semantic error 5 (67)*. Used by: test_tangler.py (`59`_) + |loz| *Tangle Test semantic error 5 (68)*. Used by: test_tangler.py (`60`_) -.. _`68`: -.. rubric:: Sample Document 5 (68) = +.. _`69`: +.. rubric:: Sample Document 5 (69) = .. parsed-literal:: :class: code @@ -2499,17 +2557,17 @@ exceptions raised. .. class:: small - |loz| *Sample Document 5 (68)*. Used by: Tangle Test semantic error 5... (`67`_) + |loz| *Sample Document 5 (69)*. Used by: Tangle Test semantic error 5... (`68`_) -.. _`69`: -.. rubric:: Tangle Test semantic error 6 (69) = +.. _`70`: +.. rubric:: Tangle Test semantic error 6 (70) = .. parsed-literal:: :class: code - |srarr|\ Sample Document 6 (`70`_) + |srarr|\ Sample Document 6 (`71`_) class Test\_SemanticError\_6(TangleTestcase): text = test6\_w @@ -2526,12 +2584,12 @@ exceptions raised. .. class:: small - |loz| *Tangle Test semantic error 6 (69)*. Used by: test_tangler.py (`59`_) + |loz| *Tangle Test semantic error 6 (70)*. Used by: test_tangler.py (`60`_) -.. _`70`: -.. rubric:: Sample Document 6 (70) = +.. _`71`: +.. rubric:: Sample Document 6 (71) = .. parsed-literal:: :class: code @@ -2552,17 +2610,17 @@ exceptions raised. .. class:: small - |loz| *Sample Document 6 (70)*. Used by: Tangle Test semantic error 6... (`69`_) + |loz| *Sample Document 6 (71)*. Used by: Tangle Test semantic error 6... (`70`_) -.. _`71`: -.. rubric:: Tangle Test include error 7 (71) = +.. _`72`: +.. rubric:: Tangle Test include error 7 (72) = .. parsed-literal:: :class: code - |srarr|\ Sample Document 7 and it's included file (`72`_) + |srarr|\ Sample Document 7 and it's included file (`73`_) class Test\_IncludeError\_7(TangleTestcase): text = test7\_w @@ -2584,12 +2642,12 @@ exceptions raised. .. class:: small - |loz| *Tangle Test include error 7 (71)*. Used by: test_tangler.py (`59`_) + |loz| *Tangle Test include error 7 (72)*. Used by: test_tangler.py (`60`_) -.. _`72`: -.. rubric:: Sample Document 7 and it's included file (72) = +.. _`73`: +.. rubric:: Sample Document 7 and it's included file (73) = .. parsed-literal:: :class: code @@ -2609,12 +2667,12 @@ exceptions raised. .. class:: small - |loz| *Sample Document 7 and it's included file (72)*. Used by: Tangle Test include error 7... (`71`_) + |loz| *Sample Document 7 and it's included file (73)*. Used by: Tangle Test include error 7... (`72`_) -.. _`73`: -.. rubric:: Tangle Test overheads: imports, etc. (73) = +.. _`74`: +.. rubric:: Tangle Test overheads: imports, etc. (74) = .. parsed-literal:: :class: code @@ -2624,6 +2682,7 @@ exceptions raised. import logging import os from pathlib import Path + from typing import ClassVar import unittest import pyweb @@ -2632,12 +2691,12 @@ exceptions raised. .. class:: small - |loz| *Tangle Test overheads: imports, etc. (73)*. Used by: test_tangler.py (`59`_) + |loz| *Tangle Test overheads: imports, etc. (74)*. Used by: test_tangler.py (`60`_) -.. _`74`: -.. rubric:: Tangle Test main program (74) = +.. _`75`: +.. rubric:: Tangle Test main program (75) = .. parsed-literal:: :class: code @@ -2651,7 +2710,7 @@ exceptions raised. .. class:: small - |loz| *Tangle Test main program (74)*. Used by: test_tangler.py (`59`_) + |loz| *Tangle Test main program (75)*. Used by: test_tangler.py (`60`_) @@ -2661,37 +2720,38 @@ Tests for Weaving We need to be able to weave a document from one or more source files. -.. _`75`: -.. rubric:: test_weaver.py (75) = +.. _`76`: +.. rubric:: test_weaver.py (76) = .. parsed-literal:: :class: code - |srarr|\ Weave Test overheads: imports, etc. (`82`_) - |srarr|\ Weave Test superclass to refactor common setup (`76`_) - |srarr|\ Weave Test references and definitions (`77`_) - |srarr|\ Weave Test evaluation of expressions (`80`_) - |srarr|\ Weave Test main program (`83`_) + |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 (75)*. + |loz| *test_weaver.py (76)*. Weaving test cases have a common setup shown in this superclass. -.. _`76`: -.. rubric:: Weave Test superclass to refactor common setup (76) = +.. _`77`: +.. rubric:: Weave Test superclass to refactor common setup (77) = .. parsed-literal:: :class: code class WeaveTestcase(unittest.TestCase): - text = "" - error = "" - file\_path: Path + text: ClassVar[str] + error: ClassVar[str] + file\_path: ClassVar[Path] + def setUp(self) -> None: self.source = io.StringIO(self.text) self.web = pyweb.Web() @@ -2707,18 +2767,18 @@ Weaving test cases have a common setup shown in this superclass. .. class:: small - |loz| *Weave Test superclass to refactor common setup (76)*. Used by: test_weaver.py (`75`_) + |loz| *Weave Test superclass to refactor common setup (77)*. Used by: test_weaver.py (`76`_) -.. _`77`: -.. rubric:: Weave Test references and definitions (77) = +.. _`78`: +.. rubric:: Weave Test references and definitions (78) = .. parsed-literal:: :class: code - |srarr|\ Sample Document 0 (`78`_) - |srarr|\ Expected Output 0 (`79`_) + |srarr|\ Sample Document 0 (`79`_) + |srarr|\ Expected Output 0 (`80`_) class Test\_RefDefWeave(WeaveTestcase): text = test0\_w @@ -2740,12 +2800,12 @@ Weaving test cases have a common setup shown in this superclass. .. class:: small - |loz| *Weave Test references and definitions (77)*. Used by: test_weaver.py (`75`_) + |loz| *Weave Test references and definitions (78)*. Used by: test_weaver.py (`76`_) -.. _`78`: -.. rubric:: Sample Document 0 (78) = +.. _`79`: +.. rubric:: Sample Document 0 (79) = .. parsed-literal:: :class: code @@ -2776,12 +2836,12 @@ Weaving test cases have a common setup shown in this superclass. .. class:: small - |loz| *Sample Document 0 (78)*. Used by: Weave Test references... (`77`_) + |loz| *Sample Document 0 (79)*. Used by: Weave Test references... (`78`_) -.. _`79`: -.. rubric:: Expected Output 0 (79) = +.. _`80`: +.. rubric:: Expected Output 0 (80) = .. parsed-literal:: :class: code @@ -2821,24 +2881,29 @@ Weaving test cases have a common setup shown in this superclass. .. class:: small - |loz| *Expected Output 0 (79)*. Used by: Weave Test references... (`77`_) + |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()``. -.. _`80`: -.. rubric:: Weave Test evaluation of expressions (80) = +.. _`81`: +.. rubric:: Weave Test evaluation of expressions (81) = .. parsed-literal:: :class: code - |srarr|\ Sample Document 9 (`81`_) + |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( ) @@ -2847,7 +2912,7 @@ to properly provide a consistent output from ``time.asctime()``. actual = self.file\_path.with\_suffix(".html").read\_text().splitlines() #print(actual) self.assertEqual("An anonymous chunk.", actual[0]) - self.assertTrue(actual[1].startswith("Time =")) + 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]) @@ -2856,12 +2921,12 @@ to properly provide a consistent output from ``time.asctime()``. .. class:: small - |loz| *Weave Test evaluation of expressions (80)*. Used by: test_weaver.py (`75`_) + |loz| *Weave Test evaluation of expressions (81)*. Used by: test_weaver.py (`76`_) -.. _`81`: -.. rubric:: Sample Document 9 (81) = +.. _`82`: +.. rubric:: Sample Document 9 (82) = .. parsed-literal:: :class: code @@ -2877,12 +2942,12 @@ to properly provide a consistent output from ``time.asctime()``. .. class:: small - |loz| *Sample Document 9 (81)*. Used by: Weave Test evaluation... (`80`_) + |loz| *Sample Document 9 (82)*. Used by: Weave Test evaluation... (`81`_) -.. _`82`: -.. rubric:: Weave Test overheads: imports, etc. (82) = +.. _`83`: +.. rubric:: Weave Test overheads: imports, etc. (83) = .. parsed-literal:: :class: code @@ -2893,6 +2958,8 @@ to properly provide a consistent output from ``time.asctime()``. import os from pathlib import Path import string + import sys + from typing import ClassVar import unittest import pyweb @@ -2901,18 +2968,17 @@ to properly provide a consistent output from ``time.asctime()``. .. class:: small - |loz| *Weave Test overheads: imports, etc. (82)*. Used by: test_weaver.py (`75`_) + |loz| *Weave Test overheads: imports, etc. (83)*. Used by: test_weaver.py (`76`_) -.. _`83`: -.. rubric:: Weave Test main program (83) = +.. _`84`: +.. rubric:: Weave Test main program (84) = .. parsed-literal:: :class: code if \_\_name\_\_ == "\_\_main\_\_": - import sys logging.basicConfig(stream=sys.stderr, level=logging.WARN) unittest.main() @@ -2920,176 +2986,330 @@ to properly provide a consistent output from ``time.asctime()``. .. class:: small - |loz| *Weave Test main program (83)*. Used by: test_weaver.py (`75`_) + |loz| *Weave Test main program (84)*. Used by: test_weaver.py (`76`_) -Combined Test Runner -===================== +Additional Scripts Testing +========================== -.. test/runner.w +.. test/scripts.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. +We provide these two additional scripts; effectively command-line short-cuts: +- ``tangle.py`` -.. _`84`: -.. rubric:: runner.py (84) = +- ``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|\ Combined Test overheads, imports, etc. (`85`_) - |srarr|\ Combined Test suite which imports all other test modules (`86`_) - |srarr|\ Combined Test command line options (`87`_) - |srarr|\ Combined Test main script (`88`_) + |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| *runner.py (84)*. + |loz| *test_scripts.py (85)*. -The overheads import unittest and logging, because those are essential -infrastructure. Additionally, each of the test modules is also imported. +Sample Web File +--------------- +This is a web ``.w`` file to create a document and tangle a small file. -.. _`85`: -.. rubric:: Combined Test overheads, imports, etc. (85) = + +.. _`86`: +.. rubric:: Sample web file to test with (86) = .. parsed-literal:: :class: code - """Combined tests.""" - import argparse - import unittest - import test\_loader - import test\_tangler - import test\_weaver - import test\_unit - import logging - import sys + 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| *Combined Test overheads, imports, etc. (85)*. Used by: runner.py (`84`_) + |loz| *Sample web file to test with (86)*. Used by: test_scripts.py (`85`_) -The test suite is built from each of the individual test modules. +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. -.. _`86`: -.. rubric:: Combined Test suite which imports all other test modules (86) = + +.. _`87`: +.. rubric:: Superclass for test cases (87) = .. parsed-literal:: :class: code - def suite(): - s = unittest.TestSuite() - for m in (test\_loader, test\_tangler, test\_weaver, test\_unit): - s.addTests(unittest.defaultTestLoader.loadTestsFromModule(m)) - return s + 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| *Combined Test suite which imports all other test modules (86)*. Used by: runner.py (`84`_) + |loz| *Superclass for test cases (87)*. Used by: test_scripts.py (`85`_) -In order to debug failing tests, we accept some command-line -parameters to the combined testing script. +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. -.. _`87`: -.. rubric:: Combined Test command line options (87) = + +.. _`88`: +.. rubric:: Test of weave.py (88) = .. parsed-literal:: :class: code - 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 + 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| *Combined Test command line options (87)*. Used by: runner.py (`84`_) + |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 -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. + |loz| *Test of tangle.py (89)*. Used by: test_scripts.py (`85`_) -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. +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. -.. _`88`: -.. rubric:: Combined Test main script (88) = +.. _`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\_\_": - 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)) + logging.basicConfig(stream=sys.stdout, level=logging.WARN) + unittest.main() .. .. class:: small - |loz| *Combined Test main script (88)*. Used by: runner.py (`84`_) + |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. -.. _`89`: -.. rubric:: docutils.conf (89) = +.. _`92`: +.. rubric:: docutils.conf (92) = .. parsed-literal:: :class: code @@ -3104,7 +3324,7 @@ To get the RST to look good, there are two additional files. .. class:: small - |loz| *docutils.conf (89)*. + |loz| *docutils.conf (92)*. ``page-layout.css`` This tweaks one CSS to be sure that @@ -3112,8 +3332,8 @@ the resulting HTML pages are easier to read. These are minor tweaks to the default CSS. -.. _`90`: -.. rubric:: page-layout.css (90) = +.. _`93`: +.. rubric:: page-layout.css (93) = .. parsed-literal:: :class: code @@ -3139,7 +3359,7 @@ tweaks to the default CSS. .. class:: small - |loz| *page-layout.css (90)*. + |loz| *page-layout.css (93)*. Indices @@ -3150,19 +3370,19 @@ Files :docutils.conf: - |srarr|\ (`89`_) + |srarr|\ (`92`_) :page-layout.css: - |srarr|\ (`90`_) -:runner.py: - |srarr|\ (`84`_) + |srarr|\ (`93`_) :test_loader.py: - |srarr|\ (`50`_) + |srarr|\ (`51`_) +:test_scripts.py: + |srarr|\ (`85`_) :test_tangler.py: - |srarr|\ (`59`_) + |srarr|\ (`60`_) :test_unit.py: |srarr|\ (`1`_) :test_weaver.py: - |srarr|\ (`75`_) + |srarr|\ (`76`_) @@ -3170,64 +3390,68 @@ Macros ------ -:Combined Test command line options: - |srarr|\ (`87`_) -:Combined Test main script: - |srarr|\ (`88`_) -:Combined Test overheads, imports, etc.: - |srarr|\ (`85`_) -:Combined Test suite which imports all other test modules: - |srarr|\ (`86`_) :Expected Output 0: - |srarr|\ (`79`_) + |srarr|\ (`80`_) :Load Test error handling with a few common syntax errors: - |srarr|\ (`53`_) + |srarr|\ (`54`_) :Load Test include processing with syntax errors: - |srarr|\ (`55`_) + |srarr|\ (`56`_) :Load Test main program: - |srarr|\ (`58`_) + |srarr|\ (`59`_) :Load Test overheads: imports, etc.: - |srarr|\ (`52`_) |srarr|\ (`57`_) + |srarr|\ (`53`_) |srarr|\ (`58`_) :Load Test superclass to refactor common setup: - |srarr|\ (`51`_) + |srarr|\ (`52`_) :Sample Document 0: - |srarr|\ (`78`_) + |srarr|\ (`79`_) :Sample Document 1 with correct and incorrect syntax: - |srarr|\ (`54`_) + |srarr|\ (`55`_) :Sample Document 2: - |srarr|\ (`62`_) + |srarr|\ (`63`_) :Sample Document 3: - |srarr|\ (`64`_) + |srarr|\ (`65`_) :Sample Document 4: - |srarr|\ (`66`_) + |srarr|\ (`67`_) :Sample Document 5: - |srarr|\ (`68`_) + |srarr|\ (`69`_) :Sample Document 6: - |srarr|\ (`70`_) + |srarr|\ (`71`_) :Sample Document 7 and it's included file: - |srarr|\ (`72`_) + |srarr|\ (`73`_) :Sample Document 8 and the file it includes: - |srarr|\ (`56`_) + |srarr|\ (`57`_) :Sample Document 9: - |srarr|\ (`81`_) + |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|\ (`71`_) + |srarr|\ (`72`_) :Tangle Test main program: - |srarr|\ (`74`_) + |srarr|\ (`75`_) :Tangle Test overheads: imports, etc.: - |srarr|\ (`73`_) + |srarr|\ (`74`_) :Tangle Test semantic error 2: - |srarr|\ (`61`_) + |srarr|\ (`62`_) :Tangle Test semantic error 3: - |srarr|\ (`63`_) + |srarr|\ (`64`_) :Tangle Test semantic error 4: - |srarr|\ (`65`_) + |srarr|\ (`66`_) :Tangle Test semantic error 5: - |srarr|\ (`67`_) + |srarr|\ (`68`_) :Tangle Test semantic error 6: - |srarr|\ (`69`_) + |srarr|\ (`70`_) :Tangle Test superclass to refactor common setup: - |srarr|\ (`60`_) + |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: @@ -3241,7 +3465,7 @@ Macros :Unit Test Web class weave: |srarr|\ (`38`_) :Unit Test main: - |srarr|\ (`49`_) + |srarr|\ (`50`_) :Unit Test of Action class hierarchy: |srarr|\ (`42`_) :Unit Test of Application class: @@ -3305,7 +3529,7 @@ Macros :Unit Test of XrefCommand superclass for all cross-reference commands: |srarr|\ (`27`_) :Unit Test overheads: imports, etc.: - |srarr|\ (`48`_) + |srarr|\ (`48`_) |srarr|\ (`49`_) :Unit test of Action Sequence class: |srarr|\ (`43`_) :Unit test of LoadAction class: @@ -3315,15 +3539,15 @@ Macros :Unit test of WeaverAction class: |srarr|\ (`44`_) :Weave Test evaluation of expressions: - |srarr|\ (`80`_) + |srarr|\ (`81`_) :Weave Test main program: - |srarr|\ (`83`_) + |srarr|\ (`84`_) :Weave Test overheads: imports, etc.: - |srarr|\ (`82`_) + |srarr|\ (`83`_) :Weave Test references and definitions: - |srarr|\ (`77`_) + |srarr|\ (`78`_) :Weave Test superclass to refactor common setup: - |srarr|\ (`76`_) + |srarr|\ (`77`_) @@ -3337,9 +3561,9 @@ User Identifiers .. class:: small - Created by pyweb.py at Sat Jun 11 08:26:49 2022. + Created by pyweb.py at Sun Jun 12 19:07:28 2022. - Source test/pyweb_test.w modified Fri Jun 10 17:07:24 2022. + Source tests/pyweb_test.w modified Sat Jun 11 08:30:06 2022. pyweb.__version__ '3.1'. diff --git a/test/pyweb_test.w b/tests/pyweb_test.w similarity index 92% rename from test/pyweb_test.w rename to tests/pyweb_test.w index 24abdfa..17c817e 100644 --- a/test/pyweb_test.w +++ b/tests/pyweb_test.w @@ -19,12 +19,15 @@ Yet Another Literate Programming Tool @i func.w -@i runner.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. @@ -62,6 +65,7 @@ div.document { width: 7in; } } @} + Indices ======= diff --git a/test/runner.py b/tests/runner.py similarity index 100% rename from test/runner.py rename to tests/runner.py diff --git a/test/runner.w b/tests/runner.w similarity index 100% rename from test/runner.w rename to tests/runner.w 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 +@{@