pax_global_header 0000666 0000000 0000000 00000000064 14572407762 0014530 g ustar 00root root 0000000 0000000 52 comment=cceb6cbbc67c7bb35758d5ba7772a8482d143b56
cog-3.4.1/ 0000775 0000000 0000000 00000000000 14572407762 0012305 5 ustar 00root root 0000000 0000000 cog-3.4.1/.coveragerc 0000664 0000000 0000000 00000000465 14572407762 0014433 0 ustar 00root root 0000000 0000000 # 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/.editorconfig 0000664 0000000 0000000 00000001052 14572407762 0014760 0 ustar 00root root 0000000 0000000 # 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/ 0000775 0000000 0000000 00000000000 14572407762 0013645 5 ustar 00root root 0000000 0000000 cog-3.4.1/.github/workflows/ 0000775 0000000 0000000 00000000000 14572407762 0015702 5 ustar 00root root 0000000 0000000 cog-3.4.1/.github/workflows/ci.yml 0000664 0000000 0000000 00000003677 14572407762 0017035 0 ustar 00root root 0000000 0000000 name: "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/.gitignore 0000664 0000000 0000000 00000000341 14572407762 0014273 0 ustar 00root root 0000000 0000000 # 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.yaml 0000664 0000000 0000000 00000000526 14572407762 0015537 0 ustar 00root root 0000000 0000000 # 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.txt 0000664 0000000 0000000 00000000415 14572407762 0014173 0 ustar 00root root 0000000 0000000 Cog 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.rst 0000664 0000000 0000000 00000025411 14572407762 0014331 0 ustar 00root root 0000000 0000000 Changelog
=========
..
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.txt 0000664 0000000 0000000 00000002064 14572407762 0014132 0 ustar 00root root 0000000 0000000 MIT 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.in 0000664 0000000 0000000 00000000524 14572407762 0014044 0 ustar 00root root 0000000 0000000 include .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/Makefile 0000664 0000000 0000000 00000003102 14572407762 0013741 0 ustar 00root root 0000000 0000000 # 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.rst 0000664 0000000 0000000 00000002371 14572407762 0013777 0 ustar 00root root 0000000 0000000 ===
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/ 0000775 0000000 0000000 00000000000 14572407762 0013556 5 ustar 00root root 0000000 0000000 cog-3.4.1/cogapp/__init__.py 0000664 0000000 0000000 00000000243 14572407762 0015666 0 ustar 00root root 0000000 0000000 """ 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__.py 0000664 0000000 0000000 00000000166 14572407762 0015653 0 ustar 00root root 0000000 0000000 """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.py 0000664 0000000 0000000 00000072101 14572407762 0015402 0 ustar 00root root 0000000 0000000 """ 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.py 0000664 0000000 0000000 00000002135 14572407762 0016071 0 ustar 00root root 0000000 0000000 """ 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.py 0000664 0000000 0000000 00000242332 14572407762 0016446 0 ustar 00root root 0000000 0000000 """ 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.py 0000664 0000000 0000000 00000006667 14572407762 0017146 0 ustar 00root root 0000000 0000000 """ 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.py 0000664 0000000 0000000 00000007533 14572407762 0017400 0 ustar 00root root 0000000 0000000 """ 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.py 0000664 0000000 0000000 00000002773 14572407762 0015301 0 ustar 00root root 0000000 0000000 """ 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.py 0000664 0000000 0000000 00000003611 14572407762 0016332 0 ustar 00root root 0000000 0000000 """ 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/ 0000775 0000000 0000000 00000000000 14572407762 0013235 5 ustar 00root root 0000000 0000000 cog-3.4.1/docs/Makefile 0000664 0000000 0000000 00000001172 14572407762 0014676 0 ustar 00root root 0000000 0000000 # 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.rst 0000664 0000000 0000000 00000000054 14572407762 0015376 0 ustar 00root root 0000000 0000000 .. _changes:
.. include:: ../CHANGELOG.rst
cog-3.4.1/docs/conf.py 0000664 0000000 0000000 00000001727 14572407762 0014543 0 ustar 00root root 0000000 0000000 # 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.rst 0000664 0000000 0000000 00000002302 14572407762 0015235 0 ustar 00root root 0000000 0000000 Design
======
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.rst 0000664 0000000 0000000 00000010517 14572407762 0015102 0 ustar 00root root 0000000 0000000 ===
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.rst 0000664 0000000 0000000 00000003466 14572407762 0015265 0 ustar 00root root 0000000 0000000 The 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.rst 0000664 0000000 0000000 00000021533 14572407762 0015453 0 ustar 00root root 0000000 0000000 Running 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.rst 0000664 0000000 0000000 00000005722 14572407762 0015275 0 ustar 00root root 0000000 0000000 Writing 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.toml 0000664 0000000 0000000 00000002536 14572407762 0015227 0 ustar 00root root 0000000 0000000 [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.pip 0000664 0000000 0000000 00000000066 14572407762 0015544 0 ustar 00root root 0000000 0000000 build
check-manifest
coverage
Sphinx
tox
tox-gh
twine
cog-3.4.1/success/ 0000775 0000000 0000000 00000000000 14572407762 0013755 5 ustar 00root root 0000000 0000000 cog-3.4.1/success/README.txt 0000664 0000000 0000000 00000000140 14572407762 0015446 0 ustar 00root root 0000000 0000000 The source of the old Python Success Story about cog:
https://www.python.org/about/success/cog/
cog-3.4.1/success/cog-success.rst 0000664 0000000 0000000 00000023655 14572407762 0016740 0 ustar 00root root 0000000 0000000 =============================================
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.png 0000664 0000000 0000000 00000023604 14572407762 0015240 0 ustar 00root root 0000000 0000000 PNG
IHDR l s
bKGD pHYs N tIME)?u IDATx]r۸PVOd 顧3cz>i
`T$K( = { X& ' ' 3 {gd[oߒN(Q[x<<҇> RC#Jy<mRݦFJl۩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>mZ&A0~XR+ޠ5QG%?mZ A 5rXRVܨ|$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"ZrZ~$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`jh@h[ekq*V-t5|Zk]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$ɺtmXNg,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倶nQ5ZH5rf͂JkyJ!ur$GUw
:=fMVeA`GmUWÁͨ-(y\6!-lpG;R/hY+UuwjGB-K6t`{kǯ۾c|oVgrQ5xgt>wR+p.S-믿]?p2B\onZ/Zl:7xgYϖ~nnap#xwdY Aბ.( (
J>:(Y\iamߧ@)6k#js; ?}%㜰xDMx[S6ZP 4Fl l lMW t磃l l lMW tv㑬Ц QS躋[{^R(Aujz<ID`ޟ=6|ߧwS=]{@KԔ_{ߵQd0\pzpϙkㅥs]<.ݏV Yz
hkS4_[B
pV>s$mii'ij~%;|>b4^c 5EM\f4A>:w#`n:LoN5w[/TKc_yܻ߭/*X)j
N \[&