pax_global_header00006660000000000000000000000064145724077620014530gustar00rootroot0000000000000052 comment=cceb6cbbc67c7bb35758d5ba7772a8482d143b56 cog-3.4.1/000077500000000000000000000000001457240776200123055ustar00rootroot00000000000000cog-3.4.1/.coveragerc000066400000000000000000000004651457240776200144330ustar00rootroot00000000000000# coverage configuration for Cog. [run] branch = True parallel = True source = cogapp [report] exclude_lines = pragma: no cover raise CogInternalError\( precision = 2 [html] title = Cog coverage [paths] source = cogapp # GitHub Actions uses a few different home dir styles */cog/cogapp cog-3.4.1/.editorconfig000066400000000000000000000010521457240776200147600ustar00rootroot00000000000000# This file is for unifying the coding style for different editors and IDEs. # More information at http://EditorConfig.org root = true [*] charset = utf-8 end_of_line = lf indent_size = 4 indent_style = space insert_final_newline = true max_line_length = 80 trim_trailing_whitespace = true [*.py] max_line_length = 100 [*.yml] indent_size = 2 [*.rst] max_line_length = 79 [Makefile] indent_style = tab indent_size = 8 [*,cover] trim_trailing_whitespace = false [*.diff] trim_trailing_whitespace = false [.git/*] trim_trailing_whitespace = false cog-3.4.1/.github/000077500000000000000000000000001457240776200136455ustar00rootroot00000000000000cog-3.4.1/.github/workflows/000077500000000000000000000000001457240776200157025ustar00rootroot00000000000000cog-3.4.1/.github/workflows/ci.yml000066400000000000000000000036771457240776200170350ustar00rootroot00000000000000name: "CI" on: push: pull_request: defaults: run: shell: bash permissions: contents: read concurrency: group: "${{ github.workflow }}-${{ github.ref }}" cancel-in-progress: true jobs: tests: name: "Python ${{ matrix.python }} on ${{ matrix.os }}" runs-on: "${{ matrix.os }}" strategy: fail-fast: false matrix: os: - ubuntu-latest - macos-latest - windows-latest python: # When changing this list, be sure to check the [gh] list in # tox.ini so that tox will run properly. - "3.7" - "3.8" - "3.9" - "3.10" - "3.11" - "3.12" steps: - name: "Check out the repo" uses: "actions/checkout@v2" - name: "Set up Python" uses: "actions/setup-python@v2" with: python-version: "${{ matrix.python }}" - name: "Install dependencies" run: | python -m pip install -r requirements.pip - name: "Run tox for ${{ matrix.python }}" run: | python -m tox python -m coverage debug data - name: "Upload coverage data" uses: actions/upload-artifact@v2 with: name: covdata path: .coverage.* combine: name: "Combine and report coverage" needs: tests runs-on: ubuntu-latest steps: - name: "Check out the repo" uses: "actions/checkout@v2" with: fetch-depth: "0" - name: "Set up Python" uses: "actions/setup-python@v2" with: python-version: "3.8" - name: "Install dependencies" run: | python -m pip install -r requirements.pip - name: "Download coverage data" uses: actions/download-artifact@v2 with: name: covdata - name: "Combine and report" run: | python -m coverage combine python -m coverage report -m cog-3.4.1/.gitignore000066400000000000000000000003411457240776200142730ustar00rootroot00000000000000# Files that can appear anywhere in the tree. *.pyc *.pyo *.pyd *$py.class *.bak # Stuff in the root. build dist .coverage .coverage.* coverage.xml htmlcov MANIFEST setuptools-*.egg cogapp.egg-info .tox .*cache docs/_build cog-3.4.1/.readthedocs.yaml000066400000000000000000000005261457240776200155370ustar00rootroot00000000000000# ReadTheDocs configuration. # See https://docs.readthedocs.io/en/stable/config-file/v2.html version: 2 build: os: ubuntu-22.04 tools: python: "3.11" sphinx: builder: html configuration: docs/conf.py # Build all the formats formats: all python: install: - requirements: requirements.pip - method: pip path: . cog-3.4.1/AUTHORS.txt000066400000000000000000000004151457240776200141730ustar00rootroot00000000000000Cog was written by Ned Batchelder (ned@nedbatchelder.com). Contributions have been made by: Alexander Belchenko Anders Hovmöller Blake Winton Daniel Murdin Doug Hellmann Hugh Perkins Jean-François Giraud Panayiotis Gavriil Petr Gladkiy Phil Kirkpatrick Ryan Santos cog-3.4.1/CHANGELOG.rst000066400000000000000000000254111457240776200143310ustar00rootroot00000000000000Changelog ========= .. split out from the main page. 2.1: -u flag more 2.1 stuff add a pointer to the russian. started the 2.2 list. 2.2 2.3 2.4 2.5.1 3.0.0 3.1.0 These are changes to Cog over time. 3.4.1 – March 7 2024 -------------------- - Dropped support for Python 2.7, 3.5, and 3.6, and added 3.11 and 3.12. - Removed the ``cog.py`` installed file. Use the ``cog`` command, or ``python -m cogapp`` to run cog. - Processing long files has been made much faster. Thanks, Panayiotis Gavriil. - Files listing other files to process can now be specified as ``&files_to_cog.txt`` to interpret the file names relative to the location of the list file. The existing ``@files_to_cog.txt`` syntax interprets file names relative to the current working directory. Thanks, Phil Kirkpatrick. - Support FIPS mode computers by marking our MD5 use as not related to security. Thanks, Ryan Santos. - Docs have moved to https://cog.readthedocs.io 3.3.0 – November 19 2021 ------------------------ - Added the ``--check`` option to check whether files would change if run again, for use in continuous integration scenarios. 3.2.0 – November 7 2021 ----------------------- - Added the ``-P`` option to use `print()` instead of `cog.outl()` for code output. 3.1.0 – August 31 2021 ---------------------- - Fix a problem with Python 3.8.10 and 3.9.5 that require absolute paths in sys.path. `issue 16`_. - Python 3.9 and 3.10 are supported. .. _issue 16: https://github.com/nedbat/cog/issues/16 3.0.0 – April 2 2019 -------------------- - Dropped support for Pythons 2.6, 3.3, and 3.4. - Errors occurring during content generation now print accurate tracebacks, showing the correct filename, line number, and source line. - Cog can now (again?) be run as just "cog" on the command line. - The ``-p=PROLOGUE`` option was added to specify Python text to prepend to embedded code. Thanks, Anders Hovmöller. - Wildcards in command line arguments will be expanded by cog to help on Windows. Thanks, Hugh Perkins. - When using implicitly imported "cog", a new module is made for each run. This is important when using the cog API multi-threaded. Thanks, Daniel Murdin. - Moved development to GitHub. 2.5.1 – October 19 2016 ----------------------- - Corrected a long-standing oversight: added a LICENSE.txt file. 2.5 – February 13 2016 ---------------------- - When specifying an output file with ``-o``, directories will be created as needed to write the file. Thanks, Jean-François Giraud. 2.4 – January 11 2015 --------------------- - A ``--markers`` option lets you control the three markers that separate the cog code and result from the rest of the file. Thanks, Doug Hellmann. - A ``-n=ENCODING`` option that lets you specify the encoding for the input and output files. Thanks, Petr Gladkiy. - A ``--verbose`` option that lets you control how much chatter is in the output while cogging. 2.3 – February 27 2012 ---------------------- - Python 3 is now supported. Older Pythons (2.5 and below) no longer are. - Added the `cog.previous` attribute to get the output from the last time cog was run. - An input file name of "-" will read input from standard in. - Cog can now be run with "python3 -m cogapp [args]". - All files are assumed to be encoded with UTF-8. 2.2 – June 25 2009 ------------------ - Jython 2.5 is now supported. - Removed a warning about using the no-longer-recommended md5 module. - Removed handyxml: most Cog users don't need it. 2.1 – May 22 2008 ----------------- - Added the ``-U`` switch to create Unix newlines on Windows. - Improved argument validation: ``-d`` can be used with stdout-destined output, and switches are validated for every line of an @file, to prevent bad interactions. 2.0 – October 6 2005 -------------------- Incompatible changes: - Python 2.2 is no longer supported. - In 1.4, you could put some generator code on the ``[[[cog`` line and some on the ``]]]`` line, to make the generators more compact. Unfortunately, this also made it more difficult to seamlessly embed those markers in source files of all kinds. Now code is only allowed on marker lines when the entire generator is single-line. - In 1.x, you could leave out the ``[[[end]]]`` marker, and it would be assumed at the end of the file. Now that behavior must be enabled with a ``-z`` switch. Without the switch, omitting the end marker is an error. Beneficial changes: - The new ``-d`` switch removes all the generator code from the output file while running it to generate output (thanks, Blake). - The new ``-D`` switch lets you define global string values for the generators. - The new ``-s`` switch lets you mark generated output lines with a suffix. - @-files now can have command line switches in addition to file names. - Cog error messages now print without a traceback, and use a format similar to compiler error messages, so that clicking the message will likely bring you to the spot in your code (thanks, Mike). - New cog method #1: `cog.error(msg)` will raise an error and end processing without creating a scary Python traceback (thanks, Alexander). - New cog method #2: `cog.msg(msg)` will print the msg to stdout. This is better than print because it allows for all cog output to be managed through Cog. - The sequence of Cog marker lines is much stricter. This helps to ensure that Cog isn't eating up your precious source code (thanks, Kevin). 1.4 – February 25 2005 ---------------------- - Added the ``-x`` switch to excise generated output. - Added the ``-c`` switch to checksum the generated output. 1.3 – December 30 2004 ---------------------- - All of the generators in a single file are now run with a common globals dictionary, so that state may be carried from one to the next. 1.2 – December 29 2004 ---------------------- - Added module attributes `cog.inFile`, `cog.outFile`, and `cog.firstLineNum`. - Made the `sOut` argument optional in `cog.out` and `cog.outl`. - Added the compact one-line form of cog markers. - Some warning messages weren't properly printing the file name. 1.12 – June 21 2004 ------------------- - Changed all the line endings in the source to the more-portable LF from the Windows-only CRLF. 1.11 – June 5 2004 ------------------ Just bug fixes: - Cog's whitespace handling deals correctly with a completely blank line (no whitespace at all) in a chunk of Cog code. - Elements returned by handyxml can now have attributes assigned to them after parsing. 1.1 – March 21 2004 ------------------- - Now if the cog marker lines and all the lines they contain have the same prefix characters, then the prefix is removed from each line. This allows cog to be used with languages that don't support multi-line comments. - Ensure the last line of the output ends with a newline, or it will merge with the end marker, ruining cog's idempotency. - Add the ``-v`` command line option, to print the version. - Running cog with no options prints the usage help. 1.0 – February 10 2004 ---------------------- First version. .. # History moved from cogapp.py: # 20040210: First public version. # 20040220: Text preceding the start and end marker are removed from Python lines. # -v option on the command line shows the version. # 20040311: Make sure the last line of output is properly ended with a newline. # 20040605: Fixed some blank line handling in cog. # Fixed problems with assigning to xml elements in handyxml. # 20040621: Changed all line-ends to LF from CRLF. # 20041002: Refactor some option handling to simplify unittesting the options. # 20041118: cog.out and cog.outl have optional string arguments. # 20041119: File names weren't being properly passed around for warnings, etc. # 20041122: Added cog.firstLineNum: a property with the line number of the [[[cog line. # Added cog.inFile and cog.outFile: the names of the input and output file. # 20041218: Single-line cog generators, with start marker and end marker on # the same line. # 20041230: Keep a single globals dict for all the code fragments in a single # file so they can share state. # 20050206: Added the -x switch to remove all generated output. # 20050218: Now code can be on the marker lines as well. # 20050219: Added -c switch to checksum the output so that edits can be # detected before they are obliterated. # 20050521: Added cog.error, contributed by Alexander Belchenko. # 20050720: Added code deletion and settable globals contributed by Blake Winton. # 20050724: Many tweaks to improve code coverage. # 20050726: Error messages are now printed with no traceback. # Code can no longer appear on the marker lines, # except for single-line style. # -z allows omission of the [[[end]]] marker, and it will be assumed # at the end of the file. # 20050729: Refactor option parsing into a separate class, in preparation for # future features. # 20050805: The cogmodule.path wasn't being properly maintained. # 20050808: Added the -D option to define a global value. # 20050810: The %s in the -w command is dealt with more robustly. # Added the -s option to suffix output lines with a marker. # 20050817: Now @files can have arguments on each line to change the cog's # behavior for that line. # 20051006: Version 2.0 # 20080521: -U options lets you create Unix newlines on Windows. Thanks, # Alexander Belchenko. # 20080522: It's now ok to have -d with output to stdout, and now we validate # the args after each line of an @file. # 20090520: Use hashlib where it's available, to avoid a warning. # Use the builtin compile() instead of compiler, for Jython. # Explicitly close files we opened, Jython likes this. # 20120205: Port to Python 3. Lowest supported version is 2.6. # 20150104: -markers option added by Doug Hellmann. # 20150104: -n ENCODING option added by Petr Gladkiy. # 20150107: Added -verbose to control what files get listed. # 20150111: Version 2.4 # 20160213: v2.5: -o makes needed directories, thanks Jean-François Giraud. # 20161019: Added a LICENSE.txt file. cog-3.4.1/LICENSE.txt000066400000000000000000000020641457240776200141320ustar00rootroot00000000000000MIT License Copyright (c) 2004-2024 Ned Batchelder Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. cog-3.4.1/MANIFEST.in000066400000000000000000000005241457240776200140440ustar00rootroot00000000000000include .coveragerc include .editorconfig include .readthedocs.yaml include CHANGELOG.rst include LICENSE.txt include Makefile include README.rst include requirements.pip include tox.ini recursive-include cogapp *.py recursive-include docs Makefile *.py *.rst recursive-include docs/_static * recursive-include success * prune doc/_build cog-3.4.1/Makefile000066400000000000000000000031021457240776200137410ustar00rootroot00000000000000# Makefile for cog work. .PHONY: help clean sterile test help: ## Show this help. @echo "Available targets:" @grep '^[a-zA-Z]' $(MAKEFILE_LIST) | sort | awk -F ':.*?## ' 'NF==2 {printf " %-26s%s\n", $$1, $$2}' clean: ## Remove artifacts of test execution, installation, etc. -rm -rf build -rm -rf dist -rm -f MANIFEST -rm -f *.pyc */*.pyc */*/*.pyc */*/*/*.pyc -rm -f *.pyo */*.pyo */*/*.pyo */*/*/*.pyo -rm -f *$$py.class */*$$py.class */*/*$$py.class */*/*/*$$py.class -rm -rf __pycache__ */__pycache__ */*/__pycache__ -rm -f *.bak */*.bak */*/*.bak */*/*/*.bak -rm -f .coverage .coverage.* coverage.xml -rm -rf cogapp.egg-info htmlcov -rm -rf docs/_build sterile: clean ## Remove all non-controlled content. -rm -rf .tox* -rm -rf .*_cache test: ## Run the test suite. tox -q # Docs .PHONY: cogdoc dochtml cogdoc: ## Run cog to keep the docs correct. # Normally I'd put this in a comment in index.px, but the # quoting/escaping would be impossible. python -m cogapp -crP --markers='{{{cog }}} {{{end}}}' docs/running.rst dochtml: ## Build local docs. $(MAKE) -C docs html # Release .PHONY: dist pypi testpypi check_release dist: ## Build distribution artifacts. python -m build twine check dist/* pypi: ## Upload distributions to PyPI. twine upload --verbose dist/* testpypi: ## Upload distributions to test PyPI twine upload --verbose --repository testpypi --password $$TWINE_TEST_PASSWORD dist/* check_release: _check_manifest ## Check that we are ready for a release @echo "Release checks passed" _check_manifest: python -m check_manifest cog-3.4.1/README.rst000066400000000000000000000023711457240776200137770ustar00rootroot00000000000000=== Cog === Cog content generation tool. Small bits of computation for static files. | |license| |versions| |status| | |ci-status| |kit| |format| See the `cog docs`_ for details. .. _cog docs: https://cog.readthedocs.io/en/latest/ Code repository and issue tracker are at `GitHub `_. To run the tests:: $ pip install -r requirements.pip $ tox .. |ci-status| image:: https://github.com/nedbat/cog/actions/workflows/ci.yml/badge.svg?branch=master&event=push :target: https://github.com/nedbat/cog/actions/workflows/ci.yml :alt: CI status .. |kit| image:: https://img.shields.io/pypi/v/cogapp.svg :target: https://pypi.org/project/cogapp/ :alt: PyPI status .. |format| image:: https://img.shields.io/pypi/format/cogapp.svg :target: https://pypi.org/project/cogapp/ :alt: Kit format .. |license| image:: https://img.shields.io/pypi/l/cogapp.svg :target: https://pypi.org/project/cogapp/ :alt: License .. |versions| image:: https://img.shields.io/pypi/pyversions/cogapp.svg :target: https://pypi.org/project/cogapp/ :alt: Python versions supported .. |status| image:: https://img.shields.io/pypi/status/cogapp.svg :target: https://pypi.org/project/cogapp/ :alt: Package stability cog-3.4.1/cogapp/000077500000000000000000000000001457240776200135565ustar00rootroot00000000000000cog-3.4.1/cogapp/__init__.py000066400000000000000000000002431457240776200156660ustar00rootroot00000000000000""" Cog content generation tool. http://nedbatchelder.com/code/cog Copyright 2004-2024, Ned Batchelder. """ from .cogapp import Cog, CogUsageError, main cog-3.4.1/cogapp/__main__.py000066400000000000000000000001661457240776200156530ustar00rootroot00000000000000"""Make Cog runnable directly from the module.""" import sys from cogapp import Cog sys.exit(Cog().main(sys.argv)) cog-3.4.1/cogapp/cogapp.py000066400000000000000000000721011457240776200154020ustar00rootroot00000000000000""" Cog content generation tool. """ import copy import getopt import glob import io import linecache import os import re import shlex import sys import traceback import types from .whiteutils import commonPrefix, reindentBlock, whitePrefix from .utils import NumberedFileReader, Redirectable, change_dir, md5 __version__ = "3.4.1" usage = """\ cog - generate content with inlined Python code. cog [OPTIONS] [INFILE | @FILELIST | &FILELIST] ... INFILE is the name of an input file, '-' will read from stdin. FILELIST is the name of a text file containing file names or other @FILELISTs. For @FILELIST, paths in the file list are relative to the working directory where cog was called. For &FILELIST, paths in the file list are relative to the file list location. OPTIONS: -c Checksum the output to protect it against accidental change. -d Delete the generator code from the output file. -D name=val Define a global string available to your generator code. -e Warn if a file has no cog code in it. -I PATH Add PATH to the list of directories for data files and modules. -n ENCODING Use ENCODING when reading and writing files. -o OUTNAME Write the output to OUTNAME. -p PROLOGUE Prepend the generator source with PROLOGUE. Useful to insert an import line. Example: -p "import math" -P Use print() instead of cog.outl() for code output. -r Replace the input file with the output. -s STRING Suffix all generated output lines with STRING. -U Write the output with Unix newlines (only LF line-endings). -w CMD Use CMD if the output file needs to be made writable. A %s in the CMD will be filled with the filename. -x Excise all the generated output without running the generators. -z The end-output marker can be omitted, and is assumed at eof. -v Print the version of cog and exit. --check Check that the files would not change if run again. --markers='START END END-OUTPUT' The patterns surrounding cog inline instructions. Should include three values separated by spaces, the start, end, and end-output markers. Defaults to '[[[cog ]]] [[[end]]]'. --verbosity=VERBOSITY Control the amount of output. 2 (the default) lists all files, 1 lists only changed files, 0 lists no files. -h Print this help. """ class CogError(Exception): """ Any exception raised by Cog. """ def __init__(self, msg, file='', line=0): if file: super().__init__(f"{file}({line}): {msg}") else: super().__init__(msg) class CogUsageError(CogError): """ An error in usage of command-line arguments in cog. """ pass class CogInternalError(CogError): """ An error in the coding of Cog. Should never happen. """ pass class CogGeneratedError(CogError): """ An error raised by a user's cog generator. """ pass class CogUserException(CogError): """ An exception caught when running a user's cog generator. The argument is the traceback message to print. """ pass class CogCheckFailed(CogError): """ A --check failed. """ pass class CogGenerator(Redirectable): """ A generator pulled from a source file. """ def __init__(self, options=None): super().__init__() self.markers = [] self.lines = [] self.options = options or CogOptions() def parseMarker(self, l): self.markers.append(l) def parseLine(self, l): self.lines.append(l.strip('\n')) def getCode(self): """ Extract the executable Python code from the generator. """ # If the markers and lines all have the same prefix # (end-of-line comment chars, for example), # then remove it from all the lines. prefIn = commonPrefix(self.markers + self.lines) if prefIn: self.markers = [ l.replace(prefIn, '', 1) for l in self.markers ] self.lines = [ l.replace(prefIn, '', 1) for l in self.lines ] return reindentBlock(self.lines, '') def evaluate(self, cog, globals, fname): # figure out the right whitespace prefix for the output prefOut = whitePrefix(self.markers) intext = self.getCode() if not intext: return '' prologue = "import " + cog.cogmodulename + " as cog\n" if self.options.sPrologue: prologue += self.options.sPrologue + '\n' code = compile(prologue + intext, str(fname), 'exec') # Make sure the "cog" module has our state. cog.cogmodule.msg = self.msg cog.cogmodule.out = self.out cog.cogmodule.outl = self.outl cog.cogmodule.error = self.error real_stdout = sys.stdout if self.options.bPrintOutput: sys.stdout = captured_stdout = io.StringIO() self.outstring = '' try: eval(code, globals) except CogError: raise except: typ, err, tb = sys.exc_info() frames = (tuple(fr) for fr in traceback.extract_tb(tb.tb_next)) frames = find_cog_source(frames, prologue) msg = "".join(traceback.format_list(frames)) msg += f"{typ.__name__}: {err}" raise CogUserException(msg) finally: sys.stdout = real_stdout if self.options.bPrintOutput: self.outstring = captured_stdout.getvalue() # We need to make sure that the last line in the output # ends with a newline, or it will be joined to the # end-output line, ruining cog's idempotency. if self.outstring and self.outstring[-1] != '\n': self.outstring += '\n' return reindentBlock(self.outstring, prefOut) def msg(self, s): self.prout("Message: "+s) def out(self, sOut='', dedent=False, trimblanklines=False): """ The cog.out function. """ if trimblanklines and ('\n' in sOut): lines = sOut.split('\n') if lines[0].strip() == '': del lines[0] if lines and lines[-1].strip() == '': del lines[-1] sOut = '\n'.join(lines)+'\n' if dedent: sOut = reindentBlock(sOut) self.outstring += sOut def outl(self, sOut='', **kw): """ The cog.outl function. """ self.out(sOut, **kw) self.out('\n') def error(self, msg='Error raised by cog generator.'): """ The cog.error function. Instead of raising standard python errors, cog generators can use this function. It will display the error without a scary Python traceback. """ raise CogGeneratedError(msg) class CogOptions: """ Options for a run of cog. """ def __init__(self): # Defaults for argument values. self.args = [] self.includePath = [] self.defines = {} self.bShowVersion = False self.sMakeWritableCmd = None self.bReplace = False self.bNoGenerate = False self.sOutputName = None self.bWarnEmpty = False self.bHashOutput = False self.bDeleteCode = False self.bEofCanBeEnd = False self.sSuffix = None self.bNewlines = False self.sBeginSpec = '[[[cog' self.sEndSpec = ']]]' self.sEndOutput = '[[[end]]]' self.sEncoding = "utf-8" self.verbosity = 2 self.sPrologue = '' self.bPrintOutput = False self.bCheck = False def __eq__(self, other): """ Comparison operator for tests to use. """ return self.__dict__ == other.__dict__ def clone(self): """ Make a clone of these options, for further refinement. """ return copy.deepcopy(self) def addToIncludePath(self, dirs): """ Add directories to the include path. """ dirs = dirs.split(os.pathsep) self.includePath.extend(dirs) def parseArgs(self, argv): # Parse the command line arguments. try: opts, self.args = getopt.getopt( argv, 'cdD:eI:n:o:rs:p:PUvw:xz', [ 'check', 'markers=', 'verbosity=', ] ) except getopt.error as msg: raise CogUsageError(msg) # Handle the command line arguments. for o, a in opts: if o == '-c': self.bHashOutput = True elif o == '-d': self.bDeleteCode = True elif o == '-D': if a.count('=') < 1: raise CogUsageError("-D takes a name=value argument") name, value = a.split('=', 1) self.defines[name] = value elif o == '-e': self.bWarnEmpty = True elif o == '-I': self.addToIncludePath(os.path.abspath(a)) elif o == '-n': self.sEncoding = a elif o == '-o': self.sOutputName = a elif o == '-r': self.bReplace = True elif o == '-s': self.sSuffix = a elif o == '-p': self.sPrologue = a elif o == '-P': self.bPrintOutput = True elif o == '-U': self.bNewlines = True elif o == '-v': self.bShowVersion = True elif o == '-w': self.sMakeWritableCmd = a elif o == '-x': self.bNoGenerate = True elif o == '-z': self.bEofCanBeEnd = True elif o == '--check': self.bCheck = True elif o == '--markers': self._parse_markers(a) elif o == '--verbosity': self.verbosity = int(a) else: # Since getopt.getopt is given a list of possible flags, # this is an internal error. raise CogInternalError(f"Don't understand argument {o}") def _parse_markers(self, val): try: self.sBeginSpec, self.sEndSpec, self.sEndOutput = val.split(" ") except ValueError: raise CogUsageError( f"--markers requires 3 values separated by spaces, could not parse {val!r}" ) def validate(self): """ Does nothing if everything is OK, raises CogError's if it's not. """ if self.bReplace and self.bDeleteCode: raise CogUsageError("Can't use -d with -r (or you would delete all your source!)") if self.bReplace and self.sOutputName: raise CogUsageError("Can't use -o with -r (they are opposites)") class Cog(Redirectable): """ The Cog engine. """ def __init__(self): super().__init__() self.options = CogOptions() self._fixEndOutputPatterns() self.cogmodulename = "cog" self.createCogModule() self.bCheckFailed = False def _fixEndOutputPatterns(self): end_output = re.escape(self.options.sEndOutput) self.reEndOutput = re.compile(end_output + r"(?P *\(checksum: (?P[a-f0-9]+)\))") self.sEndFormat = self.options.sEndOutput + " (checksum: %s)" def showWarning(self, msg): self.prout(f"Warning: {msg}") def isBeginSpecLine(self, s): return self.options.sBeginSpec in s def isEndSpecLine(self, s): return self.options.sEndSpec in s and not self.isEndOutputLine(s) def isEndOutputLine(self, s): return self.options.sEndOutput in s def createCogModule(self): """ Make a cog "module" object so that imported Python modules can say "import cog" and get our state. """ self.cogmodule = types.SimpleNamespace() self.cogmodule.path = [] def openOutputFile(self, fname): """ Open an output file, taking all the details into account. """ opts = {} mode = "w" opts['encoding'] = self.options.sEncoding if self.options.bNewlines: opts["newline"] = "\n" fdir = os.path.dirname(fname) if os.path.dirname(fdir) and not os.path.exists(fdir): os.makedirs(fdir) return open(fname, mode, **opts) def openInputFile(self, fname): """ Open an input file. """ if fname == "-": return sys.stdin else: return open(fname, encoding=self.options.sEncoding) def processFile(self, fIn, fOut, fname=None, globals=None): """ Process an input file object to an output file object. fIn and fOut can be file objects, or file names. """ sFileIn = fname or '' sFileOut = fname or '' fInToClose = fOutToClose = None # Convert filenames to files. if isinstance(fIn, (bytes, str)): # Open the input file. sFileIn = fIn fIn = fInToClose = self.openInputFile(fIn) if isinstance(fOut, (bytes, str)): # Open the output file. sFileOut = fOut fOut = fOutToClose = self.openOutputFile(fOut) try: fIn = NumberedFileReader(fIn) bSawCog = False self.cogmodule.inFile = sFileIn self.cogmodule.outFile = sFileOut self.cogmodulename = 'cog_' + md5(sFileOut.encode()).hexdigest() sys.modules[self.cogmodulename] = self.cogmodule # if "import cog" explicitly done in code by user, note threading will cause clashes. sys.modules['cog'] = self.cogmodule # The globals dict we'll use for this file. if globals is None: globals = {} # If there are any global defines, put them in the globals. globals.update(self.options.defines) # loop over generator chunks l = fIn.readline() while l: # Find the next spec begin while l and not self.isBeginSpecLine(l): if self.isEndSpecLine(l): raise CogError( f"Unexpected {self.options.sEndSpec!r}", file=sFileIn, line=fIn.linenumber(), ) if self.isEndOutputLine(l): raise CogError( f"Unexpected {self.options.sEndOutput!r}", file=sFileIn, line=fIn.linenumber(), ) fOut.write(l) l = fIn.readline() if not l: break if not self.options.bDeleteCode: fOut.write(l) # l is the begin spec gen = CogGenerator(options=self.options) gen.setOutput(stdout=self.stdout) gen.parseMarker(l) firstLineNum = fIn.linenumber() self.cogmodule.firstLineNum = firstLineNum # If the spec begin is also a spec end, then process the single # line of code inside. if self.isEndSpecLine(l): beg = l.find(self.options.sBeginSpec) end = l.find(self.options.sEndSpec) if beg > end: raise CogError("Cog code markers inverted", file=sFileIn, line=firstLineNum) else: sCode = l[beg+len(self.options.sBeginSpec):end].strip() gen.parseLine(sCode) else: # Deal with an ordinary code block. l = fIn.readline() # Get all the lines in the spec while l and not self.isEndSpecLine(l): if self.isBeginSpecLine(l): raise CogError( f"Unexpected {self.options.sBeginSpec!r}", file=sFileIn, line=fIn.linenumber(), ) if self.isEndOutputLine(l): raise CogError( f"Unexpected {self.options.sEndOutput!r}", file=sFileIn, line=fIn.linenumber(), ) if not self.options.bDeleteCode: fOut.write(l) gen.parseLine(l) l = fIn.readline() if not l: raise CogError( "Cog block begun but never ended.", file=sFileIn, line=firstLineNum) if not self.options.bDeleteCode: fOut.write(l) gen.parseMarker(l) l = fIn.readline() # Eat all the lines in the output section. While reading past # them, compute the md5 hash of the old output. previous = [] hasher = md5() while l and not self.isEndOutputLine(l): if self.isBeginSpecLine(l): raise CogError( f"Unexpected {self.options.sBeginSpec!r}", file=sFileIn, line=fIn.linenumber(), ) if self.isEndSpecLine(l): raise CogError( f"Unexpected {self.options.sEndSpec!r}", file=sFileIn, line=fIn.linenumber(), ) previous.append(l) hasher.update(l.encode("utf-8")) l = fIn.readline() curHash = hasher.hexdigest() if not l and not self.options.bEofCanBeEnd: # We reached end of file before we found the end output line. raise CogError( f"Missing {self.options.sEndOutput!r} before end of file.", file=sFileIn, line=fIn.linenumber(), ) # Make the previous output available to the current code self.cogmodule.previous = "".join(previous) # Write the output of the spec to be the new output if we're # supposed to generate code. hasher = md5() if not self.options.bNoGenerate: sFile = f"" sGen = gen.evaluate(cog=self, globals=globals, fname=sFile) sGen = self.suffixLines(sGen) hasher.update(sGen.encode("utf-8")) fOut.write(sGen) newHash = hasher.hexdigest() bSawCog = True # Write the ending output line hashMatch = self.reEndOutput.search(l) if self.options.bHashOutput: if hashMatch: oldHash = hashMatch['hash'] if oldHash != curHash: raise CogError("Output has been edited! Delete old checksum to unprotect.", file=sFileIn, line=fIn.linenumber()) # Create a new end line with the correct hash. endpieces = l.split(hashMatch.group(0), 1) else: # There was no old hash, but we want a new hash. endpieces = l.split(self.options.sEndOutput, 1) l = (self.sEndFormat % newHash).join(endpieces) else: # We don't want hashes output, so if there was one, get rid of # it. if hashMatch: l = l.replace(hashMatch['hashsect'], '', 1) if not self.options.bDeleteCode: fOut.write(l) l = fIn.readline() if not bSawCog and self.options.bWarnEmpty: self.showWarning(f"no cog code found in {sFileIn}") finally: if fInToClose: fInToClose.close() if fOutToClose: fOutToClose.close() # A regex for non-empty lines, used by suffixLines. reNonEmptyLines = re.compile(r"^\s*\S+.*$", re.MULTILINE) def suffixLines(self, text): """ Add suffixes to the lines in text, if our options desire it. text is many lines, as a single string. """ if self.options.sSuffix: # Find all non-blank lines, and add the suffix to the end. repl = r"\g<0>" + self.options.sSuffix.replace('\\', '\\\\') text = self.reNonEmptyLines.sub(repl, text) return text def processString(self, sInput, fname=None): """ Process sInput as the text to cog. Return the cogged output as a string. """ fOld = io.StringIO(sInput) fNew = io.StringIO() self.processFile(fOld, fNew, fname=fname) return fNew.getvalue() def replaceFile(self, sOldPath, sNewText): """ Replace file sOldPath with the contents sNewText """ if not os.access(sOldPath, os.W_OK): # Need to ensure we can write. if self.options.sMakeWritableCmd: # Use an external command to make the file writable. cmd = self.options.sMakeWritableCmd.replace('%s', sOldPath) with os.popen(cmd) as cmdout: self.stdout.write(cmdout.read()) if not os.access(sOldPath, os.W_OK): raise CogError(f"Couldn't make {sOldPath} writable") else: # Can't write! raise CogError(f"Can't overwrite {sOldPath}") f = self.openOutputFile(sOldPath) f.write(sNewText) f.close() def saveIncludePath(self): self.savedInclude = self.options.includePath[:] self.savedSysPath = sys.path[:] def restoreIncludePath(self): self.options.includePath = self.savedInclude self.cogmodule.path = self.options.includePath sys.path = self.savedSysPath def addToIncludePath(self, includePath): self.cogmodule.path.extend(includePath) sys.path.extend(includePath) def processOneFile(self, sFile): """ Process one filename through cog. """ self.saveIncludePath() bNeedNewline = False try: self.addToIncludePath(self.options.includePath) # Since we know where the input file came from, # push its directory onto the include path. self.addToIncludePath([os.path.dirname(sFile)]) # How we process the file depends on where the output is going. if self.options.sOutputName: self.processFile(sFile, self.options.sOutputName, sFile) elif self.options.bReplace or self.options.bCheck: # We want to replace the cog file with the output, # but only if they differ. verb = "Cogging" if self.options.bReplace else "Checking" if self.options.verbosity >= 2: self.prout(f"{verb} {sFile}", end="") bNeedNewline = True try: fOldFile = self.openInputFile(sFile) sOldText = fOldFile.read() fOldFile.close() sNewText = self.processString(sOldText, fname=sFile) if sOldText != sNewText: if self.options.verbosity >= 1: if self.options.verbosity < 2: self.prout(f"{verb} {sFile}", end="") self.prout(" (changed)") bNeedNewline = False if self.options.bReplace: self.replaceFile(sFile, sNewText) else: assert self.options.bCheck self.bCheckFailed = True finally: # The try-finally block is so we can print a partial line # with the name of the file, and print (changed) on the # same line, but also make sure to break the line before # any traceback. if bNeedNewline: self.prout("") else: self.processFile(sFile, self.stdout, sFile) finally: self.restoreIncludePath() def processWildcards(self, sFile): files = glob.glob(sFile) if files: for sMatchingFile in files: self.processOneFile(sMatchingFile) else: self.processOneFile(sFile) def processFileList(self, sFileList): """ Process the files in a file list. """ flist = self.openInputFile(sFileList) lines = flist.readlines() flist.close() for l in lines: # Use shlex to parse the line like a shell. lex = shlex.shlex(l, posix=True) lex.whitespace_split = True lex.commenters = '#' # No escapes, so that backslash can be part of the path lex.escape = '' args = list(lex) if args: self.processArguments(args) def processArguments(self, args): """ Process one command-line. """ saved_options = self.options self.options = self.options.clone() self.options.parseArgs(args[1:]) self.options.validate() if args[0][0] == '@': if self.options.sOutputName: raise CogUsageError("Can't use -o with @file") self.processFileList(args[0][1:]) elif args[0][0] == '&': if self.options.sOutputName: raise CogUsageError("Can't use -o with &file") file_list = args[0][1:] with change_dir(os.path.dirname(file_list)): self.processFileList(os.path.basename(file_list)) else: self.processWildcards(args[0]) self.options = saved_options def callableMain(self, argv): """ All of command-line cog, but in a callable form. This is used by main. argv is the equivalent of sys.argv. """ argv = argv[1:] # Provide help if asked for anywhere in the command line. if '-?' in argv or '-h' in argv: self.prerr(usage, end="") return self.options.parseArgs(argv) self.options.validate() self._fixEndOutputPatterns() if self.options.bShowVersion: self.prout(f"Cog version {__version__}") return if self.options.args: for a in self.options.args: self.processArguments([a]) else: raise CogUsageError("No files to process") if self.bCheckFailed: raise CogCheckFailed("Check failed") def main(self, argv): """ Handle the command-line execution for cog. """ try: self.callableMain(argv) return 0 except CogUsageError as err: self.prerr(err) self.prerr("(for help use -h)") return 2 except CogGeneratedError as err: self.prerr(f"Error: {err}") return 3 except CogUserException as err: self.prerr("Traceback (most recent call last):") self.prerr(err.args[0]) return 4 except CogCheckFailed as err: self.prerr(err) return 5 except CogError as err: self.prerr(err) return 1 def find_cog_source(frame_summary, prologue): """Find cog source lines in a frame summary list, for printing tracebacks. Arguments: frame_summary: a list of 4-item tuples, as returned by traceback.extract_tb. prologue: the text of the code prologue. Returns A list of 4-item tuples, updated to correct the cog entries. """ prolines = prologue.splitlines() for filename, lineno, funcname, source in frame_summary: if not source: m = re.search(r"^$", filename) if m: if lineno <= len(prolines): filename = '' source = prolines[lineno-1] lineno -= 1 # Because "import cog" is the first line in the prologue else: filename, coglineno = m.groups() coglineno = int(coglineno) lineno += coglineno - len(prolines) source = linecache.getline(filename, lineno).strip() yield filename, lineno, funcname, source def main(): """Main function for entry_points to use.""" return Cog().main(sys.argv) cog-3.4.1/cogapp/makefiles.py000066400000000000000000000021351457240776200160710ustar00rootroot00000000000000""" Dictionary-to-filetree functions, to create test files for testing. """ import os.path from .whiteutils import reindentBlock def makeFiles(d, basedir='.'): """ Create files from the dictionary `d`, in the directory named by `basedir`. """ for name, contents in d.items(): child = os.path.join(basedir, name) if isinstance(contents, (bytes, str)): mode = "w" if isinstance(contents, bytes): mode += "b" with open(child, mode) as f: f.write(reindentBlock(contents)) else: if not os.path.exists(child): os.mkdir(child) makeFiles(contents, child) def removeFiles(d, basedir='.'): """ Remove the files created by makeFiles. Directories are removed if they are empty. """ for name, contents in d.items(): child = os.path.join(basedir, name) if isinstance(contents, (bytes, str)): os.remove(child) else: removeFiles(contents, child) if not os.listdir(child): os.rmdir(child) cog-3.4.1/cogapp/test_cogapp.py000066400000000000000000002423321457240776200164460ustar00rootroot00000000000000""" Test cogapp. """ import io import os import os.path import random import re import shutil import stat import sys import tempfile import threading from unittest import TestCase from .cogapp import Cog, CogOptions, CogGenerator from .cogapp import CogError, CogUsageError, CogGeneratedError, CogUserException from .cogapp import usage, __version__, main from .makefiles import makeFiles from .whiteutils import reindentBlock class CogTestsInMemory(TestCase): """ Test cases for cogapp.Cog() """ def testNoCog(self): strings = [ '', ' ', ' \t \t \tx', 'hello', 'the cat\nin the\nhat.', 'Horton\n\tHears A\n\t\tWho' ] for s in strings: self.assertEqual(Cog().processString(s), s) def testSimple(self): infile = """\ Some text. //[[[cog import cog cog.outl("This is line one\\n") cog.outl("This is line two") //]]] gobbledegook. //[[[end]]] epilogue. """ outfile = """\ Some text. //[[[cog import cog cog.outl("This is line one\\n") cog.outl("This is line two") //]]] This is line one This is line two //[[[end]]] epilogue. """ self.assertEqual(Cog().processString(infile), outfile) def testEmptyCog(self): # The cog clause can be totally empty. Not sure why you'd want it, # but it works. infile = """\ hello //[[[cog //]]] //[[[end]]] goodbye """ infile = reindentBlock(infile) self.assertEqual(Cog().processString(infile), infile) def testMultipleCogs(self): # One file can have many cog chunks, even abutting each other. infile = """\ //[[[cog cog.out("chunk1") //]]] chunk1 //[[[end]]] //[[[cog cog.out("chunk2") //]]] chunk2 //[[[end]]] between chunks //[[[cog cog.out("chunk3") //]]] chunk3 //[[[end]]] """ infile = reindentBlock(infile) self.assertEqual(Cog().processString(infile), infile) def testTrimBlankLines(self): infile = """\ //[[[cog cog.out("This is line one\\n", trimblanklines=True) cog.out(''' This is line two ''', dedent=True, trimblanklines=True) cog.outl("This is line three", trimblanklines=True) //]]] This is line one This is line two This is line three //[[[end]]] """ infile = reindentBlock(infile) self.assertEqual(Cog().processString(infile), infile) def testTrimEmptyBlankLines(self): infile = """\ //[[[cog cog.out("This is line one\\n", trimblanklines=True) cog.out(''' This is line two ''', dedent=True, trimblanklines=True) cog.out('', dedent=True, trimblanklines=True) cog.outl("This is line three", trimblanklines=True) //]]] This is line one This is line two This is line three //[[[end]]] """ infile = reindentBlock(infile) self.assertEqual(Cog().processString(infile), infile) def testTrimBlankLinesWithLastPartial(self): infile = """\ //[[[cog cog.out("This is line one\\n", trimblanklines=True) cog.out("\\nLine two\\nLine three", trimblanklines=True) //]]] This is line one Line two Line three //[[[end]]] """ infile = reindentBlock(infile) self.assertEqual(Cog().processString(infile), infile) def testCogOutDedent(self): infile = """\ //[[[cog cog.out("This is the first line\\n") cog.out(''' This is dedent=True 1 This is dedent=True 2 ''', dedent=True, trimblanklines=True) cog.out(''' This is dedent=False 1 This is dedent=False 2 ''', dedent=False, trimblanklines=True) cog.out(''' This is dedent=default 1 This is dedent=default 2 ''', trimblanklines=True) cog.out("This is the last line\\n") //]]] This is the first line This is dedent=True 1 This is dedent=True 2 This is dedent=False 1 This is dedent=False 2 This is dedent=default 1 This is dedent=default 2 This is the last line //[[[end]]] """ infile = reindentBlock(infile) self.assertEqual(Cog().processString(infile), infile) def test22EndOfLine(self): # In Python 2.2, this cog file was not parsing because the # last line is indented but didn't end with a newline. infile = """\ //[[[cog import cog for i in range(3): cog.out("%d\\n" % i) //]]] 0 1 2 //[[[end]]] """ infile = reindentBlock(infile) self.assertEqual(Cog().processString(infile), infile) def testIndentedCode(self): infile = """\ first line [[[cog import cog for i in range(3): cog.out("xx%d\\n" % i) ]]] xx0 xx1 xx2 [[[end]]] last line """ infile = reindentBlock(infile) self.assertEqual(Cog().processString(infile), infile) def testPrefixedCode(self): infile = """\ --[[[cog --import cog --for i in range(3): -- cog.out("xx%d\\n" % i) --]]] xx0 xx1 xx2 --[[[end]]] """ infile = reindentBlock(infile) self.assertEqual(Cog().processString(infile), infile) def testPrefixedIndentedCode(self): infile = """\ prologue --[[[cog -- import cog -- for i in range(3): -- cog.out("xy%d\\n" % i) --]]] xy0 xy1 xy2 --[[[end]]] """ infile = reindentBlock(infile) self.assertEqual(Cog().processString(infile), infile) def testBogusPrefixMatch(self): infile = """\ prologue #[[[cog import cog # This comment should not be clobbered by removing the pound sign. for i in range(3): cog.out("xy%d\\n" % i) #]]] xy0 xy1 xy2 #[[[end]]] """ infile = reindentBlock(infile) self.assertEqual(Cog().processString(infile), infile) def testNoFinalNewline(self): # If the cog'ed output has no final newline, # it shouldn't eat up the cog terminator. infile = """\ prologue [[[cog import cog for i in range(3): cog.out("%d" % i) ]]] 012 [[[end]]] epilogue """ infile = reindentBlock(infile) self.assertEqual(Cog().processString(infile), infile) def testNoOutputAtAll(self): # If there is absolutely no cog output, that's ok. infile = """\ prologue [[[cog i = 1 ]]] [[[end]]] epilogue """ infile = reindentBlock(infile) self.assertEqual(Cog().processString(infile), infile) def testPurelyBlankLine(self): # If there is a blank line in the cog code with no whitespace # prefix, that should be OK. infile = """\ prologue [[[cog import sys cog.out("Hello") $ cog.out("There") ]]] HelloThere [[[end]]] epilogue """ infile = reindentBlock(infile.replace('$', '')) self.assertEqual(Cog().processString(infile), infile) def testEmptyOutl(self): # Alexander Belchenko suggested the string argument to outl should # be optional. Does it work? infile = """\ prologue [[[cog cog.outl("x") cog.outl() cog.outl("y") cog.out() # Also optional, a complete no-op. cog.outl(trimblanklines=True) cog.outl("z") ]]] x y z [[[end]]] epilogue """ infile = reindentBlock(infile) self.assertEqual(Cog().processString(infile), infile) def testFirstLineNum(self): infile = """\ fooey [[[cog cog.outl("started at line number %d" % cog.firstLineNum) ]]] started at line number 2 [[[end]]] blah blah [[[cog cog.outl("and again at line %d" % cog.firstLineNum) ]]] and again at line 8 [[[end]]] """ infile = reindentBlock(infile) self.assertEqual(Cog().processString(infile), infile) def testCompactOneLineCode(self): infile = """\ first line hey: [[[cog cog.outl("hello %d" % (3*3*3*3)) ]]] looky! get rid of this! [[[end]]] last line """ outfile = """\ first line hey: [[[cog cog.outl("hello %d" % (3*3*3*3)) ]]] looky! hello 81 [[[end]]] last line """ infile = reindentBlock(infile) self.assertEqual(Cog().processString(infile), reindentBlock(outfile)) def testInsideOutCompact(self): infile = """\ first line hey?: ]]] what is this? [[[cog strange! get rid of this! [[[end]]] last line """ with self.assertRaisesRegex(CogError, r"^infile.txt\(2\): Cog code markers inverted$"): Cog().processString(reindentBlock(infile), "infile.txt") def testSharingGlobals(self): infile = """\ first line hey: [[[cog s="hey there" ]]] looky! [[[end]]] more literal junk. [[[cog cog.outl(s) ]]] [[[end]]] last line """ outfile = """\ first line hey: [[[cog s="hey there" ]]] looky! [[[end]]] more literal junk. [[[cog cog.outl(s) ]]] hey there [[[end]]] last line """ infile = reindentBlock(infile) self.assertEqual(Cog().processString(infile), reindentBlock(outfile)) def testAssertInCogCode(self): # Check that we can test assertions in cog code in the test framework. infile = """\ [[[cog assert 1 == 2, "Oops" ]]] [[[end]]] """ infile = reindentBlock(infile) with self.assertRaisesRegex(CogUserException, "AssertionError: Oops"): Cog().processString(infile) def testCogPrevious(self): # Check that we can access the previous run's output. infile = """\ [[[cog assert cog.previous == "Hello there!\\n", "WTF??" cog.out(cog.previous) cog.outl("Ran again!") ]]] Hello there! [[[end]]] """ outfile = """\ [[[cog assert cog.previous == "Hello there!\\n", "WTF??" cog.out(cog.previous) cog.outl("Ran again!") ]]] Hello there! Ran again! [[[end]]] """ infile = reindentBlock(infile) self.assertEqual(Cog().processString(infile), reindentBlock(outfile)) class CogOptionsTests(TestCase): """ Test the CogOptions class. """ def testEquality(self): o = CogOptions() p = CogOptions() self.assertEqual(o, p) o.parseArgs(['-r']) self.assertNotEqual(o, p) p.parseArgs(['-r']) self.assertEqual(o, p) def testCloning(self): o = CogOptions() o.parseArgs(['-I', 'fooey', '-I', 'booey', '-s', ' /*x*/']) p = o.clone() self.assertEqual(o, p) p.parseArgs(['-I', 'huey', '-D', 'foo=quux']) self.assertNotEqual(o, p) q = CogOptions() q.parseArgs(['-I', 'fooey', '-I', 'booey', '-s', ' /*x*/', '-I', 'huey', '-D', 'foo=quux']) self.assertEqual(p, q) def testCombiningFlags(self): # Single-character flags can be combined. o = CogOptions() o.parseArgs(['-e', '-r', '-z']) p = CogOptions() p.parseArgs(['-erz']) self.assertEqual(o, p) def testMarkers(self): o = CogOptions() o._parse_markers('a b c') self.assertEqual('a', o.sBeginSpec) self.assertEqual('b', o.sEndSpec) self.assertEqual('c', o.sEndOutput) def testMarkersSwitch(self): o = CogOptions() o.parseArgs(['--markers', 'a b c']) self.assertEqual('a', o.sBeginSpec) self.assertEqual('b', o.sEndSpec) self.assertEqual('c', o.sEndOutput) class FileStructureTests(TestCase): """ Test cases to check that we're properly strict about the structure of files. """ def isBad(self, infile, msg=None): infile = reindentBlock(infile) with self.assertRaisesRegex(CogError, "^"+re.escape(msg)+"$"): Cog().processString(infile, 'infile.txt') def testBeginNoEnd(self): infile = """\ Fooey #[[[cog cog.outl('hello') """ self.isBad(infile, "infile.txt(2): Cog block begun but never ended.") def testNoEoo(self): infile = """\ Fooey #[[[cog cog.outl('hello') #]]] """ self.isBad(infile, "infile.txt(4): Missing '[[[end]]]' before end of file.") infile2 = """\ Fooey #[[[cog cog.outl('hello') #]]] #[[[cog cog.outl('goodbye') #]]] """ self.isBad(infile2, "infile.txt(5): Unexpected '[[[cog'") def testStartWithEnd(self): infile = """\ #]]] """ self.isBad(infile, "infile.txt(1): Unexpected ']]]'") infile2 = """\ #[[[cog cog.outl('hello') #]]] #[[[end]]] #]]] """ self.isBad(infile2, "infile.txt(5): Unexpected ']]]'") def testStartWithEoo(self): infile = """\ #[[[end]]] """ self.isBad(infile, "infile.txt(1): Unexpected '[[[end]]]'") infile2 = """\ #[[[cog cog.outl('hello') #]]] #[[[end]]] #[[[end]]] """ self.isBad(infile2, "infile.txt(5): Unexpected '[[[end]]]'") def testNoEnd(self): infile = """\ #[[[cog cog.outl("hello") #[[[end]]] """ self.isBad(infile, "infile.txt(3): Unexpected '[[[end]]]'") infile2 = """\ #[[[cog cog.outl('hello') #]]] #[[[end]]] #[[[cog cog.outl("hello") #[[[end]]] """ self.isBad(infile2, "infile.txt(7): Unexpected '[[[end]]]'") def testTwoBegins(self): infile = """\ #[[[cog #[[[cog cog.outl("hello") #]]] #[[[end]]] """ self.isBad(infile, "infile.txt(2): Unexpected '[[[cog'") infile2 = """\ #[[[cog cog.outl("hello") #]]] #[[[end]]] #[[[cog #[[[cog cog.outl("hello") #]]] #[[[end]]] """ self.isBad(infile2, "infile.txt(6): Unexpected '[[[cog'") def testTwoEnds(self): infile = """\ #[[[cog cog.outl("hello") #]]] #]]] #[[[end]]] """ self.isBad(infile, "infile.txt(4): Unexpected ']]]'") infile2 = """\ #[[[cog cog.outl("hello") #]]] #[[[end]]] #[[[cog cog.outl("hello") #]]] #]]] #[[[end]]] """ self.isBad(infile2, "infile.txt(8): Unexpected ']]]'") class CogErrorTests(TestCase): """ Test cases for cog.error(). """ def testErrorMsg(self): infile = """\ [[[cog cog.error("This ain't right!")]]] [[[end]]] """ infile = reindentBlock(infile) with self.assertRaisesRegex(CogGeneratedError, "^This ain't right!$"): Cog().processString(infile) def testErrorNoMsg(self): infile = """\ [[[cog cog.error()]]] [[[end]]] """ infile = reindentBlock(infile) with self.assertRaisesRegex(CogGeneratedError, "^Error raised by cog generator.$"): Cog().processString(infile) def testNoErrorIfErrorNotCalled(self): infile = """\ --[[[cog --import cog --for i in range(3): -- if i > 10: -- cog.error("Something is amiss!") -- cog.out("xx%d\\n" % i) --]]] xx0 xx1 xx2 --[[[end]]] """ infile = reindentBlock(infile) self.assertEqual(Cog().processString(infile), infile) class CogGeneratorGetCodeTests(TestCase): """ Unit tests against CogGenerator to see if its getCode() method works properly. """ def setUp(self): """ All tests get a generator to use, and short same-length names for the functions we're going to use. """ self.gen = CogGenerator() self.m = self.gen.parseMarker self.l = self.gen.parseLine def testEmpty(self): self.m('// [[[cog') self.m('// ]]]') self.assertEqual(self.gen.getCode(), '') def testSimple(self): self.m('// [[[cog') self.l(' print "hello"') self.l(' print "bye"') self.m('// ]]]') self.assertEqual(self.gen.getCode(), 'print "hello"\nprint "bye"') def testCompressed1(self): # For a while, I supported compressed code blocks, but no longer. self.m('// [[[cog: print """') self.l('// hello') self.l('// bye') self.m('// """)]]]') self.assertEqual(self.gen.getCode(), 'hello\nbye') def testCompressed2(self): # For a while, I supported compressed code blocks, but no longer. self.m('// [[[cog: print """') self.l('hello') self.l('bye') self.m('// """)]]]') self.assertEqual(self.gen.getCode(), 'hello\nbye') def testCompressed3(self): # For a while, I supported compressed code blocks, but no longer. self.m('// [[[cog') self.l('print """hello') self.l('bye') self.m('// """)]]]') self.assertEqual(self.gen.getCode(), 'print """hello\nbye') def testCompressed4(self): # For a while, I supported compressed code blocks, but no longer. self.m('// [[[cog: print """') self.l('hello') self.l('bye""")') self.m('// ]]]') self.assertEqual(self.gen.getCode(), 'hello\nbye""")') def testNoCommonPrefixForMarkers(self): # It's important to be able to use #if 0 to hide lines from a # C++ compiler. self.m('#if 0 //[[[cog') self.l('\timport cog, sys') self.l('') self.l('\tprint sys.argv') self.m('#endif //]]]') self.assertEqual(self.gen.getCode(), 'import cog, sys\n\nprint sys.argv') class TestCaseWithTempDir(TestCase): def newCog(self): """ Initialize the cog members for another run. """ # Create a cog engine, and catch its output. self.cog = Cog() self.output = io.StringIO() self.cog.setOutput(stdout=self.output, stderr=self.output) def setUp(self): # Create a temporary directory. self.tempdir = os.path.join(tempfile.gettempdir(), 'testcog_tempdir_' + str(random.random())[2:]) os.mkdir(self.tempdir) self.olddir = os.getcwd() os.chdir(self.tempdir) self.newCog() def tearDown(self): os.chdir(self.olddir) # Get rid of the temporary directory. shutil.rmtree(self.tempdir) def assertFilesSame(self, sFName1, sFName2): with open(os.path.join(self.tempdir, sFName1), 'rb') as f1: text1 = f1.read() with open(os.path.join(self.tempdir, sFName2), 'rb') as f2: text2 = f2.read() self.assertEqual(text1, text2) def assertFileContent(self, fname, content): absname = os.path.join(self.tempdir, fname) with open(absname, 'rb') as f: file_content = f.read() self.assertEqual(file_content, content.encode("utf-8")) class ArgumentHandlingTests(TestCaseWithTempDir): def testArgumentFailure(self): # Return value 2 means usage problem. self.assertEqual(self.cog.main(['argv0', '-j']), 2) output = self.output.getvalue() self.assertIn("option -j not recognized", output) with self.assertRaisesRegex(CogUsageError, r"^No files to process$"): self.cog.callableMain(['argv0']) with self.assertRaisesRegex(CogUsageError, r"^option -j not recognized$"): self.cog.callableMain(['argv0', '-j']) def testNoDashOAndAtFile(self): makeFiles({"cogfiles.txt": "# Please run cog"}) with self.assertRaisesRegex(CogUsageError, r"^Can't use -o with @file$"): self.cog.callableMain(['argv0', '-o', 'foo', '@cogfiles.txt']) def testNoDashOAndAmpFile(self): makeFiles({"cogfiles.txt": "# Please run cog"}) with self.assertRaisesRegex(CogUsageError, r"^Can't use -o with &file$"): self.cog.callableMain(['argv0', '-o', 'foo', '&cogfiles.txt']) def testDashV(self): self.assertEqual(self.cog.main(['argv0', '-v']), 0) output = self.output.getvalue() self.assertEqual('Cog version %s\n' % __version__, output) def producesHelp(self, args): self.newCog() argv = ['argv0'] + args.split() self.assertEqual(self.cog.main(argv), 0) self.assertEqual(usage, self.output.getvalue()) def testDashH(self): # -h or -? anywhere on the command line should just print help. self.producesHelp("-h") self.producesHelp("-?") self.producesHelp("fooey.txt -h") self.producesHelp("-o -r @fooey.txt -? @booey.txt") def testDashOAndDashR(self): d = { 'cogfile.txt': """\ # Please run cog """ } makeFiles(d) with self.assertRaisesRegex(CogUsageError, r"^Can't use -o with -r \(they are opposites\)$"): self.cog.callableMain(['argv0', '-o', 'foo', '-r', 'cogfile.txt']) def testDashZ(self): d = { 'test.cog': """\ // This is my C++ file. //[[[cog fnames = ['DoSomething', 'DoAnotherThing', 'DoLastThing'] for fn in fnames: cog.outl("void %s();" % fn) //]]] """, 'test.out': """\ // This is my C++ file. //[[[cog fnames = ['DoSomething', 'DoAnotherThing', 'DoLastThing'] for fn in fnames: cog.outl("void %s();" % fn) //]]] void DoSomething(); void DoAnotherThing(); void DoLastThing(); """, } makeFiles(d) with self.assertRaisesRegex(CogError, r"^test.cog\(6\): Missing '\[\[\[end\]\]\]' before end of file.$"): self.cog.callableMain(['argv0', '-r', 'test.cog']) self.newCog() self.cog.callableMain(['argv0', '-r', '-z', 'test.cog']) self.assertFilesSame('test.cog', 'test.out') def testBadDashD(self): with self.assertRaisesRegex(CogUsageError, r"^-D takes a name=value argument$"): self.cog.callableMain(['argv0', '-Dfooey', 'cog.txt']) with self.assertRaisesRegex(CogUsageError, r"^-D takes a name=value argument$"): self.cog.callableMain(['argv0', '-D', 'fooey', 'cog.txt']) def testBadMarkers(self): with self.assertRaisesRegex(CogUsageError, r"^--markers requires 3 values separated by spaces, could not parse 'X'$"): self.cog.callableMain(['argv0', '--markers=X']) with self.assertRaisesRegex(CogUsageError, r"^--markers requires 3 values separated by spaces, could not parse 'A B C D'$"): self.cog.callableMain(['argv0', '--markers=A B C D']) class TestMain(TestCaseWithTempDir): def setUp(self): super().setUp() self.old_argv = sys.argv[:] self.old_stderr = sys.stderr sys.stderr = io.StringIO() def tearDown(self): sys.stderr = self.old_stderr sys.argv = self.old_argv sys.modules.pop('mycode', None) super().tearDown() def test_main_function(self): sys.argv = ["argv0", "-Z"] ret = main() self.assertEqual(ret, 2) stderr = sys.stderr.getvalue() self.assertEqual(stderr, 'option -Z not recognized\n(for help use -h)\n') files = { 'test.cog': """\ //[[[cog def func(): import mycode mycode.boom() //]]] //[[[end]]] ----- //[[[cog func() //]]] //[[[end]]] """, 'mycode.py': """\ def boom(): [][0] """, } def test_error_report(self): self.check_error_report() def test_error_report_with_prologue(self): self.check_error_report("-p", "#1\n#2") def check_error_report(self, *args): """Check that the error report is right.""" makeFiles(self.files) sys.argv = ["argv0"] + list(args) + ["-r", "test.cog"] main() expected = reindentBlock("""\ Traceback (most recent call last): File "test.cog", line 9, in func() File "test.cog", line 4, in func mycode.boom() File "MYCODE", line 2, in boom [][0] IndexError: list index out of range """) expected = expected.replace("MYCODE", os.path.abspath("mycode.py")) assert expected == sys.stderr.getvalue() def test_error_in_prologue(self): makeFiles(self.files) sys.argv = ["argv0", "-p", "import mycode; mycode.boom()", "-r", "test.cog"] main() expected = reindentBlock("""\ Traceback (most recent call last): File "", line 1, in import mycode; mycode.boom() File "MYCODE", line 2, in boom [][0] IndexError: list index out of range """) expected = expected.replace("MYCODE", os.path.abspath("mycode.py")) assert expected == sys.stderr.getvalue() class TestFileHandling(TestCaseWithTempDir): def testSimple(self): d = { 'test.cog': """\ // This is my C++ file. //[[[cog fnames = ['DoSomething', 'DoAnotherThing', 'DoLastThing'] for fn in fnames: cog.outl("void %s();" % fn) //]]] //[[[end]]] """, 'test.out': """\ // This is my C++ file. //[[[cog fnames = ['DoSomething', 'DoAnotherThing', 'DoLastThing'] for fn in fnames: cog.outl("void %s();" % fn) //]]] void DoSomething(); void DoAnotherThing(); void DoLastThing(); //[[[end]]] """, } makeFiles(d) self.cog.callableMain(['argv0', '-r', 'test.cog']) self.assertFilesSame('test.cog', 'test.out') output = self.output.getvalue() self.assertIn("(changed)", output) def testPrintOutput(self): d = { 'test.cog': """\ // This is my C++ file. //[[[cog fnames = ['DoSomething', 'DoAnotherThing', 'DoLastThing'] for fn in fnames: print("void %s();" % fn) //]]] //[[[end]]] """, 'test.out': """\ // This is my C++ file. //[[[cog fnames = ['DoSomething', 'DoAnotherThing', 'DoLastThing'] for fn in fnames: print("void %s();" % fn) //]]] void DoSomething(); void DoAnotherThing(); void DoLastThing(); //[[[end]]] """, } makeFiles(d) self.cog.callableMain(['argv0', '-rP', 'test.cog']) self.assertFilesSame('test.cog', 'test.out') output = self.output.getvalue() self.assertIn("(changed)", output) def testWildcards(self): d = { 'test.cog': """\ // This is my C++ file. //[[[cog fnames = ['DoSomething', 'DoAnotherThing', 'DoLastThing'] for fn in fnames: cog.outl("void %s();" % fn) //]]] //[[[end]]] """, 'test2.cog': """\ // This is my C++ file. //[[[cog fnames = ['DoSomething', 'DoAnotherThing', 'DoLastThing'] for fn in fnames: cog.outl("void %s();" % fn) //]]] //[[[end]]] """, 'test.out': """\ // This is my C++ file. //[[[cog fnames = ['DoSomething', 'DoAnotherThing', 'DoLastThing'] for fn in fnames: cog.outl("void %s();" % fn) //]]] void DoSomething(); void DoAnotherThing(); void DoLastThing(); //[[[end]]] """, 'not_this_one.cog': """\ // This is my C++ file. //[[[cog fnames = ['DoSomething', 'DoAnotherThing', 'DoLastThing'] for fn in fnames: cog.outl("void %s();" % fn) //]]] //[[[end]]] """, 'not_this_one.out': """\ // This is my C++ file. //[[[cog fnames = ['DoSomething', 'DoAnotherThing', 'DoLastThing'] for fn in fnames: cog.outl("void %s();" % fn) //]]] //[[[end]]] """, } makeFiles(d) self.cog.callableMain(['argv0', '-r', 't*.cog']) self.assertFilesSame('test.cog', 'test.out') self.assertFilesSame('test2.cog', 'test.out') self.assertFilesSame('not_this_one.cog', 'not_this_one.out') output = self.output.getvalue() self.assertIn("(changed)", output) def testOutputFile(self): # -o sets the output file. d = { 'test.cog': """\ // This is my C++ file. //[[[cog fnames = ['DoSomething', 'DoAnotherThing', 'DoLastThing'] for fn in fnames: cog.outl("void %s();" % fn) //]]] //[[[end]]] """, 'test.out': """\ // This is my C++ file. //[[[cog fnames = ['DoSomething', 'DoAnotherThing', 'DoLastThing'] for fn in fnames: cog.outl("void %s();" % fn) //]]] void DoSomething(); void DoAnotherThing(); void DoLastThing(); //[[[end]]] """, } makeFiles(d) self.cog.callableMain(['argv0', '-o', 'in/a/dir/test.cogged', 'test.cog']) self.assertFilesSame('in/a/dir/test.cogged', 'test.out') def testAtFile(self): d = { 'one.cog': """\ //[[[cog cog.outl("hello world") //]]] //[[[end]]] """, 'one.out': """\ //[[[cog cog.outl("hello world") //]]] hello world //[[[end]]] """, 'two.cog': """\ //[[[cog cog.outl("goodbye cruel world") //]]] //[[[end]]] """, 'two.out': """\ //[[[cog cog.outl("goodbye cruel world") //]]] goodbye cruel world //[[[end]]] """, 'cogfiles.txt': """\ # Please run cog one.cog two.cog """ } makeFiles(d) self.cog.callableMain(['argv0', '-r', '@cogfiles.txt']) self.assertFilesSame('one.cog', 'one.out') self.assertFilesSame('two.cog', 'two.out') output = self.output.getvalue() self.assertIn("(changed)", output) def testNestedAtFile(self): d = { 'one.cog': """\ //[[[cog cog.outl("hello world") //]]] //[[[end]]] """, 'one.out': """\ //[[[cog cog.outl("hello world") //]]] hello world //[[[end]]] """, 'two.cog': """\ //[[[cog cog.outl("goodbye cruel world") //]]] //[[[end]]] """, 'two.out': """\ //[[[cog cog.outl("goodbye cruel world") //]]] goodbye cruel world //[[[end]]] """, 'cogfiles.txt': """\ # Please run cog one.cog @cogfiles2.txt """, 'cogfiles2.txt': """\ # This one too, please. two.cog """, } makeFiles(d) self.cog.callableMain(['argv0', '-r', '@cogfiles.txt']) self.assertFilesSame('one.cog', 'one.out') self.assertFilesSame('two.cog', 'two.out') output = self.output.getvalue() self.assertIn("(changed)", output) def testAtFileWithArgs(self): d = { 'both.cog': """\ //[[[cog cog.outl("one: %s" % ('one' in globals())) cog.outl("two: %s" % ('two' in globals())) //]]] //[[[end]]] """, 'one.out': """\ //[[[cog cog.outl("one: %s" % ('one' in globals())) cog.outl("two: %s" % ('two' in globals())) //]]] one: True // ONE two: False // ONE //[[[end]]] """, 'two.out': """\ //[[[cog cog.outl("one: %s" % ('one' in globals())) cog.outl("two: %s" % ('two' in globals())) //]]] one: False // TWO two: True // TWO //[[[end]]] """, 'cogfiles.txt': """\ # Please run cog both.cog -o in/a/dir/both.one -s ' // ONE' -D one=x both.cog -o in/a/dir/both.two -s ' // TWO' -D two=x """ } makeFiles(d) self.cog.callableMain(['argv0', '@cogfiles.txt']) self.assertFilesSame('in/a/dir/both.one', 'one.out') self.assertFilesSame('in/a/dir/both.two', 'two.out') def testAtFileWithBadArgCombo(self): d = { 'both.cog': """\ //[[[cog cog.outl("one: %s" % ('one' in globals())) cog.outl("two: %s" % ('two' in globals())) //]]] //[[[end]]] """, 'cogfiles.txt': """\ # Please run cog both.cog both.cog -d # This is bad: -r and -d """ } makeFiles(d) with self.assertRaisesRegex(CogUsageError, r"^Can't use -d with -r \(or you would delete all your source!\)$"): self.cog.callableMain(['argv0', '-r', '@cogfiles.txt']) def testAtFileWithTrickyFilenames(self): def fix_backslashes(files_txt): """Make the contents of a files.txt sensitive to the platform.""" if sys.platform != "win32": files_txt = files_txt.replace("\\", "/") return files_txt d = { 'one 1.cog': """\ //[[[cog cog.outl("hello world") ]]] """, 'one.out': """\ //[[[cog cog.outl("hello world") ]]] hello world //xxx """, 'subdir': { 'subback.cog': """\ //[[[cog cog.outl("down deep with backslashes") ]]] """, 'subfwd.cog': """\ //[[[cog cog.outl("down deep with slashes") ]]] """, }, 'subback.out': """\ //[[[cog cog.outl("down deep with backslashes") ]]] down deep with backslashes //yyy """, 'subfwd.out': """\ //[[[cog cog.outl("down deep with slashes") ]]] down deep with slashes //zzz """, 'cogfiles.txt': fix_backslashes("""\ # Please run cog 'one 1.cog' -s ' //xxx' subdir\\subback.cog -s ' //yyy' subdir/subfwd.cog -s ' //zzz' """) } makeFiles(d) self.cog.callableMain(['argv0', '-z', '-r', '@cogfiles.txt']) self.assertFilesSame('one 1.cog', 'one.out') self.assertFilesSame('subdir/subback.cog', 'subback.out') self.assertFilesSame('subdir/subfwd.cog', 'subfwd.out') def testAmpFile(self): d = { 'code': { 'files_to_cog': """\ # A locally resolved file name. test.cog """, 'test.cog': """\ //[[[cog import myampsubmodule //]]] //[[[end]]] """, 'test.out': """\ //[[[cog import myampsubmodule //]]] Hello from myampsubmodule //[[[end]]] """, 'myampsubmodule.py': """\ import cog cog.outl("Hello from myampsubmodule") """ } } makeFiles(d) print(os.path.abspath("code/test.out")) self.cog.callableMain(['argv0', '-r', '&code/files_to_cog']) self.assertFilesSame('code/test.cog', 'code/test.out') def run_with_verbosity(self, verbosity): d = { 'unchanged.cog': """\ //[[[cog cog.outl("hello world") //]]] hello world //[[[end]]] """, 'changed.cog': """\ //[[[cog cog.outl("goodbye cruel world") //]]] //[[[end]]] """, 'cogfiles.txt': """\ unchanged.cog changed.cog """ } makeFiles(d) self.cog.callableMain(['argv0', '-r', '--verbosity='+verbosity, '@cogfiles.txt']) output = self.output.getvalue() return output def test_verbosity0(self): output = self.run_with_verbosity("0") self.assertEqual(output, "") def test_verbosity1(self): output = self.run_with_verbosity("1") self.assertEqual(output, "Cogging changed.cog (changed)\n") def test_verbosity2(self): output = self.run_with_verbosity("2") self.assertEqual(output, "Cogging unchanged.cog\nCogging changed.cog (changed)\n") class CogTestLineEndings(TestCaseWithTempDir): """Tests for -U option (force LF line-endings in output).""" lines_in = ['Some text.', '//[[[cog', 'cog.outl("Cog text")', '//]]]', 'gobbledegook.', '//[[[end]]]', 'epilogue.', ''] lines_out = ['Some text.', '//[[[cog', 'cog.outl("Cog text")', '//]]]', 'Cog text', '//[[[end]]]', 'epilogue.', ''] def testOutputNativeEol(self): makeFiles({'infile': '\n'.join(self.lines_in)}) self.cog.callableMain(['argv0', '-o', 'outfile', 'infile']) self.assertFileContent('outfile', os.linesep.join(self.lines_out)) def testOutputLfEol(self): makeFiles({'infile': '\n'.join(self.lines_in)}) self.cog.callableMain(['argv0', '-U', '-o', 'outfile', 'infile']) self.assertFileContent('outfile', '\n'.join(self.lines_out)) def testReplaceNativeEol(self): makeFiles({'test.cog': '\n'.join(self.lines_in)}) self.cog.callableMain(['argv0', '-r', 'test.cog']) self.assertFileContent('test.cog', os.linesep.join(self.lines_out)) def testReplaceLfEol(self): makeFiles({'test.cog': '\n'.join(self.lines_in)}) self.cog.callableMain(['argv0', '-U', '-r', 'test.cog']) self.assertFileContent('test.cog', '\n'.join(self.lines_out)) class CogTestCharacterEncoding(TestCaseWithTempDir): def testSimple(self): d = { 'test.cog': b"""\ // This is my C++ file. //[[[cog cog.outl("// Unicode: \xe1\x88\xb4 (U+1234)") //]]] //[[[end]]] """, 'test.out': b"""\ // This is my C++ file. //[[[cog cog.outl("// Unicode: \xe1\x88\xb4 (U+1234)") //]]] // Unicode: \xe1\x88\xb4 (U+1234) //[[[end]]] """.replace(b"\n", os.linesep.encode()), } makeFiles(d) self.cog.callableMain(['argv0', '-r', 'test.cog']) self.assertFilesSame('test.cog', 'test.out') output = self.output.getvalue() self.assertIn("(changed)", output) def testFileEncodingOption(self): d = { 'test.cog': b"""\ // \xca\xee\xe4\xe8\xf0\xe2\xea\xe0 Windows //[[[cog cog.outl("\xd1\xfa\xe5\xf8\xfc \xe5\xf9\xb8 \xfd\xf2\xe8\xf5 \xec\xff\xe3\xea\xe8\xf5 \xf4\xf0\xe0\xed\xf6\xf3\xe7\xf1\xea\xe8\xf5 \xe1\xf3\xeb\xee\xea \xe4\xe0 \xe2\xfb\xef\xe5\xe9 \xf7\xe0\xfe") //]]] //[[[end]]] """, 'test.out': b"""\ // \xca\xee\xe4\xe8\xf0\xe2\xea\xe0 Windows //[[[cog cog.outl("\xd1\xfa\xe5\xf8\xfc \xe5\xf9\xb8 \xfd\xf2\xe8\xf5 \xec\xff\xe3\xea\xe8\xf5 \xf4\xf0\xe0\xed\xf6\xf3\xe7\xf1\xea\xe8\xf5 \xe1\xf3\xeb\xee\xea \xe4\xe0 \xe2\xfb\xef\xe5\xe9 \xf7\xe0\xfe") //]]] \xd1\xfa\xe5\xf8\xfc \xe5\xf9\xb8 \xfd\xf2\xe8\xf5 \xec\xff\xe3\xea\xe8\xf5 \xf4\xf0\xe0\xed\xf6\xf3\xe7\xf1\xea\xe8\xf5 \xe1\xf3\xeb\xee\xea \xe4\xe0 \xe2\xfb\xef\xe5\xe9 \xf7\xe0\xfe //[[[end]]] """.replace(b"\n", os.linesep.encode()), } makeFiles(d) self.cog.callableMain(['argv0', '-n', 'cp1251', '-r', 'test.cog']) self.assertFilesSame('test.cog', 'test.out') output = self.output.getvalue() self.assertIn("(changed)", output) class TestCaseWithImports(TestCaseWithTempDir): """ When running tests which import modules, the sys.modules list leaks from one test to the next. This test case class scrubs the list after each run to keep the tests isolated from each other. """ def setUp(self): super().setUp() self.sysmodulekeys = list(sys.modules) def tearDown(self): modstoscrub = [ modname for modname in sys.modules if modname not in self.sysmodulekeys ] for modname in modstoscrub: del sys.modules[modname] super().tearDown() class CogIncludeTests(TestCaseWithImports): dincludes = { 'test.cog': """\ //[[[cog import mymodule //]]] //[[[end]]] """, 'test.out': """\ //[[[cog import mymodule //]]] Hello from mymodule //[[[end]]] """, 'test2.out': """\ //[[[cog import mymodule //]]] Hello from mymodule in inc2 //[[[end]]] """, 'include': { 'mymodule.py': """\ import cog cog.outl("Hello from mymodule") """ }, 'inc2': { 'mymodule.py': """\ import cog cog.outl("Hello from mymodule in inc2") """ }, 'inc3': { 'someothermodule.py': """\ import cog cog.outl("This is some other module.") """ }, } def testNeedIncludePath(self): # Try it without the -I, to see that an ImportError happens. makeFiles(self.dincludes) msg = "(ImportError|ModuleNotFoundError): No module named '?mymodule'?" with self.assertRaisesRegex(CogUserException, msg): self.cog.callableMain(['argv0', '-r', 'test.cog']) def testIncludePath(self): # Test that -I adds include directories properly. makeFiles(self.dincludes) self.cog.callableMain(['argv0', '-r', '-I', 'include', 'test.cog']) self.assertFilesSame('test.cog', 'test.out') def testTwoIncludePaths(self): # Test that two -I's add include directories properly. makeFiles(self.dincludes) self.cog.callableMain(['argv0', '-r', '-I', 'include', '-I', 'inc2', 'test.cog']) self.assertFilesSame('test.cog', 'test.out') def testTwoIncludePaths2(self): # Test that two -I's add include directories properly. makeFiles(self.dincludes) self.cog.callableMain(['argv0', '-r', '-I', 'inc2', '-I', 'include', 'test.cog']) self.assertFilesSame('test.cog', 'test2.out') def testUselessIncludePath(self): # Test that the search will continue past the first directory. makeFiles(self.dincludes) self.cog.callableMain(['argv0', '-r', '-I', 'inc3', '-I', 'include', 'test.cog']) self.assertFilesSame('test.cog', 'test.out') def testSysPathIsUnchanged(self): d = { 'bad.cog': """\ //[[[cog cog.error("Oh no!") ]]] //[[[end]]] """, 'good.cog': """\ //[[[cog cog.outl("Oh yes!") ]]] //[[[end]]] """, } makeFiles(d) # Is it unchanged just by creating a cog engine? oldsyspath = sys.path[:] self.newCog() self.assertEqual(oldsyspath, sys.path) # Is it unchanged for a successful run? self.newCog() self.cog.callableMain(['argv0', '-r', 'good.cog']) self.assertEqual(oldsyspath, sys.path) # Is it unchanged for a successful run with includes? self.newCog() self.cog.callableMain(['argv0', '-r', '-I', 'xyzzy', 'good.cog']) self.assertEqual(oldsyspath, sys.path) # Is it unchanged for a successful run with two includes? self.newCog() self.cog.callableMain(['argv0', '-r', '-I', 'xyzzy', '-I', 'quux', 'good.cog']) self.assertEqual(oldsyspath, sys.path) # Is it unchanged for a failed run? self.newCog() with self.assertRaisesRegex(CogError, r"^Oh no!$"): self.cog.callableMain(['argv0', '-r', 'bad.cog']) self.assertEqual(oldsyspath, sys.path) # Is it unchanged for a failed run with includes? self.newCog() with self.assertRaisesRegex(CogError, r"^Oh no!$"): self.cog.callableMain(['argv0', '-r', '-I', 'xyzzy', 'bad.cog']) self.assertEqual(oldsyspath, sys.path) # Is it unchanged for a failed run with two includes? self.newCog() with self.assertRaisesRegex(CogError, r"^Oh no!$"): self.cog.callableMain(['argv0', '-r', '-I', 'xyzzy', '-I', 'quux', 'bad.cog']) self.assertEqual(oldsyspath, sys.path) def testSubDirectories(self): # Test that relative paths on the command line work, with includes. d = { 'code': { 'test.cog': """\ //[[[cog import mysubmodule //]]] //[[[end]]] """, 'test.out': """\ //[[[cog import mysubmodule //]]] Hello from mysubmodule //[[[end]]] """, 'mysubmodule.py': """\ import cog cog.outl("Hello from mysubmodule") """ } } makeFiles(d) # We should be able to invoke cog without the -I switch, and it will # auto-include the current directory self.cog.callableMain(['argv0', '-r', 'code/test.cog']) self.assertFilesSame('code/test.cog', 'code/test.out') class CogTestsInFiles(TestCaseWithTempDir): def testWarnIfNoCogCode(self): # Test that the -e switch warns if there is no Cog code. d = { 'with.cog': """\ //[[[cog cog.outl("hello world") //]]] hello world //[[[end]]] """, 'without.cog': """\ There's no cog code in this file. """, } makeFiles(d) self.cog.callableMain(['argv0', '-e', 'with.cog']) output = self.output.getvalue() self.assertNotIn("Warning", output) self.newCog() self.cog.callableMain(['argv0', '-e', 'without.cog']) output = self.output.getvalue() self.assertIn("Warning: no cog code found in without.cog", output) self.newCog() self.cog.callableMain(['argv0', 'without.cog']) output = self.output.getvalue() self.assertNotIn("Warning", output) def testFileNameProps(self): d = { 'cog1.txt': """\ //[[[cog cog.outl("This is %s in, %s out" % (cog.inFile, cog.outFile)) //]]] this is cog1.txt in, cog1.txt out [[[end]]] """, 'cog1.out': """\ //[[[cog cog.outl("This is %s in, %s out" % (cog.inFile, cog.outFile)) //]]] This is cog1.txt in, cog1.txt out [[[end]]] """, 'cog1out.out': """\ //[[[cog cog.outl("This is %s in, %s out" % (cog.inFile, cog.outFile)) //]]] This is cog1.txt in, cog1out.txt out [[[end]]] """, } makeFiles(d) self.cog.callableMain(['argv0', '-r', 'cog1.txt']) self.assertFilesSame('cog1.txt', 'cog1.out') self.newCog() self.cog.callableMain(['argv0', '-o', 'cog1out.txt', 'cog1.txt']) self.assertFilesSame('cog1out.txt', 'cog1out.out') def testGlobalsDontCrossFiles(self): # Make sure that global values don't get shared between files. d = { 'one.cog': """\ //[[[cog s = "This was set in one.cog" ]]] //[[[end]]] //[[[cog cog.outl(s) ]]] //[[[end]]] """, 'one.out': """\ //[[[cog s = "This was set in one.cog" ]]] //[[[end]]] //[[[cog cog.outl(s) ]]] This was set in one.cog //[[[end]]] """, 'two.cog': """\ //[[[cog try: cog.outl(s) except NameError: cog.outl("s isn't set!") //]]] //[[[end]]] """, 'two.out': """\ //[[[cog try: cog.outl(s) except NameError: cog.outl("s isn't set!") //]]] s isn't set! //[[[end]]] """, 'cogfiles.txt': """\ # Please run cog one.cog two.cog """ } makeFiles(d) self.cog.callableMain(['argv0', '-r', '@cogfiles.txt']) self.assertFilesSame('one.cog', 'one.out') self.assertFilesSame('two.cog', 'two.out') output = self.output.getvalue() self.assertIn("(changed)", output) def testRemoveGeneratedOutput(self): d = { 'cog1.txt': """\ //[[[cog cog.outl("This line was generated.") //]]] This line was generated. //[[[end]]] This line was not. """, 'cog1.out': """\ //[[[cog cog.outl("This line was generated.") //]]] //[[[end]]] This line was not. """, 'cog1.out2': """\ //[[[cog cog.outl("This line was generated.") //]]] This line was generated. //[[[end]]] This line was not. """, } makeFiles(d) # Remove generated output. self.cog.callableMain(['argv0', '-r', '-x', 'cog1.txt']) self.assertFilesSame('cog1.txt', 'cog1.out') self.newCog() # Regenerate the generated output. self.cog.callableMain(['argv0', '-r', 'cog1.txt']) self.assertFilesSame('cog1.txt', 'cog1.out2') self.newCog() # Remove the generated output again. self.cog.callableMain(['argv0', '-r', '-x', 'cog1.txt']) self.assertFilesSame('cog1.txt', 'cog1.out') def testMsgCall(self): infile = """\ #[[[cog cog.msg("Hello there!") #]]] #[[[end]]] """ infile = reindentBlock(infile) self.assertEqual(self.cog.processString(infile), infile) output = self.output.getvalue() self.assertEqual(output, "Message: Hello there!\n") def testErrorMessageHasNoTraceback(self): # Test that a Cog error is printed to stderr with no traceback. d = { 'cog1.txt': """\ //[[[cog cog.outl("This line was newly") cog.outl("generated by cog") cog.outl("blah blah.") //]]] Xhis line was newly generated by cog blah blah. //[[[end]]] (checksum: a8540982e5ad6b95c9e9a184b26f4346) """, } makeFiles(d) stderr = io.StringIO() self.cog.setOutput(stderr=stderr) self.cog.main(['argv0', '-c', '-r', "cog1.txt"]) self.assertEqual(self.output.getvalue(), "Cogging cog1.txt\n") self.assertEqual(stderr.getvalue(), "cog1.txt(9): Output has been edited! Delete old checksum to unprotect.\n") def testDashD(self): d = { 'test.cog': """\ --[[[cog cog.outl("Defined fooey as " + fooey) ]]] --[[[end]]] """, 'test.kablooey': """\ --[[[cog cog.outl("Defined fooey as " + fooey) ]]] Defined fooey as kablooey --[[[end]]] """, 'test.einstein': """\ --[[[cog cog.outl("Defined fooey as " + fooey) ]]] Defined fooey as e=mc2 --[[[end]]] """, } makeFiles(d) self.cog.callableMain(['argv0', '-r', '-D', 'fooey=kablooey', 'test.cog']) self.assertFilesSame('test.cog', 'test.kablooey') makeFiles(d) self.cog.callableMain(['argv0', '-r', '-Dfooey=kablooey', 'test.cog']) self.assertFilesSame('test.cog', 'test.kablooey') makeFiles(d) self.cog.callableMain(['argv0', '-r', '-Dfooey=e=mc2', 'test.cog']) self.assertFilesSame('test.cog', 'test.einstein') makeFiles(d) self.cog.callableMain(['argv0', '-r', '-Dbar=quux', '-Dfooey=kablooey', 'test.cog']) self.assertFilesSame('test.cog', 'test.kablooey') makeFiles(d) self.cog.callableMain(['argv0', '-r', '-Dfooey=kablooey', '-Dbar=quux', 'test.cog']) self.assertFilesSame('test.cog', 'test.kablooey') makeFiles(d) self.cog.callableMain(['argv0', '-r', '-Dfooey=gooey', '-Dfooey=kablooey', 'test.cog']) self.assertFilesSame('test.cog', 'test.kablooey') def testOutputToStdout(self): d = { 'test.cog': """\ --[[[cog cog.outl('Hey there!') ]]] --[[[end]]] """ } makeFiles(d) stderr = io.StringIO() self.cog.setOutput(stderr=stderr) self.cog.callableMain(['argv0', 'test.cog']) output = self.output.getvalue() outerr = stderr.getvalue() self.assertEqual(output, "--[[[cog cog.outl('Hey there!') ]]]\nHey there!\n--[[[end]]]\n") self.assertEqual(outerr, "") def testReadFromStdin(self): stdin = io.StringIO("--[[[cog cog.outl('Wow') ]]]\n--[[[end]]]\n") def restore_stdin(old_stdin): sys.stdin = old_stdin self.addCleanup(restore_stdin, sys.stdin) sys.stdin = stdin stderr = io.StringIO() self.cog.setOutput(stderr=stderr) self.cog.callableMain(['argv0', '-']) output = self.output.getvalue() outerr = stderr.getvalue() self.assertEqual(output, "--[[[cog cog.outl('Wow') ]]]\nWow\n--[[[end]]]\n") self.assertEqual(outerr, "") def testSuffixOutputLines(self): d = { 'test.cog': """\ Hey there. ;[[[cog cog.outl('a\\nb\\n \\nc') ]]] ;[[[end]]] Good bye. """, 'test.out': """\ Hey there. ;[[[cog cog.outl('a\\nb\\n \\nc') ]]] a (foo) b (foo) """ # These three trailing spaces are important. # The suffix is not applied to completely blank lines. """ c (foo) ;[[[end]]] Good bye. """, } makeFiles(d) self.cog.callableMain(['argv0', '-r', '-s', ' (foo)', 'test.cog']) self.assertFilesSame('test.cog', 'test.out') def testEmptySuffix(self): d = { 'test.cog': """\ ;[[[cog cog.outl('a\\nb\\nc') ]]] ;[[[end]]] """, 'test.out': """\ ;[[[cog cog.outl('a\\nb\\nc') ]]] a b c ;[[[end]]] """, } makeFiles(d) self.cog.callableMain(['argv0', '-r', '-s', '', 'test.cog']) self.assertFilesSame('test.cog', 'test.out') def testHellishSuffix(self): d = { 'test.cog': """\ ;[[[cog cog.outl('a\\n\\nb') ]]] """, 'test.out': """\ ;[[[cog cog.outl('a\\n\\nb') ]]] a /\\n*+([)]>< b /\\n*+([)]>< """, } makeFiles(d) self.cog.callableMain(['argv0', '-z', '-r', '-s', r' /\n*+([)]><', 'test.cog']) self.assertFilesSame('test.cog', 'test.out') def testPrologue(self): d = { 'test.cog': """\ Some text. //[[[cog cog.outl(str(math.sqrt(2))[:12])]]] //[[[end]]] epilogue. """, 'test.out': """\ Some text. //[[[cog cog.outl(str(math.sqrt(2))[:12])]]] 1.4142135623 //[[[end]]] epilogue. """, } makeFiles(d) self.cog.callableMain(['argv0', '-r', '-p', 'import math', 'test.cog']) self.assertFilesSame('test.cog', 'test.out') def testThreads(self): # Test that the implicitly imported cog module is actually different for # different threads. numthreads = 20 d = {} for i in range(numthreads): d[f'f{i}.cog'] = ( "x\n" * i + "[[[cog\n" + f"assert cog.firstLineNum == int(FIRST) == {i+1}\n" + "]]]\n" + "[[[end]]]\n" ) makeFiles(d) results = [] def thread_main(num): try: ret = Cog().main( ['cog.py', '-r', '-D', f'FIRST={num+1}', f'f{num}.cog'] ) assert ret == 0 except Exception as exc: # pragma: no cover (only happens on test failure) results.append(exc) else: results.append(None) ts = [threading.Thread(target=thread_main, args=(i,)) for i in range(numthreads)] for t in ts: t.start() for t in ts: t.join() assert results == [None] * numthreads class CheckTests(TestCaseWithTempDir): def run_check(self, args, status=0): actual_status = self.cog.main(['argv0', '--check'] + args) print(self.output.getvalue()) self.assertEqual(status, actual_status) def assert_made_files_unchanged(self, d): for name, content in d.items(): content = reindentBlock(content) if os.name == 'nt': content = content.replace("\n", "\r\n") self.assertFileContent(name, content) def test_check_no_cog(self): d = { 'hello.txt': """\ Hello. """, } makeFiles(d) self.run_check(['hello.txt'], status=0) self.assertEqual(self.output.getvalue(), "Checking hello.txt\n") self.assert_made_files_unchanged(d) def test_check_good(self): d = { 'unchanged.cog': """\ //[[[cog cog.outl("hello world") //]]] hello world //[[[end]]] """, } makeFiles(d) self.run_check(['unchanged.cog'], status=0) self.assertEqual(self.output.getvalue(), "Checking unchanged.cog\n") self.assert_made_files_unchanged(d) def test_check_bad(self): d = { 'changed.cog': """\ //[[[cog cog.outl("goodbye world") //]]] hello world //[[[end]]] """, } makeFiles(d) self.run_check(['changed.cog'], status=5) self.assertEqual(self.output.getvalue(), "Checking changed.cog (changed)\nCheck failed\n") self.assert_made_files_unchanged(d) def test_check_mixed(self): d = { 'unchanged.cog': """\ //[[[cog cog.outl("hello world") //]]] hello world //[[[end]]] """, 'changed.cog': """\ //[[[cog cog.outl("goodbye world") //]]] hello world //[[[end]]] """, } makeFiles(d) for verbosity, output in [ ("0", "Check failed\n"), ("1", "Checking changed.cog (changed)\nCheck failed\n"), ("2", "Checking unchanged.cog\nChecking changed.cog (changed)\nCheck failed\n"), ]: self.newCog() self.run_check(['--verbosity=%s' % verbosity, 'unchanged.cog', 'changed.cog'], status=5) self.assertEqual(self.output.getvalue(), output) self.assert_made_files_unchanged(d) def test_check_with_good_checksum(self): d = { 'good.txt': """\ //[[[cog cog.outl("This line was newly") cog.outl("generated by cog") cog.outl("blah blah.") //]]] This line was newly generated by cog blah blah. //[[[end]]] (checksum: a8540982e5ad6b95c9e9a184b26f4346) """, } makeFiles(d) # Have to use -c with --check if there are checksums in the file. self.run_check(['-c', 'good.txt'], status=0) self.assertEqual(self.output.getvalue(), "Checking good.txt\n") self.assert_made_files_unchanged(d) def test_check_with_bad_checksum(self): d = { 'bad.txt': """\ //[[[cog cog.outl("This line was newly") cog.outl("generated by cog") cog.outl("blah blah.") //]]] This line was newly generated by cog blah blah. //[[[end]]] (checksum: a9999999e5ad6b95c9e9a184b26f4346) """, } makeFiles(d) # Have to use -c with --check if there are checksums in the file. self.run_check(['-c', 'bad.txt'], status=1) self.assertEqual(self.output.getvalue(), "Checking bad.txt\nbad.txt(9): Output has been edited! Delete old checksum to unprotect.\n") self.assert_made_files_unchanged(d) class WritabilityTests(TestCaseWithTempDir): d = { 'test.cog': """\ //[[[cog for fn in ['DoSomething', 'DoAnotherThing', 'DoLastThing']: cog.outl("void %s();" % fn) //]]] //[[[end]]] """, 'test.out': """\ //[[[cog for fn in ['DoSomething', 'DoAnotherThing', 'DoLastThing']: cog.outl("void %s();" % fn) //]]] void DoSomething(); void DoAnotherThing(); void DoLastThing(); //[[[end]]] """, } if os.name == 'nt': # for Windows cmd_w_args = 'attrib -R %s' cmd_w_asterisk = 'attrib -R *' else: # for unix-like cmd_w_args = 'chmod +w %s' cmd_w_asterisk = 'chmod +w *' def setUp(self): super().setUp() makeFiles(self.d) self.testcog = os.path.join(self.tempdir, 'test.cog') os.chmod(self.testcog, stat.S_IREAD) # Make the file readonly. assert not os.access(self.testcog, os.W_OK) def tearDown(self): os.chmod(self.testcog, stat.S_IWRITE) # Make the file writable again. super().tearDown() def testReadonlyNoCommand(self): with self.assertRaisesRegex(CogError, "^Can't overwrite test.cog$"): self.cog.callableMain(['argv0', '-r', 'test.cog']) assert not os.access(self.testcog, os.W_OK) def testReadonlyWithCommand(self): self.cog.callableMain(['argv0', '-r', '-w', self.cmd_w_args, 'test.cog']) self.assertFilesSame('test.cog', 'test.out') assert os.access(self.testcog, os.W_OK) def testReadonlyWithCommandWithNoSlot(self): self.cog.callableMain(['argv0', '-r', '-w', self.cmd_w_asterisk, 'test.cog']) self.assertFilesSame('test.cog', 'test.out') assert os.access(self.testcog, os.W_OK) def testReadonlyWithIneffectualCommand(self): with self.assertRaisesRegex(CogError, "^Couldn't make test.cog writable$"): self.cog.callableMain(['argv0', '-r', '-w', 'echo %s', 'test.cog']) assert not os.access(self.testcog, os.W_OK) class ChecksumTests(TestCaseWithTempDir): def testCreateChecksumOutput(self): d = { 'cog1.txt': """\ //[[[cog cog.outl("This line was generated.") //]]] This line was generated. //[[[end]]] This line was not. """, 'cog1.out': """\ //[[[cog cog.outl("This line was generated.") //]]] This line was generated. //[[[end]]] (checksum: 8adb13fb59b996a1c7f0065ea9f3d893) This line was not. """, } makeFiles(d) self.cog.callableMain(['argv0', '-r', '-c', 'cog1.txt']) self.assertFilesSame('cog1.txt', 'cog1.out') def testCheckChecksumOutput(self): d = { 'cog1.txt': """\ //[[[cog cog.outl("This line was newly") cog.outl("generated by cog") cog.outl("blah blah.") //]]] This line was generated. //[[[end]]] (checksum: 8adb13fb59b996a1c7f0065ea9f3d893) """, 'cog1.out': """\ //[[[cog cog.outl("This line was newly") cog.outl("generated by cog") cog.outl("blah blah.") //]]] This line was newly generated by cog blah blah. //[[[end]]] (checksum: a8540982e5ad6b95c9e9a184b26f4346) """, } makeFiles(d) self.cog.callableMain(['argv0', '-r', '-c', 'cog1.txt']) self.assertFilesSame('cog1.txt', 'cog1.out') def testRemoveChecksumOutput(self): d = { 'cog1.txt': """\ //[[[cog cog.outl("This line was newly") cog.outl("generated by cog") cog.outl("blah blah.") //]]] This line was generated. //[[[end]]] (checksum: 8adb13fb59b996a1c7f0065ea9f3d893) fooey """, 'cog1.out': """\ //[[[cog cog.outl("This line was newly") cog.outl("generated by cog") cog.outl("blah blah.") //]]] This line was newly generated by cog blah blah. //[[[end]]] fooey """, } makeFiles(d) self.cog.callableMain(['argv0', '-r', 'cog1.txt']) self.assertFilesSame('cog1.txt', 'cog1.out') def testTamperedChecksumOutput(self): d = { 'cog1.txt': """\ //[[[cog cog.outl("This line was newly") cog.outl("generated by cog") cog.outl("blah blah.") //]]] Xhis line was newly generated by cog blah blah. //[[[end]]] (checksum: a8540982e5ad6b95c9e9a184b26f4346) """, 'cog2.txt': """\ //[[[cog cog.outl("This line was newly") cog.outl("generated by cog") cog.outl("blah blah.") //]]] This line was newly generated by cog blah blah! //[[[end]]] (checksum: a8540982e5ad6b95c9e9a184b26f4346) """, 'cog3.txt': """\ //[[[cog cog.outl("This line was newly") cog.outl("generated by cog") cog.outl("blah blah.") //]]] This line was newly generated by cog blah blah. //[[[end]]] (checksum: a8540982e5ad6b95c9e9a184b26f4346) """, 'cog4.txt': """\ //[[[cog cog.outl("This line was newly") cog.outl("generated by cog") cog.outl("blah blah.") //]]] This line was newly generated by cog blah blah.. //[[[end]]] (checksum: a8540982e5ad6b95c9e9a184b26f4346) """, 'cog5.txt': """\ //[[[cog cog.outl("This line was newly") cog.outl("generated by cog") cog.outl("blah blah.") //]]] This line was newly generated by cog blah blah. extra //[[[end]]] (checksum: a8540982e5ad6b95c9e9a184b26f4346) """, 'cog6.txt': """\ //[[[cog cog.outl("This line was newly") cog.outl("generated by cog") cog.outl("blah blah.") //]]] //[[[end]]] (checksum: a8540982e5ad6b95c9e9a184b26f4346) """, } makeFiles(d) with self.assertRaisesRegex(CogError, r"^cog1.txt\(9\): Output has been edited! Delete old checksum to unprotect.$"): self.cog.callableMain(['argv0', '-c', "cog1.txt"]) with self.assertRaisesRegex(CogError, r"^cog2.txt\(9\): Output has been edited! Delete old checksum to unprotect.$"): self.cog.callableMain(['argv0', '-c', "cog2.txt"]) with self.assertRaisesRegex(CogError, r"^cog3.txt\(10\): Output has been edited! Delete old checksum to unprotect.$"): self.cog.callableMain(['argv0', '-c', "cog3.txt"]) with self.assertRaisesRegex(CogError, r"^cog4.txt\(9\): Output has been edited! Delete old checksum to unprotect.$"): self.cog.callableMain(['argv0', '-c', "cog4.txt"]) with self.assertRaisesRegex(CogError, r"^cog5.txt\(10\): Output has been edited! Delete old checksum to unprotect.$"): self.cog.callableMain(['argv0', '-c', "cog5.txt"]) with self.assertRaisesRegex(CogError, r"^cog6.txt\(6\): Output has been edited! Delete old checksum to unprotect.$"): self.cog.callableMain(['argv0', '-c', "cog6.txt"]) def testArgvIsntModified(self): argv = ['argv0', '-v'] orig_argv = argv[:] self.cog.callableMain(argv) self.assertEqual(argv, orig_argv) class CustomMarkerTests(TestCaseWithTempDir): def testCustomerMarkers(self): d = { 'test.cog': """\ //{{ cog.outl("void %s();" % "MyFunction") //}} //{{end}} """, 'test.out': """\ //{{ cog.outl("void %s();" % "MyFunction") //}} void MyFunction(); //{{end}} """, } makeFiles(d) self.cog.callableMain([ 'argv0', '-r', '--markers={{ }} {{end}}', 'test.cog' ]) self.assertFilesSame('test.cog', 'test.out') def testTrulyWackyMarkers(self): # Make sure the markers are properly re-escaped. d = { 'test.cog': """\ //**( cog.outl("void %s();" % "MyFunction") //**) //**(end)** """, 'test.out': """\ //**( cog.outl("void %s();" % "MyFunction") //**) void MyFunction(); //**(end)** """, } makeFiles(d) self.cog.callableMain([ 'argv0', '-r', '--markers=**( **) **(end)**', 'test.cog' ]) self.assertFilesSame('test.cog', 'test.out') def testChangeJustOneMarker(self): d = { 'test.cog': """\ //**( cog.outl("void %s();" % "MyFunction") //]]] //[[[end]]] """, 'test.out': """\ //**( cog.outl("void %s();" % "MyFunction") //]]] void MyFunction(); //[[[end]]] """, } makeFiles(d) self.cog.callableMain([ 'argv0', '-r', '--markers=**( ]]] [[[end]]]', 'test.cog' ]) self.assertFilesSame('test.cog', 'test.out') class BlakeTests(TestCaseWithTempDir): # Blake Winton's contributions. def testDeleteCode(self): # -o sets the output file. d = { 'test.cog': """\ // This is my C++ file. //[[[cog fnames = ['DoSomething', 'DoAnotherThing', 'DoLastThing'] for fn in fnames: cog.outl("void %s();" % fn) //]]] Some Sample Code Here //[[[end]]]Data Data And Some More """, 'test.out': """\ // This is my C++ file. void DoSomething(); void DoAnotherThing(); void DoLastThing(); And Some More """, } makeFiles(d) self.cog.callableMain(['argv0', '-d', '-o', 'test.cogged', 'test.cog']) self.assertFilesSame('test.cogged', 'test.out') def testDeleteCodeWithDashRFails(self): d = { 'test.cog': """\ // This is my C++ file. """ } makeFiles(d) with self.assertRaisesRegex(CogUsageError, r"^Can't use -d with -r \(or you would delete all your source!\)$"): self.cog.callableMain(['argv0', '-r', '-d', 'test.cog']) def testSettingGlobals(self): # Blake Winton contributed a way to set the globals that will be used in # processFile(). d = { 'test.cog': """\ // This is my C++ file. //[[[cog for fn in fnames: cog.outl("void %s();" % fn) //]]] Some Sample Code Here //[[[end]]]""", 'test.out': """\ // This is my C++ file. void DoBlake(); void DoWinton(); void DoContribution(); """, } makeFiles(d) globals = {} globals['fnames'] = ['DoBlake', 'DoWinton', 'DoContribution'] self.cog.options.bDeleteCode = True self.cog.processFile('test.cog', 'test.cogged', globals=globals) self.assertFilesSame('test.cogged', 'test.out') class ErrorCallTests(TestCaseWithTempDir): def testErrorCallHasNoTraceback(self): # Test that cog.error() doesn't show a traceback. d = { 'error.cog': """\ //[[[cog cog.error("Something Bad!") //]]] //[[[end]]] """, } makeFiles(d) self.cog.main(['argv0', '-r', 'error.cog']) output = self.output.getvalue() self.assertEqual(output, "Cogging error.cog\nError: Something Bad!\n") def testRealErrorHasTraceback(self): # Test that a genuine error does show a traceback. d = { 'error.cog': """\ //[[[cog raise RuntimeError("Hey!") //]]] //[[[end]]] """, } makeFiles(d) self.cog.main(['argv0', '-r', 'error.cog']) output = self.output.getvalue() msg = 'Actual output:\n' + output self.assertTrue(output.startswith("Cogging error.cog\nTraceback (most recent"), msg) self.assertIn("RuntimeError: Hey!", output) # Things not yet tested: # - A bad -w command (currently fails silently). cog-3.4.1/cogapp/test_makefiles.py000066400000000000000000000066671457240776200171460ustar00rootroot00000000000000""" Test the cogapp.makefiles modules """ import shutil import os import random import tempfile from unittest import TestCase from . import makefiles class SimpleTests(TestCase): def setUp(self): # Create a temporary directory. my_dir = 'testmakefiles_tempdir_' + str(random.random())[2:] self.tempdir = os.path.join(tempfile.gettempdir(), my_dir) os.mkdir(self.tempdir) def tearDown(self): # Get rid of the temporary directory. shutil.rmtree(self.tempdir) def exists(self, dname, fname): return os.path.exists(os.path.join(dname, fname)) def checkFilesExist(self, d, dname): for fname in d.keys(): assert(self.exists(dname, fname)) if type(d[fname]) == type({}): self.checkFilesExist(d[fname], os.path.join(dname, fname)) def checkFilesDontExist(self, d, dname): for fname in d.keys(): assert(not self.exists(dname, fname)) def testOneFile(self): fname = 'foo.txt' notfname = 'not_here.txt' d = { fname: "howdy" } assert(not self.exists(self.tempdir, fname)) assert(not self.exists(self.tempdir, notfname)) makefiles.makeFiles(d, self.tempdir) assert(self.exists(self.tempdir, fname)) assert(not self.exists(self.tempdir, notfname)) makefiles.removeFiles(d, self.tempdir) assert(not self.exists(self.tempdir, fname)) assert(not self.exists(self.tempdir, notfname)) def testManyFiles(self): d = { 'top1.txt': "howdy", 'top2.txt': "hello", 'sub': { 'sub1.txt': "inside", 'sub2.txt': "inside2", }, } self.checkFilesDontExist(d, self.tempdir) makefiles.makeFiles(d, self.tempdir) self.checkFilesExist(d, self.tempdir) makefiles.removeFiles(d, self.tempdir) self.checkFilesDontExist(d, self.tempdir) def testOverlapping(self): d1 = { 'top1.txt': "howdy", 'sub': { 'sub1.txt': "inside", }, } d2 = { 'top2.txt': "hello", 'sub': { 'sub2.txt': "inside2", }, } self.checkFilesDontExist(d1, self.tempdir) self.checkFilesDontExist(d2, self.tempdir) makefiles.makeFiles(d1, self.tempdir) makefiles.makeFiles(d2, self.tempdir) self.checkFilesExist(d1, self.tempdir) self.checkFilesExist(d2, self.tempdir) makefiles.removeFiles(d1, self.tempdir) makefiles.removeFiles(d2, self.tempdir) self.checkFilesDontExist(d1, self.tempdir) self.checkFilesDontExist(d2, self.tempdir) def testContents(self): fname = 'bar.txt' cont0 = "I am bar.txt" d = { fname: cont0 } makefiles.makeFiles(d, self.tempdir) with open(os.path.join(self.tempdir, fname)) as fcont1: assert(fcont1.read() == cont0) def testDedent(self): fname = 'dedent.txt' d = { fname: """\ This is dedent.txt \tTabbed in. spaced in. OK. """, } makefiles.makeFiles(d, self.tempdir) with open(os.path.join(self.tempdir, fname)) as fcont: assert(fcont.read() == "This is dedent.txt\n\tTabbed in.\n spaced in.\nOK.\n") cog-3.4.1/cogapp/test_whiteutils.py000066400000000000000000000075331457240776200174000ustar00rootroot00000000000000""" Test the cogapp.whiteutils module. """ from unittest import TestCase from .whiteutils import commonPrefix, reindentBlock, whitePrefix class WhitePrefixTests(TestCase): """ Test cases for cogapp.whiteutils. """ def testSingleLine(self): self.assertEqual(whitePrefix(['']), '') self.assertEqual(whitePrefix([' ']), '') self.assertEqual(whitePrefix(['x']), '') self.assertEqual(whitePrefix([' x']), ' ') self.assertEqual(whitePrefix(['\tx']), '\t') self.assertEqual(whitePrefix([' x']), ' ') self.assertEqual(whitePrefix([' \t \tx ']), ' \t \t') def testMultiLine(self): self.assertEqual(whitePrefix([' x',' x',' x']), ' ') self.assertEqual(whitePrefix([' y',' y',' y']), ' ') self.assertEqual(whitePrefix([' y',' y',' y']), ' ') def testBlankLinesAreIgnored(self): self.assertEqual(whitePrefix([' x',' x','',' x']), ' ') self.assertEqual(whitePrefix(['',' x',' x',' x']), ' ') self.assertEqual(whitePrefix([' x',' x',' x','']), ' ') self.assertEqual(whitePrefix([' x',' x',' ',' x']), ' ') def testTabCharacters(self): self.assertEqual(whitePrefix(['\timport sys', '', '\tprint sys.argv']), '\t') def testDecreasingLengths(self): self.assertEqual(whitePrefix([' x',' x',' x']), ' ') self.assertEqual(whitePrefix([' x',' x',' x']), ' ') class ReindentBlockTests(TestCase): """ Test cases for cogapp.reindentBlock. """ def testNonTermLine(self): self.assertEqual(reindentBlock(''), '') self.assertEqual(reindentBlock('x'), 'x') self.assertEqual(reindentBlock(' x'), 'x') self.assertEqual(reindentBlock(' x'), 'x') self.assertEqual(reindentBlock('\tx'), 'x') self.assertEqual(reindentBlock('x', ' '), ' x') self.assertEqual(reindentBlock('x', '\t'), '\tx') self.assertEqual(reindentBlock(' x', ' '), ' x') self.assertEqual(reindentBlock(' x', '\t'), '\tx') self.assertEqual(reindentBlock(' x', ' '), ' x') def testSingleLine(self): self.assertEqual(reindentBlock('\n'), '\n') self.assertEqual(reindentBlock('x\n'), 'x\n') self.assertEqual(reindentBlock(' x\n'), 'x\n') self.assertEqual(reindentBlock(' x\n'), 'x\n') self.assertEqual(reindentBlock('\tx\n'), 'x\n') self.assertEqual(reindentBlock('x\n', ' '), ' x\n') self.assertEqual(reindentBlock('x\n', '\t'), '\tx\n') self.assertEqual(reindentBlock(' x\n', ' '), ' x\n') self.assertEqual(reindentBlock(' x\n', '\t'), '\tx\n') self.assertEqual(reindentBlock(' x\n', ' '), ' x\n') def testRealBlock(self): self.assertEqual( reindentBlock('\timport sys\n\n\tprint sys.argv\n'), 'import sys\n\nprint sys.argv\n' ) class CommonPrefixTests(TestCase): """ Test cases for cogapp.commonPrefix. """ def testDegenerateCases(self): self.assertEqual(commonPrefix([]), '') self.assertEqual(commonPrefix(['']), '') self.assertEqual(commonPrefix(['','','','','']), '') self.assertEqual(commonPrefix(['cat in the hat']), 'cat in the hat') def testNoCommonPrefix(self): self.assertEqual(commonPrefix(['a','b']), '') self.assertEqual(commonPrefix(['a','b','c','d','e','f']), '') self.assertEqual(commonPrefix(['a','a','a','a','a','x']), '') def testUsualCases(self): self.assertEqual(commonPrefix(['ab', 'ac']), 'a') self.assertEqual(commonPrefix(['aab', 'aac']), 'aa') self.assertEqual(commonPrefix(['aab', 'aab', 'aab', 'aac']), 'aa') def testBlankLine(self): self.assertEqual(commonPrefix(['abc', 'abx', '', 'aby']), '') def testDecreasingLengths(self): self.assertEqual(commonPrefix(['abcd', 'abc', 'ab']), 'ab') cog-3.4.1/cogapp/utils.py000066400000000000000000000027731457240776200153010ustar00rootroot00000000000000""" Utilities for cog. """ import contextlib import functools import hashlib import os import sys # Support FIPS mode where possible (Python >= 3.9). We don't use MD5 for security. md5 = ( functools.partial(hashlib.md5, usedforsecurity=False) if sys.version_info >= (3, 9) else hashlib.md5 ) class Redirectable: """ An object with its own stdout and stderr files. """ def __init__(self): self.stdout = sys.stdout self.stderr = sys.stderr def setOutput(self, stdout=None, stderr=None): """ Assign new files for standard out and/or standard error. """ if stdout: self.stdout = stdout if stderr: self.stderr = stderr def prout(self, s, end="\n"): print(s, file=self.stdout, end=end) def prerr(self, s, end="\n"): print(s, file=self.stderr, end=end) class NumberedFileReader: """ A decorator for files that counts the readline()'s called. """ def __init__(self, f): self.f = f self.n = 0 def readline(self): l = self.f.readline() if l: self.n += 1 return l def linenumber(self): return self.n @contextlib.contextmanager def change_dir(new_dir): """Change directory, and then change back. Use as a context manager, it will return to the original directory at the end of the block. """ old_dir = os.getcwd() os.chdir(str(new_dir)) try: yield finally: os.chdir(old_dir) cog-3.4.1/cogapp/whiteutils.py000066400000000000000000000036111457240776200163320ustar00rootroot00000000000000""" Indentation utilities for Cog. """ import re def whitePrefix(strings): """ Determine the whitespace prefix common to all non-blank lines in the argument list. """ # Remove all blank lines from the list strings = [s for s in strings if s.strip() != ''] if not strings: return '' # Find initial whitespace chunk in the first line. # This is the best prefix we can hope for. pat = r'\s*' if isinstance(strings[0], bytes): pat = pat.encode("utf-8") prefix = re.match(pat, strings[0]).group(0) # Loop over the other strings, keeping only as much of # the prefix as matches each string. for s in strings: for i in range(len(prefix)): if prefix[i] != s[i]: prefix = prefix[:i] break return prefix def reindentBlock(lines, newIndent=''): """ Take a block of text as a string or list of lines. Remove any common whitespace indentation. Re-indent using newIndent, and return it as a single string. """ sep, nothing = '\n', '' if isinstance(lines, bytes): sep, nothing = b'\n', b'' if isinstance(lines, (bytes, str)): lines = lines.split(sep) oldIndent = whitePrefix(lines) outLines = [] for l in lines: if oldIndent: l = l.replace(oldIndent, nothing, 1) if l and newIndent: l = newIndent + l outLines.append(l) return sep.join(outLines) def commonPrefix(strings): """ Find the longest string that is a prefix of all the strings. """ if not strings: return '' prefix = strings[0] for s in strings: if len(s) < len(prefix): prefix = prefix[:len(s)] if not prefix: return '' for i in range(len(prefix)): if prefix[i] != s[i]: prefix = prefix[:i] break return prefix cog-3.4.1/docs/000077500000000000000000000000001457240776200132355ustar00rootroot00000000000000cog-3.4.1/docs/Makefile000066400000000000000000000011721457240776200146760ustar00rootroot00000000000000# Minimal makefile for Sphinx documentation # # You can set these variables from the command line, and also # from the environment for the first two. SPHINXOPTS ?= SPHINXBUILD ?= sphinx-build SOURCEDIR = . BUILDDIR = _build # Put it first so that "make" without argument is like "make help". help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) .PHONY: help Makefile # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) cog-3.4.1/docs/changes.rst000066400000000000000000000000541457240776200153760ustar00rootroot00000000000000.. _changes: .. include:: ../CHANGELOG.rst cog-3.4.1/docs/conf.py000066400000000000000000000017271457240776200145430ustar00rootroot00000000000000# Configuration file for the Sphinx documentation builder. # # For the full list of built-in configuration values, see the documentation: # https://www.sphinx-doc.org/en/master/usage/configuration.html # -- Project information ----------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information project = 'cog' copyright = '2004–2024, Ned Batchelder' author = 'Ned Batchelder' release = '3.4.1' # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration extensions = [] templates_path = ['_templates'] exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] language = 'en' # -- Options for HTML output ------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output html_theme = 'alabaster' html_static_path = ['_static'] cog-3.4.1/docs/design.rst000066400000000000000000000023021457240776200152350ustar00rootroot00000000000000Design ====== Cog is designed to be easy to run. It writes its results back into the original file while retaining the code it executed. This means cog can be run any number of times on the same file. Rather than have a source generator file, and a separate output file, typically cog is run with one file serving as both generator and output. Because the marker lines accommodate any language syntax, the markers can hide the cog Python code from the source file. This means cog files can be checked into source control without worrying about keeping the source files separate from the output files, without modifying build procedures, and so on. I experimented with using a templating engine for generating code, and found myself constantly struggling with white space in the generated output, and mentally converting from the Python code I could imagine, into its templating equivalent. The advantages of a templating system (that most of the code could be entered literally) were lost as the code generation tasks became more complex, and the generation process needed more logic. Cog lets you use the full power of Python for text generation, without a templating system dumbing down your tools for you. cog-3.4.1/docs/index.rst000066400000000000000000000105171457240776200151020ustar00rootroot00000000000000=== Cog === .. Created. Version 1.1. Minor edits for clarity. Updated to cog 1.11, added a See Also section, and fixed a sample. Updated to cog 1.12. Updated to cog 1.2. Updated to cog 1.3. Updated to cog 1.4. Added links to other Cog implementations. Added links to 2.0 beta 2. Updating for 2.0. Added PCG. Added an explicit mention of the license: MIT. Added links to 3rd-party packages. Clarified -D value types, and fixed a 3rd-party link. Tried to explain better about indentation, and fixed an incorrect parameter name. Added -U switch from Alexander Belchenko. Fixed the russian pointer to be to a current document. Removed handyxml, files are now at pypi. Python 3 is supported! Polish up Cog 2.3 Version 2.4 Version 3.0.0 Version 3.2.0 Version 3.3.0 Cog is a file generation tool. It lets you use pieces of Python code as generators in your source files to generate whatever text you need. This page describes version 3.4.1, released March 7, 2024. What does it do? ================ Cog transforms files in a very simple way: it finds chunks of Python code embedded in them, executes the Python code, and inserts its output back into the original file. The file can contain whatever text you like around the Python code. It will usually be source code. For example, if you run this file through cog: .. code-block:: cpp // This is my C++ file. ... /*[[[cog import cog fnames = ['DoSomething', 'DoAnotherThing', 'DoLastThing'] for fn in fnames: cog.outl("void %s();" % fn) ]]]*/ //[[[end]]] ... it will come out like this: .. code-block:: cpp // This is my C++ file. ... /*[[[cog import cog fnames = ['DoSomething', 'DoAnotherThing', 'DoLastThing'] for fn in fnames: cog.outl("void %s();" % fn) ]]]*/ void DoSomething(); void DoAnotherThing(); void DoLastThing(); //[[[end]]] ... Lines with triple square brackets are marker lines. The lines between ``[[[cog`` and ``]]]`` are the generator Python code. The lines between ``]]]`` and ``[[[end]]]`` are the output from the generator. Output is written with `cog.outl()`, or if you use the ``-P`` option, normal `print()` calls. When cog runs, it discards the last generated Python output, executes the generator Python code, and writes its generated output into the file. All text lines outside of the special markers are passed through unchanged. The cog marker lines can contain any text in addition to the triple square bracket tokens. This makes it possible to hide the generator Python code from the source file. In the sample above, the entire chunk of Python code is a C++ comment, so the Python code can be left in place while the file is treated as C++ code. Installation ============ Cog requires Python 3.7 or higher. Cog is installed in the usual way, except the installation name is "cogapp", not "cog": .. code-block:: bash $ python3 -m pip install cogapp You should now have a "cog" command you can run. See the :ref:`changelog ` for the history of changes. Cog is distributed under the `MIT license`_. Use it to spread goodness through the world. .. _MIT License: http://www.opensource.org/licenses/mit-license.php More ==== .. toctree:: :maxdepth: 1 changes design source module running cog-3.4.1/docs/module.rst000066400000000000000000000034661457240776200152650ustar00rootroot00000000000000The cog module ============== A synthetic module called ``cog`` provides functions you can call to produce output into your file. You don't need to use these functions: with the ``-P`` command-line option, your program's stdout writes to the output file, so `print()` will be enough. The module contents are: **cog.out** `(sOut='' [, dedent=False][, trimblanklines=False])` Writes text to the output. `sOut` is the string to write to the output. If `dedent` is True, then common initial white space is removed from the lines in `sOut` before adding them to the output. If `trimblanklines` is True, then an initial and trailing blank line are removed from `sOut` before adding them to the output. Together, these option arguments make it easier to use multi-line strings, and they only are useful for multi-line strings:: cog.out(""" These are lines I want to write into my source file. """, dedent=True, trimblanklines=True) **cog.outl** Same as **cog.out**, but adds a trailing newline. **cog.msg** `(msg)` Prints `msg` to stdout with a "Message: " prefix. **cog.error** `(msg)` Raises an exception with `msg` as the text. No traceback is included, so that non-Python programmers using your code generators won't be scared. **cog.inFile** An attribute, the path of the input file. **cog.outFile** An attribute, the path of the output file. **cog.firstLineNum** An attribute, the line number of the first line of Python code in the generator. This can be used to distinguish between two generators in the same input file, if needed. **cog.previous** An attribute, the text output of the previous run of this generator. This can be used for whatever purpose you like, including outputting again with **cog.out**. cog-3.4.1/docs/running.rst000066400000000000000000000215331457240776200154530ustar00rootroot00000000000000Running cog =========== Cog is a command-line utility which takes arguments in standard form. .. {{{cog # Re-run this with `make cogdoc` # Here we use unconventional markers so the docs can use [[[ without # getting tangled up in the cog processing. import io import textwrap from cogapp import Cog print("\n.. code-block:: text\n") outf = io.StringIO() print("$ cog -h", file=outf) cog = Cog() cog.setOutput(stdout=outf, stderr=outf) cog.main(["cog", "-h"]) print(textwrap.indent(outf.getvalue(), " ")) .. }}} .. code-block:: text $ cog -h cog - generate content with inlined Python code. cog [OPTIONS] [INFILE | @FILELIST | &FILELIST] ... INFILE is the name of an input file, '-' will read from stdin. FILELIST is the name of a text file containing file names or other @FILELISTs. For @FILELIST, paths in the file list are relative to the working directory where cog was called. For &FILELIST, paths in the file list are relative to the file list location. OPTIONS: -c Checksum the output to protect it against accidental change. -d Delete the generator code from the output file. -D name=val Define a global string available to your generator code. -e Warn if a file has no cog code in it. -I PATH Add PATH to the list of directories for data files and modules. -n ENCODING Use ENCODING when reading and writing files. -o OUTNAME Write the output to OUTNAME. -p PROLOGUE Prepend the generator source with PROLOGUE. Useful to insert an import line. Example: -p "import math" -P Use print() instead of cog.outl() for code output. -r Replace the input file with the output. -s STRING Suffix all generated output lines with STRING. -U Write the output with Unix newlines (only LF line-endings). -w CMD Use CMD if the output file needs to be made writable. A %s in the CMD will be filled with the filename. -x Excise all the generated output without running the generators. -z The end-output marker can be omitted, and is assumed at eof. -v Print the version of cog and exit. --check Check that the files would not change if run again. --markers='START END END-OUTPUT' The patterns surrounding cog inline instructions. Should include three values separated by spaces, the start, end, and end-output markers. Defaults to '[[[cog ]]] [[[end]]]'. --verbosity=VERBOSITY Control the amount of output. 2 (the default) lists all files, 1 lists only changed files, 0 lists no files. -h Print this help. .. {{{end}}} (checksum: 159e7d7aebb9dcc98f250d47879703dd) In addition to running cog as a command on the command line, you can also invoke it as a module with the Python interpreter: .. code-block:: bash $ python3 -m cogapp [options] [arguments] Note that the Python module is called "cogapp". Input files ----------- Files on the command line are processed as input files. All input files are assumed to be UTF-8 encoded. Using a minus for a filename (``-``) will read the standard input. Files can also be listed in a text file named on the command line with an ``@``: .. code-block:: bash $ cog @files_to_cog.txt File names in the list file are relative to the current directory. You can also use ``&files_to_cog.txt`` and the file names will be relative to the location of the list file. These list files can be nested, and each line can contain switches as well as a file to process. For example, you can create a file cogfiles.txt: .. code-block:: text # These are the files I run through cog mycode.cpp myothercode.cpp myschema.sql -s " --**cogged**" readme.txt -s "" then invoke cog like this: .. code-block:: bash $ cog -s " //**cogged**" @cogfiles.txt Now cog will process four files, using C++ syntax for markers on all the C++ files, SQL syntax for the .sql file, and no markers at all on the readme.txt file. As another example, cogfiles2.txt could be: .. code-block:: text template.h -D thefile=data1.xml -o data1.h template.h -D thefile=data2.xml -o data2.h with cog invoked like this: .. code-block:: bash $ cog -D version=3.4.1 @cogfiles2.txt Cog will process template.h twice, creating both data1.h and data2.h. Both executions would define the variable version as "3.4.1", but the first run would have thefile equal to "data1.xml" and the second run would have thefile equal to "data2.xml". Overwriting files ----------------- The ``-r`` flag tells cog to write the output back to the input file. If the input file is not writable (for example, because it has not been checked out of a source control system), a command to make the file writable can be provided with ``-w``: .. code-block:: bash $ cog -r -w "p4 edit %s" @files_to_cog.txt Setting globals --------------- Global values can be set from the command line with the ``-D`` flag. For example, invoking Cog like this: .. code-block:: bash $ cog -D thefile=fooey.xml mycode.txt will run Cog over mycode.txt, but first define a global variable called thefile with a value of "fooey.xml". This variable can then be referenced in your generator code. You can provide multiple ``-D`` arguments on the command line, and all will be defined and available. The value is always interpreted as a Python string, to simplify the problem of quoting. This means that: .. code-block:: bash $ cog -D NUM_TO_DO=12 will define ``NUM_TO_DO`` not as the integer ``12``, but as the string ``"12"``, which are different and not equal values in Python. Use `int(NUM_TO_DO)` to get the numeric value. Checksummed output ------------------ If cog is run with the ``-c`` flag, then generated output is accompanied by a checksum: .. code-block:: sql --[[[cog -- import cog -- for i in range(10): -- cog.out("%d " % i) --]]] 0 1 2 3 4 5 6 7 8 9 --[[[end]]] (checksum: bd7715304529f66c4d3493e786bb0f1f) If the generated code is edited by a misguided developer, the next time cog is run, the checksum won't match, and cog will stop to avoid overwriting the edited code. Continuous integration ---------------------- You can use the ``--check`` option to run cog just to check that the files would not change if run again. This is useful in continuous integration to check that your files have been updated properly. Output line suffixes -------------------- To make it easier to identify generated lines when grepping your source files, the ``-s`` switch provides a suffix which is appended to every non-blank text line generated by Cog. For example, with this input file (mycode.txt): .. code-block:: text [[[cog cog.outl('Three times:\n') for i in range(3): cog.outl('This is line %d' % i) ]]] [[[end]]] invoking cog like this: .. code-block:: bash $ cog -s " //(generated)" mycode.txt will produce this output: .. code-block:: text [[[cog cog.outl('Three times:\n') for i in range(3): cog.outl('This is line %d' % i) ]]] Three times: //(generated) This is line 0 //(generated) This is line 1 //(generated) This is line 2 //(generated) [[[end]]] Miscellaneous ------------- The ``-n`` option lets you tell cog what encoding to use when reading and writing files. The ``--verbose`` option lets you control how much cog should chatter about the files it is cogging. ``--verbose=2`` is the default: cog will name every file it considers, and whether it has changed. ``--verbose=1`` will only name the changed files. ``--verbose=0`` won't mention any files at all. The ``--markers`` option lets you control the syntax of the marker lines. The value must be a string with two spaces in it. The three markers are the three pieces separated by the spaces. The default value for markers is ``"[[[cog ]]] [[[end]]]"``. The ``-x`` flag tells cog to delete the old generated output without running the generators. This lets you remove all the generated output from a source file. The ``-d`` flag tells cog to delete the generators from the output file. This lets you generate content in a public file but not have to show the generator to your customers. The ``-U`` flag causes the output file to use pure Unix newlines rather than the platform's native line endings. You can use this on Windows to produce Unix-style output files. The ``-I`` flag adds a directory to the path used to find Python modules. The ``-p`` option specifies Python text to prepend to embedded generator source, which can keep common imports out of source files. The ``-z`` flag lets you omit the ``[[[end]]]`` marker line, and it will be assumed at the end of the file. cog-3.4.1/docs/source.rst000066400000000000000000000057221457240776200152750ustar00rootroot00000000000000Writing the source files ======================== Source files to be run through cog are mostly just plain text that will be passed through untouched. The Python code in your source file is standard Python code. Any way you want to use Python to generate text to go into your file is fine. Each chunk of Python code (between the ``[[[cog`` and ``]]]`` lines) is called a generator and is executed in sequence. The output area for each generator (between the ``]]]`` and ``[[[end]]]`` lines) is deleted, and the output of running the Python code is inserted in its place. To accommodate all source file types, the format of the marker lines is irrelevant. If the line contains the special character sequence, the whole line is taken as a marker. Any of these lines mark the beginning of executable Python code: .. code-block:: text //[[[cog /* cog starts now: [[[cog */ -- [[[cog (this is cog Python code) #if 0 // [[[cog Cog can also be used in languages without multi-line comments. If the marker lines all have the same text before the triple brackets, and all the lines in the generator code also have this text as a prefix, then the prefixes are removed from all the generator lines before execution. For example, in a SQL file, this: .. code-block:: sql --[[[cog -- import cog -- for table in ['customers', 'orders', 'suppliers']: -- cog.outl("drop table %s;" % table) --]]] --[[[end]]] will produce this: .. code-block:: sql --[[[cog -- import cog -- for table in ['customers', 'orders', 'suppliers']: -- cog.outl("drop table %s;" % table) --]]] drop table customers; drop table orders; drop table suppliers; --[[[end]]] Finally, a compact form can be used for single-line generators. The begin-code marker and the end-code marker can appear on the same line, and all the text between them will be taken as a single Python line: .. code-block:: cpp // blah blah //[[[cog import MyModule as m; m.generateCode() ]]] //[[[end]]] You can also use this form to simply import a module. The top-level statements in the module can generate the code. If you have special requirements for the syntax of your file, you can use the ``--markers`` option to define new markers. If there are multiple generators in the same file, they are executed with the same globals dictionary, so it is as if they were all one Python module. Cog tries to do the right thing with white space. Your Python code can be block-indented to match the surrounding text in the source file, and cog will re-indent the output to fit as well. All of the output for a generator is collected as a block of text, a common whitespace prefix is removed, and then the block is indented to match the indentation of the cog generator. This means the left-most non-whitespace character in your output will have the same indentation as the begin-code marker line. Other lines in your output keep their relative indentation. cog-3.4.1/pyproject.toml000066400000000000000000000025361457240776200152270ustar00rootroot00000000000000[project] name = "cogapp" description = "Cog: A content generator for executing Python snippets in source files." readme = "README.rst" authors = [ {name = "Ned Batchelder", email = "ned@nedbatchelder.com"}, ] license.text = "MIT" classifiers = [ "Development Status :: 5 - Production/Stable", "Environment :: Console", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Topic :: Software Development :: Code Generators", ] requires-python = ">= 3.7" dynamic = ["version"] [project.scripts] cog = "cogapp:main" [project.urls] "Documentation" = "https://cog.readthedocs.io/" "Code" = "http://github.com/nedbat/cog" "Issues" = "https://github.com/nedbat/cog/issues" "Funding" = "https://github.com/users/nedbat/sponsorship" "Mastodon" = "https://hachyderm.io/@nedbat" [tool.pytest.ini_options] addopts = "-q -rfe" [tool.setuptools] packages = ["cogapp"] [tool.setuptools.dynamic] version.attr = "cogapp.cogapp.__version__" [build-system] requires = ["setuptools"] build-backend = "setuptools.build_meta" cog-3.4.1/requirements.pip000066400000000000000000000000661457240776200155440ustar00rootroot00000000000000build check-manifest coverage Sphinx tox tox-gh twine cog-3.4.1/success/000077500000000000000000000000001457240776200137555ustar00rootroot00000000000000cog-3.4.1/success/README.txt000066400000000000000000000001401457240776200154460ustar00rootroot00000000000000The source of the old Python Success Story about cog: https://www.python.org/about/success/cog/ cog-3.4.1/success/cog-success.rst000066400000000000000000000236551457240776200167400ustar00rootroot00000000000000============================================= Cog: A Code Generation Tool Written in Python ============================================= :Category: Business :Keywords: cpython, code generation, utility, scripting, companion language :Title: Cog: A Code Generation Tool Written in Python :Author: Ned Batchelder :Date: $Date: 2004/05/25 21:12:37 $ :Websites: http://www.nedbatchelder.com/ :Website: http://www.kubisoftware.com/ :Summary: Cog, a general-purpose Python-based code generation tool, is used to speed development of a collaboration system written in C++. :Logo: images/batchelder-logo.gif Introduction ------------ `Cog`__ is a simple code generation tool written in Python. We use it or its results every day in the production of Kubi. __ http://www.nedbatchelder.com/code/cog `Kubi`__ is a collaboration system embodied in a handful of different products. We have a schema that describes the representation of customers' collaboration data: discussion topics, documents, calendar events, and so on. This data has to be handled in many ways: stored in a number of different data stores, shipped over the wire in an XML representation, manipulated in memory using traditional C++ objects, presented for debugging, and reasoned about to assess data validity, to name a few. __ http://www.kubisoftware.com/ We needed a way to describe this schema once and then reliably produce executable code from it. The Hard Way with C++ --------------------- Our first implementation of this schema involved a fractured collection of representations. The XML protocol module had tables describing the serialization and deserialization of XML streams. The storage modules had other tables describing the mapping from disk to memory structures. The validation module had its own tables containing rules about which properties had to be present on which items. The in-memory objects had getters and setters for each property. It worked, after a fashion, but was becoming unmanageable. Adding a new property to the schema required editing ten tables in different formats in as many source files, as well as adding getters and setters for the new property. There was no single authority in the code for the schema as a whole. Different aspects of the schema were represented in different ways in different files. We tried to simplify the mess using C++ macros. This worked to a degree, but was still difficult to manage. The schema representation was hampered by the simplistic nature of C++ macros, and the possibilities for expansion were extremely limited. The schema tables that could not be created with these primitive macros were still composed and edited by hand. Changing a property in the schema still meant touching a dozen files. This was tedious and error prone. Missing one place might introduce a bug that would go unnoticed for days. Searching for a Better Way -------------------------- It was becoming clear that we needed a better way to manage the property schema. Not only were the existing modifications difficult, but new areas of development were going to require new uses of the schema, and new kinds of modification that would be even more onerous. We'd been using C++ macros to try to turn a declarative description of the schema into executable code. The better way to do it is with code generation: a program that writes programs. We could use a tool to read the schema and generate the C++ code, then compile that generated code into the product. We needed a way to read the schema description file and output pieces of code that could be integrated into our C++ sources to be compiled with the rest of the product. Rather than write a program specific to our problem, I chose instead to write a general-purpose, although simple, code generator tool. It would solve the problem of managing small chunks of generator code sprinkled throughout a large collection of files. We could then use this general purpose tool to solve our specific generation problem. The tool I wrote is called Cog. Its requirements were: * We needed to be able to perform interesting computation on the schema to create the code we needed. Cog would have to provide a powerful language to write the code generators in. An existing language would make it easier for developers to begin using Cog. * I wanted developers to be able to change the schema, and then run the tool without having to understand the complexities of the code generation. Cog would have to make it simple to combine the generated chunks of code with the rest of the C++ source, and it should be simple to run Cog to generate the final code. * The tool shouldn't care about the language of the host file. We originally wanted to generate C++ files, but we were branching out into other languages. The generation process should be a pure text process, without regard to the eventual interpretation of that text. * Because the schema would change infrequently, the generation of code should be an edit-time activity, rather than a build-time activity. This avoided having to run the code generator as part of the build, and meant that the generated code would be available to our IDE and debugger. Code Generation with Python --------------------------- The language I chose for the code generators was, of course, Python. Its simplicity and power are perfect for the job of reading data files and producing code. To simplify the integration with the C++ code, the Python generators are inserted directly into the C++ file as comments. Cog reads a text file (C++ in our case), looking for specially-marked sections of text, that it will use as generators. It executes those sections as Python code, capturing the output. The output is then spliced into the file following the generator code. Because the generator code and its output are both kept in the file, there is no distinction between the input file and output file. Cog reads and writes the same file, and can be run over and over again without losing information. .. figure:: images/cog-web.png :alt: Cog's Processing Model *Cog processes text files, converting specially marked sections of the file into new content without disturbing the rest of the file or the sections that it executes to produce the generated content.* `Zoom in`__ __ images/cog.png In addition to executing Python generators, Cog itself is written in Python. Python's dynamic nature made it simple to execute the Python code Cog found, and its flexibility made it possible to execute it in a properly-constructed environment to get the desired semantics. Much of Cog's code is concerned with getting indentation correct: I wanted the author to be able to organize his generator code to look good in the host file, and produce generated code that looked good as well, without worrying about fiddly whitespace issues. Python's OS-level integration let me execute shell commands where needed. We use Perforce for source control, which keeps files read-only until they need to be edited. When running Cog, it may need to change files that the developer has not edited yet. It can execute a shell command to check out files that are read-only. Lastly, we used XML for our new property schema description, and Python's wide variety of XML processing libraries made parsing the XML a snap. An Example ---------- Here's a concrete but slightly contrived example. The properties are described in an XML file:: We can write a C++ file with inlined Python code:: // SchemaPropEnum.h enum SchemaPropEnum { /* [[[cog import cog, handyxml for p in handyxml.xpath('Properties.xml', '//property'): cog.outl("Property%s," % p.name) ]]] */ // [[[end]]] }; After running this file through Cog, it looks like this:: // SchemaPropEnum.h enum SchemaPropEnum { /* [[[cog import cog, handyxml for p in handyxml.xpath('Properties.xml', '//property'): cog.outl("Property%s," % p.name) ]]] */ PropertyId, PropertyRevNum, PropertySubject, PropertyModDate, // [[[end]]] }; The lines with triple-brackets are marker lines that delimit the sections Cog cares about. The text between the **[[[cog and ]]]** lines is generator Python code. The text between **]]]** and **[[[end]]]** is the output from the last run of Cog (if any). For each chunk of generator code it finds, Cog will: 1. discard the output from the last run, 2. execute the generator code, 3. capture the output, from the cog.outl calls, and 4. insert the output back into the output section. How It Worked Out ----------------- In a word, great. We now have a powerful tool that lets us maintain a single XML file that describes our data schema. Developers changing the schema have a simple tool to run that generates code from the schema, producing output code in four different languages across 50 files. Where we once used a repetitive and aggravating process that was inadequate to our needs, we now have an automated process that lets developers express themselves and have Cog do the hard work. Python's flexibility and power were put to work in two ways: to develop Cog itself, and sprinkled throughout our C++ source code to give our developers a powerful tool to turn static data into running code. Although our product is built in C++, we've used Python to increase our productivity and expressive power, ease maintenance work, and automate error-prone tasks. Our shipping software is built every day with Python hard at work behind the scenes. More information, and Cog itself, is available at http://www.nedbatchelder.com/code/cog About the Author ---------------- *Ned Batchelder is a professional software developer who struggles along with C++, using Python to ease the friction every chance he gets. A previous project of his,* `Natsworld`__, *was the subject of an earlier Python Success Story.* __ /success&story=natsworld cog-3.4.1/success/cog.png000066400000000000000000000236041457240776200152400ustar00rootroot00000000000000PNG  IHDRls bKGD pHYsNtIME)?u IDATx]r۸PVOd顧3cz>i `T$K( = {X&''3{gd[oߒN(Q[x<<҇>R؀C#Jy<mR؀ݦFJ l۩mR؀͖FJ lSS2 6 XuTG l=B5R1%92TK`x Fۀ=6?5Iht]w~ħygml@Qe #AR؎< m z4v5gm6hU#83;hؠ!Wڴξ(m 1RJ`K#A6\љ[ia $ArȴRNg/m *t(L-2ؠ"wخF۠ Tіw6(\"|(mHO`BE+˝"}hm8O`E)!Nc6(H䑓%?rؠGK\X&ApD,W+Җ lXI"%+KjW; *q$r>m Z&A0~XR+ޠ5QG%?mZ A5rXRVܨ|$D`6q6 />ߧNc<s>:5uO(:q&-myQB6ȨQm[ۖ &mWsRlA #Wl: lZhket"e`9I[y]+"HDڇmkl= lpG!r%:~|j>%z9E`Zyh1 [n>j`[op5 N2:\I`*|h=,I]끥?f6Kil³,W`ckw0Yсs6X!m#j!:']X5:uՂs&mg6`$`;Fئ[KN7Y` F# lGX` 9ۘe lYO!R`[γh5uGG끥*|huGGuFKdFXS_w68l?pKcuճv.E9gZ%Q-kۤ Z۹?ںNpl"*Y &eخ l)sZ"QkۥX[KZ~XiF5mosQ[MSZnS;YP%]Z^P3Y@%7u%FIseZsE`Vj#QkyB`֘="z[wP"XK玂z[(Fh:*!V)FH1wÃ\} ~ l=8eP 0XG#R{(Fr;$ E;DQD!"ظ5h[i^JO"*X=.bA8O9E4'qSj= D$*"ZqыoZuJ",SB-aGRø嬝SJ-e>Gޜ^wظ5sJ+Jgd6 q kXTKJ%kd9rHʚyR;>i>\6y^EyO_B["}__ F8ĚdJ]C-+Q#IM`cR$ZJ>jiQC`c3kRi/(`66)EҲo)ac5tjGHYV#O(Y5(j+|65|O(7$ ^ !$p$5EPKBSV&Hl!u]%6rN-_m+!FDg.=9:߾}'3[ìͥWk!VҨ{GQS[! AQkS8ҪGfH`kLM"Z rZ~$5aM5\S$U+]OPSaDwmsy+UujkB5~ϵ/$UBEIɫe$XVHG{Jvlӏs{ m~Au-EqU|\%~F gZ(ZV(jmG eMz{,-VQ@vQTXj]ZY ֘R1ёVrQ; Ep0B^P#lY3ʣ.CSk]jJjﬣhhhWejmvU#- k@ytpAO']7jMY`jh@h[ekq*V-t5|Z k]LZ>s2a$V5:;F&%ݨ7VvVV~V(Zig%n` &֩UhZY+m-wZZ}[^hE'ebM%;zlZ[^P( ZT#C=KYgBHөP[me~j=։F#ZnYvw7-1kЁ<[oAG`K3 ǻځe?t5谡]d. 2b{bi dM>::F.ŢF'bgA;dICK #dVۉ"3 ljJBFVX3R~cۋz.IhGbjM%Mp?tprџĤ#ܩEsvI -#WrA#O \W* atORҞ7il} :y:/]w"S5tX14i\ID'}\`YSLA;@;}L`pp29ioB[ F!šc/e&|M"EX[rg}Z6P,:u,w>Z.#__~e"z;)og-&l(yx7-*:h)aBGWap4Jw?_ *~TTg+D>,b[ISE_Ƕ] iݵZe1#t$ɺt mXNg,5[Tq;#㷬"PA:8Na@ K]zRUՒk%oRa((u^b+Oə"t`Iij|՞m iB[Jl![ f[;ƴiB[J!.:F&\A{fd\0t[3/$a+%^9ڶP^f,W}k`+!Q92Pศfafw?}glQ+"E,Y:`Ck8ox;daA!k8_5Z"2Vܕm. l^Z倶nQ5ZH 5rf͂JkyJ!ur$GUw :=fMVeA`GmUWÁͨ-(y\6 !- lpG;R/hY+UuwjGB-K6t`{kǯ۾c|oVgrQ5xgt>wR+p.S-믿]?p2B\onZ/Z l:7xgYϖ~nnap#xwdY Aბ.(( J>:(Y\iamߧ@)6k#js;?}%㜰xDMx[S6ZP 4Fl l lMWt磃l l lMWtv㑬ЦQS躋[{^R(Aujz<ID`ޟ=6|ߧwS=]{@KԔ_{ߵQd0\pzpϙkㅥs]<.ݏV Yz hk S4_[B pV>s$mii'ij~%;|&GTb4^c 5EM\f4A> :w#`n:LoN5w[/TKc_yܻ߭/–*X)j N \[&wɉss[:񙳧Τ=>#{6uj g^e@CR/@Jj Q֮ݖ=<ǦvoookuݧSLq~~nGU~R1Dx{{5v;өBDtjt||>+BTU- _/ oO צǦ^6l>~|9SSvt_6}o gEsjjj Q@666 #'mԟORSK`(& 1C6]Ajpzlx]{YKp?5TƎ6ԩ]=l 65\ l)] cv),i;_.칶#h_-Xq)RxyXz>7O]l]PqZbSSCsDJ#4ƨ G`N`N`l68G`N`N`l666668!3G`N`N`l68!3珵'14@Mc'A`6]u6.kǏտmVoKw* x{oC_{*+fVSÁm>6tuĭka?~8ilhxSS`YX"-`yoX"p蠃H ^GK:x<~TWM6oSLgIܵ).[5DjhBWLm϶+)a[kB]\|7GMmE4ܱ屩!74Y5 l 9t{nRR=*ՑPSîvvWqΆ>5EMasa{<t/_d8'۞USXMq-Ql&?3|51\->wP}uԔX?~E%hsr}hmJWmjJRva#~Y㿿L>?6~Ҵ^_zϔrR]~|.H@^j2>5Քl#l? ܩ?6~|csz3~ߵkw{zCSϙ)jkj)tp]NfxosִCM 54Ք;6 \@|tf}3EԔs"daKyN3]l[.lkY5<{t fĤN~CMFM)_KS.Sti[eK榽nΪyKS muJjRW::)/M_#9g;g>3㵪+H))e G&BXڈFMCM'A?L罵 숸SMyZc@QSSSʔ+֌8Ԏif|u[^fiKM=4Sp'5EMiE挻:)2pL \EM!#52\XzΖ65Ȋ֡`>%Y#l@^>ۿc>hal6ج }ׯ_Ƕ<^9q|@Jj l; wlylso[Տԝ j lSi= BeMɢM%W5[] _dj )&&^__?u{XR_5}ۼO1%)j I>-@ܥoMAn_1dj )&-&y9R-Jmj U UjHlքUSHSSԉs<#,XHs>q೚F.`TsZ)4к?3|51ҟ l@@tZOo!J?4 51c5Jsh`tft̂0Μ^%jՃ{kԖ[wjF5+Z?=lZ Sˁۿ3gU5e8a]֕}_75-Z?[_7Z LGwOh[~ysOE3T-]Vy.2~lꤌó[My>o`ennm}_}G|ioBilˆ/=2~l& mqpm+thKK(!)6vtX0y}}}SmrZ-xբo,7RS(A PDB=Dp4ڜ7|< tĹG s#~b%J]MMEKM={i?#A| ]\ke3mؠVg{&+ϡz///E>yʡp*N1wNO~ܭәza>|Ҿ"*~ohKL~pS iϋ5#خ6jRS+E]f Gۆ#8OVS 5fF]\#Rp0W&= ֆ=OJsm._?GM!1kIDATGضc0s8֦e{Yk Th;ROk)Dx.ǣҶ|z///_|}m[v);RS$!@͖GAש)W&ѫX j%H槦p6VG@8f]'4'幀ߝ:ׯݏ?V6|7Ƕ<~V?=uv#(0/QÆ4P-ǏOa{[z|wٕRS9҆ÁmЇ )wC[6ܵ绲&6'ΥMG۫r҆ an {sk"S9߹J)˪o5^Fآ%e^#6hs^__ow(w]]:SS(A8oÂwgs%5e;5~(ݖp\%:5<>fmi?ݕd9G஠ծ{ K`ѐSShW`K/v8K/4CN-X@ Ύ)5x<нw ƾ|qnLj ;RS\K1% Y$'|51t@ l l l l l)z" 8 8  P 8 8  P 8 8  P 8 8  P 8féHBM%M.צ IljJH=㑴 9S?g{mkm)~<%jZRdm5\;^X-K]2ڭ2T\'YVB޲qnn5# &5{w+YVrŽ:WWUtHMIOM)_l-X9}Z *5EM^35=#s:KOp$|ͱHMY\MiCu?{5ggWL;k-T@ԔՔ]_SE6\7@RZnJuvl\?k= ?s@A׽/ ooo%@ [CgGjM3HK` ! #py86 k @y666̦+N`h2Cy666̦+N`O`N`N`N`h2Cy666̦+N`h2Cy666+Rz< l l l lI %4BXr l lPE&A!lh#P6 r l֠Ʀ-lPG52kP?֞(@L}hS])pb`[[矤3@8&Q(XQ*as#K`nTK5!AVO/ yP76@ lЬѤq!(Q΀=LƯm5`#l9J eIENDB`cog-3.4.1/tox.ini000066400000000000000000000006271457240776200136250ustar00rootroot00000000000000# tox configuration for Cog. [tox] envlist = py37,py38,py39,py310,py311,py312,coverage [testenv] deps = coverage pytest commands = coverage run -m pytest {posargs} usedevelop = True [testenv:coverage] skip_install = True commands = coverage combine -q coverage report -m [gh] python = 3.7 = py37 3.8 = py38 3.9 = py39 3.10 = py310 3.11 = py311 3.12 = py312