summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/cmake.yml2
-rw-r--r--.github/workflows/nix-flake.yml2
-rw-r--r--CMakeLists.txt9
-rw-r--r--README.md2
-rwxr-xr-xacprep23
-rwxr-xr-xcontrib/getquote-uk.py13
-rwxr-xr-xcontrib/ledger-du32
-rwxr-xr-xcontrib/non-profit-audit-reports/csv2ods.py38
-rw-r--r--contrib/non-profit-audit-reports/ooolib2/__init__.py3930
-rwxr-xr-xcontrib/non-profit-audit-reports/readcsv.py8
-rw-r--r--doc/ledger.120
-rw-r--r--flake.lock6
-rw-r--r--flake.nix2
-rwxr-xr-xpython/demo.py2
-rw-r--r--src/query.cc4
-rw-r--r--test/CMakeLists.txt12
-rwxr-xr-xtest/CheckBaselineTests.py26
-rw-r--r--test/CheckComments.py46
-rwxr-xr-xtest/CheckManpage.py24
-rwxr-xr-xtest/CheckOptions.py29
-rwxr-xr-xtest/CheckTexinfo.py24
-rwxr-xr-xtest/ConfirmTests.py26
-rwxr-xr-xtest/DocTests.py3
-rwxr-xr-xtest/GenerateTests.py41
-rwxr-xr-xtest/LedgerHarness.py90
-rwxr-xr-xtest/RegressTests.py141
-rw-r--r--test/baseline/feat-value_py.test (renamed from test/baseline/feat-value_py3.test)0
-rw-r--r--test/baseline/feat-value_py2.test23
-rwxr-xr-xtest/convert.py2
-rw-r--r--test/manual/transaction-notes-1.test11
-rw-r--r--test/python/JournalTest.py2
-rw-r--r--test/python/PostingTest.py2
-rw-r--r--test/python/TransactionTest.py2
-rw-r--r--test/regress/1182_2.test2
-rw-r--r--test/regress/B21BF389.py3
-rw-r--r--test/unit/t_expr.cc4
-rwxr-xr-xtools/average6
-rwxr-xr-xtools/genuuid29
38 files changed, 2321 insertions, 2320 deletions
diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml
index 62674d77..8d3456ad 100644
--- a/.github/workflows/cmake.yml
+++ b/.github/workflows/cmake.yml
@@ -53,7 +53,7 @@ jobs:
brew list -1 | grep python | while read formula; do brew unlink $formula; brew link --overwrite $formula; done
brew update
- brew install boost boost-python3 gmp mpfr gpgme
+ brew install --force --overwrite boost boost-python3 gmp mpfr icu4c gpgme
- name: Configure CMake
# Configure CMake in a 'build' subdirectory. `CMAKE_BUILD_TYPE` is only required if you are using a single-configuration generator such as make.
diff --git a/.github/workflows/nix-flake.yml b/.github/workflows/nix-flake.yml
index 0d526236..757b022f 100644
--- a/.github/workflows/nix-flake.yml
+++ b/.github/workflows/nix-flake.yml
@@ -13,7 +13,7 @@ jobs:
os: [ubuntu-latest, macos-latest]
steps:
- - uses: cachix/install-nix-action@v23
+ - uses: cachix/install-nix-action@v24
with:
nix_path: nixpkgs=channel:nixos-unstable
diff --git a/CMakeLists.txt b/CMakeLists.txt
index ab17cedd..e6b1270d 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -107,6 +107,9 @@ endif()
########################################################################
+include(FindICU)
+find_package(ICU 63 OPTIONAL_COMPONENTS uc i18n)
+
include(CheckIncludeFiles)
include(CheckLibraryExists)
include(CheckFunctionExists)
@@ -159,8 +162,8 @@ endif()
cmake_push_check_state()
-set(CMAKE_REQUIRED_INCLUDES ${CMAKE_INCLUDE_PATH} ${Boost_INCLUDE_DIRS})
-set(CMAKE_REQUIRED_LIBRARIES ${Boost_LIBRARIES} icuuc ${PROFILE_LIBS})
+set(CMAKE_REQUIRED_INCLUDES ${CMAKE_INCLUDE_PATH} ${Boost_INCLUDE_DIRS} ${ICUE_INCLUDE_DIRS})
+set(CMAKE_REQUIRED_LIBRARIES ${Boost_LIBRARIES} ${ICU_LIBRARIES} ${PROFILE_LIBS})
check_cxx_source_runs("
#include <boost/regex/icu.hpp>
@@ -298,7 +301,7 @@ macro(add_ledger_library_dependencies _target)
target_link_libraries(${_target} ${Boost_LIBRARIES})
endif()
if (HAVE_BOOST_REGEX_UNICODE)
- target_link_libraries(${_target} icuuc)
+ target_link_libraries(${_target} ${ICU_LIBRARIES})
endif()
target_link_libraries(${_target} ${PROFILE_LIBS})
endmacro(add_ledger_library_dependencies _target)
diff --git a/README.md b/README.md
index 76563266..caf3e92f 100644
--- a/README.md
+++ b/README.md
@@ -63,6 +63,7 @@ Dependency | Version (or greater)
[Gmp] | 6.1.2
[Mpfr] | 4.0.2
[utfcpp] | 3.2.3
+[ICU] | 63 _optional_
[gettext] | 0.17 _optional_
[libedit] | 20090111-3.0 _optional_
[Python] | 3.9 _optional_
@@ -189,6 +190,7 @@ hack as much as you like, then [open a pull request on GitHub](https://github.co
[GMP]: https://gmplib.org/
[MPFR]: https://www.mpfr.org/
[utfcpp]: https://utfcpp.sourceforge.net
+[ICU]: https://icu.unicode.org
[gettext]: https://www.gnu.org/software/gettext/
[libedit]: https://thrysoee.dk/editline/
[Python]: https://python.org
diff --git a/acprep b/acprep
index c6665e24..0322d886 100755
--- a/acprep
+++ b/acprep
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
# acprep, version 3.1
#
@@ -63,7 +63,7 @@ class BoostInfo(object):
if system in ['centos']:
return [ 'boost-devel' ]
- elif system in ['ubuntu-focal', 'ubuntu-bionic', 'ubuntu-xenial',
+ elif system in ['ubuntu-jammy', 'ubuntu-focal', 'ubuntu-bionic', 'ubuntu-xenial',
'ubuntu-eoan', 'ubuntu-trusty', 'ubuntu-cosmic']:
return [ 'libboost-dev',
'libboost-date-time-dev',
@@ -549,7 +549,24 @@ class PrepareBuild(CommandLineApp):
'sudo', 'apt-get', 'install',
'build-essential',
]
- if release == 'focal':
+ if release == 'jammy':
+ packages.extend([
+ 'doxygen',
+ 'cmake',
+ 'ninja-build',
+ 'zlib1g-dev',
+ 'libbz2-dev',
+ 'python-dev',
+ 'libgmp3-dev',
+ 'libmpfr-dev',
+ 'gettext',
+ 'libedit-dev',
+ 'texinfo',
+ 'lcov',
+ 'libutfcpp-dev',
+ 'sloccount'
+ ])
+ elif release == 'focal':
packages.extend([
'doxygen',
'cmake',
diff --git a/contrib/getquote-uk.py b/contrib/getquote-uk.py
index a69d4e7d..0c6c052a 100755
--- a/contrib/getquote-uk.py
+++ b/contrib/getquote-uk.py
@@ -1,7 +1,6 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
+#!/usr/bin/env python3
-import urllib, string, sys
+import urllib, string, sys, os
def download(sym):
url = "http://uk.old.finance.yahoo.com/d/quotes.csv?s="
@@ -13,11 +12,13 @@ def download(sym):
result = float(fields[1])/100
return result
-
+if len(sys.argv) == 1:
+ print(f'USAGE: {os.path.basename(__file__)} SYMBOL', file=sys.stderr)
+ sys.exit(-1)
sym = sys.argv[1]
sym = sym.replace('_', '.')
if sym == '£':
- print '£1.00'
+ print('£1.00')
else:
- try: print "£" +str(download(sym))
+ try: print(f'£ {str(download(sym))}')
except: pass
diff --git a/contrib/ledger-du b/contrib/ledger-du
index 580e916e..fe5a0706 100755
--- a/contrib/ledger-du
+++ b/contrib/ledger-du
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
import string
import sys
@@ -9,28 +9,28 @@ from stat import *
from os.path import *
def report_file(path):
- dir_elems = string.split(dirname(path), os.sep)
+ dir_elems = dirname(path).split(os.sep)
if dir_elems[0] == "." or dir_elems[0] == "":
- dir_elems = dir_elems[1 :]
- account = string.join(dir_elems, ":")
+ dir_elems = dir_elems[1 :]
+ account = ":".join(dir_elems)
info = os.stat(path)
- print time.strftime("%Y/%m/%d", time.localtime(info[ST_MTIME])),
+ print(time.strftime("%Y/%m/%d", time.localtime(info[ST_MTIME])))
- print basename(path)
- print " ", account, " ", info[ST_SIZE], "b"
- print " Equity:Files"
- print
+ print(f'''{basename(path)}
+ \t{account} {info[ST_SIZE]}b
+ \tEquity:Files
+ ''')
def find_files(path):
xacts = os.listdir(path)
for xact in xacts:
xact = join(path, xact)
- if not islink(xact):
- if isdir(xact) and xact != "/proc":
- find_files(xact)
- else:
- report_file(xact)
+ if not islink(xact):
+ if isdir(xact) and xact != "/proc":
+ find_files(xact)
+ else:
+ report_file(xact)
args = sys.argv[1:]
if len(args):
@@ -38,12 +38,12 @@ if len(args):
else:
paths = ["."]
-print """
+print("""
C 1.00 Kb = 1024 b
C 1.00 Mb = 1024 Kb
C 1.00 Gb = 1024 Mb
C 1.00 Tb = 1024 Gb
-"""
+""")
for path in paths:
find_files(path)
diff --git a/contrib/non-profit-audit-reports/csv2ods.py b/contrib/non-profit-audit-reports/csv2ods.py
index 6aabcb59..d2eda740 100755
--- a/contrib/non-profit-audit-reports/csv2ods.py
+++ b/contrib/non-profit-audit-reports/csv2ods.py
@@ -1,4 +1,4 @@
-#!/usr/bin/python
+#!/usr/bin/env python3
# csv2ods.py
# Convert example csv file to ods
#
@@ -25,10 +25,15 @@ import csv
import ooolib2
import shutil
import string
-from Crypto.Hash import SHA256
+try:
+ from Crypto.Hash import SHA256
+except ModuleNotFoundError:
+ print("Missing pycrypto")
+ #sys.exit(-1)
+
def err(msg):
- print 'error: %s' % msg
+ print(f'error: {msg}')
sys.exit(1)
def ReadChecksums(inputFile):
@@ -57,7 +62,7 @@ def ChecksumFile(filename):
def main():
program = os.path.basename(sys.argv[0])
- print get_file_checksum(sys.argv[1])
+ print(get_file_checksum(sys.argv[1]))
def csv2ods(csvname, odsname, encoding='', singleFileDirectory=None, knownChecksums={}, verbose = False):
filesSavedinManifest = {}
@@ -66,7 +71,7 @@ def csv2ods(csvname, odsname, encoding='', singleFileDirectory=None, knownChecks
checksumCache = {}
if verbose:
- print 'converting from %s to %s' % (csvname, odsname)
+ print(f'converting from {csvname} to {odsname}')
if singleFileDirectory:
if not os.path.isdir(os.path.join(os.getcwd(),singleFileDirectory)):
@@ -88,14 +93,14 @@ def csv2ods(csvname, odsname, encoding='', singleFileDirectory=None, knownChecks
csvdir = os.path.dirname(csvname)
if len(csvdir) == 0:
csvdir = '.'
- csvfile = open(csvname, 'rb')
+ csvfile = open(csvname, 'r')
reader = csv.reader(csvfile, delimiter=',', quotechar='"')
for fields in reader:
if len(fields) > 0:
for col in range(len(fields)):
val = fields[col]
if encoding != '' and val[0:5] != "link:": # Only utf8 encode if it's not a filename
- val = unicode(val, 'utf8')
+ val = val.encode('utf-8')
if len(val) > 0 and val[0] == '$':
doc.set_cell_value(col + 1, row, 'currency', val[1:])
else:
@@ -116,7 +121,7 @@ def csv2ods(csvname, odsname, encoding='', singleFileDirectory=None, knownChecks
if knownChecksums.has_key(checksum):
newFile = knownChecksums[checksum]
- print "FOUND new file in known: " + newFile
+ print(f'FOUND new file in known: {newFile}')
if not newFile:
relativeFileWithPath = os.path.basename(val)
@@ -161,10 +166,10 @@ def csv2ods(csvname, odsname, encoding='', singleFileDirectory=None, knownChecks
filesSavedinManifest[newFile] = val
if not os.path.exists(linkpath):
- print "WARNING: link %s DOES NOT EXIST at %s" % (val, linkpath)
+ print(f'WARNING: link {val} DOES NOT EXIST at {linkpath}')
if verbose:
if os.path.exists(linkpath):
- print 'relative link %s EXISTS at %s' % (val, linkpath)
+ print('relative link {val} EXISTS at {linkpath}')
else:
if val == "pagebreak":
doc.sheets[doc.sheet_index].set_sheet_config(('row', row), style_pagebreak)
@@ -210,16 +215,19 @@ def main():
if len(args) != 0:
parser.error("not expecting extra args")
+ if not options.csv:
+ parser.error('Missing required --csv option')
if not os.path.exists(options.csv):
- err('csv does not exist: %s' % options.csv)
+ err(f'csv does not exist: {options.csv}')
if not options.ods:
(root, ext) = os.path.splitext(options.csv)
options.ods = root + '.ods'
if options.verbose:
- print '%s: verbose mode on' % program
- print 'csv:', options.csv
- print 'ods:', options.ods
- print 'ods:', options.encoding
+ print(f'''{program}: verbose mode on
+ csv: {options.csv}
+ ods: {options.ods}
+ ods: {options.encoding}
+ ''')
if options.known_checksum_list and not options.single_file_directory:
err(program + ": --known-checksum-list option is completely useless without --single-file-directory")
knownChecksums = {}
diff --git a/contrib/non-profit-audit-reports/ooolib2/__init__.py b/contrib/non-profit-audit-reports/ooolib2/__init__.py
index 3bc940bd..0b149e54 100644
--- a/contrib/non-profit-audit-reports/ooolib2/__init__.py
+++ b/contrib/non-profit-audit-reports/ooolib2/__init__.py
@@ -22,1966 +22,1992 @@
# Import Standard Modules
import zipfile # Needed for reading/writing documents
import time
-import sys
-import glob
-import os
import re
import xml.parsers.expat # Needed for parsing documents
+
def version_number():
- "Get the ooolib-python version number"
- return "0.0.17"
+ "Get the ooolib-python version number"
+ return "0.1.0"
+
def version():
- "Get the ooolib-python version"
- return "ooolib-python-%s" % version_number()
+ "Get the ooolib-python version"
+ return "ooolib-python-%s" % version_number()
+
def clean_string(data):
- "Returns an XML friendly copy of the data string"
-
- data = unicode(data) # This line thanks to Chris Ender
-
- data = data.replace('&', '&amp;')
- data = data.replace("'", '&apos;')
- data = data.replace('"', '&quot;')
- data = data.replace('<', '&lt;')
- data = data.replace('>', '&gt;')
- data = data.replace('\t', '<text:tab-stop/>')
- data = data.replace('\n', '<text:line-break/>')
- return data
-
-class XML:
- "XML Class - Used to convert nested lists into XML"
- def __init__(self):
- "Initialize ooolib XML instance"
- pass
-
- def _xmldata(self, data):
- datatype = data.pop(0)
- datavalue = data.pop(0)
- outstring = '%s' % datavalue
- return outstring
-
- def _xmltag(self, data):
- outstring = ''
- # First two
- datatype = data.pop(0)
- dataname = data.pop(0)
- outstring = '<%s' % dataname
- # Element Section
- element = 1
- while(data):
- # elements
- newdata = data.pop(0)
- if (newdata[0] == 'element' and element):
- newstring = self._xmlelement(newdata)
- outstring = '%s %s' % (outstring, newstring)
- continue
- if (newdata[0] != 'element' and element):
- element = 0
- outstring = '%s>' % outstring
- if (newdata[0] == 'tag' or newdata[0] == 'tagline'):
- outstring = '%s\n' % outstring
- if (newdata[0] == 'tag'):
- newstring = self._xmltag(newdata)
- outstring = '%s%s' % (outstring, newstring)
- continue
- if (newdata[0] == 'tagline'):
- newstring = self._xmltagline(newdata)
- outstring = '%s%s' % (outstring, newstring)
- continue
- if (newdata[0] == 'data'):
- newstring = self._xmldata(newdata)
- outstring = '%s%s' % (outstring, newstring)
- continue
- if (element):
- element = 0
- outstring = '%s>\n' % outstring
- outstring = '%s</%s>\n' % (outstring, dataname)
- return outstring
-
- def _xmltagline(self, data):
- outstring = ''
- # First two
- datatype = data.pop(0)
- dataname = data.pop(0)
- outstring = '<%s' % dataname
- # Element Section
- while(data):
- # elements
- newdata = data.pop(0)
- if (newdata[0] != 'element'): break
- newstring = self._xmlelement(newdata)
- outstring = '%s %s' % (outstring, newstring)
- outstring = '%s/>\n' % outstring
- # Non-Element Section should not exist
- return outstring
-
- def _xmlelement(self, data):
- datatype = data.pop(0)
- dataname = data.pop(0)
- datavalue = data.pop(0)
- outstring = '%s="%s"' % (dataname, datavalue)
- return outstring
-
- def convert(self, data):
- """Convert nested lists into XML
-
- The convert method takes a nested lists and converts them
- into XML to be used in Open Document Format documents.
- There are three types of lists that are recognized at this
- time. They are as follows:
-
- 'tag' - Tag opens a set of data that is eventually closed
- with a similar tag.
- List: ['tag', 'xml']
- XML: <xml></xml>
-
- 'tagline' - Taglines are similar to tags, except they open
- and close themselves.
- List: ['tagline', 'xml']
- XML: <xml/>
-
- 'element' - Elements are pieces of information stored in an
- opening tag or tagline.
- List: ['element', 'color', 'blue']
- XML: color="blue"
-
- 'data' - Data is plain text directly inserted into the XML
- document.
- List: ['data', 'hello']
- XML: hello
-
- Bring them all together for something like this.
-
- Lists:
- ['tag', 'xml', ['element', 'a', 'b'], ['tagline', 'xml2'],
- ['data', 'asdf']]
-
- XML:
- <xml a="b"><xml2/>asdf</xml>
- """
- outlines = []
- outlines.append('<?xml version="1.0" encoding="UTF-8"?>')
- if (type(data) == type([]) and len(data) > 0):
- if data[0] == 'tag': outlines.append(self._xmltag(data))
- return outlines
-
-class Meta:
- "Meta Data Class"
-
- def __init__(self, doctype, debug=False):
- self.doctype = doctype
-
- # Set the debug mode
- self.debug = debug
-
- # The generator should always default to the version number
- self.meta_generator = version()
- self.meta_title = ''
- self.meta_subject = ''
- self.meta_description = ''
- self.meta_keywords = []
- self.meta_creator = 'ooolib-python'
- self.meta_editor = ''
- self.meta_user1_name = 'Info 1'
- self.meta_user2_name = 'Info 2'
- self.meta_user3_name = 'Info 3'
- self.meta_user4_name = 'Info 4'
- self.meta_user1_value = ''
- self.meta_user2_value = ''
- self.meta_user3_value = ''
- self.meta_user4_value = ''
- self.meta_creation_date = self.meta_time()
-
- # Parser data
- self.parser_element_list = []
- self.parser_element = ""
- self.parser_count = 0
-
- def set_meta(self, metaname, value):
- """Set meta data in your document.
-
- Currently implemented metaname options are as follows:
- 'creator' - The document author
- """
- if metaname == 'creator': self.meta_creator = value
- if metaname == 'editor': self.meta_editor = value
- if metaname == 'title': self.meta_title = value
- if metaname == 'subject': self.meta_subject = value
- if metaname == 'description': self.meta_description = value
- if metaname == 'user1name': self.meta_user1_name = value
- if metaname == 'user2name': self.meta_user2_name = value
- if metaname == 'user3name': self.meta_user3_name = value
- if metaname == 'user4name': self.meta_user4_name = value
- if metaname == 'user1value': self.meta_user1_value = value
- if metaname == 'user2value': self.meta_user2_value = value
- if metaname == 'user3value': self.meta_user3_value = value
- if metaname == 'user4value': self.meta_user4_value = value
- if metaname == 'keyword':
- if value not in self.meta_keywords:
- self.meta_keywords.append(value)
-
- def get_meta_value(self, metaname):
- "Get meta data value for a given metaname."
-
- if metaname == 'creator': return self.meta_creator
- if metaname == 'editor': return self.meta_editor
- if metaname == 'title': return self.meta_title
- if metaname == 'subject': return self.meta_subject
- if metaname == 'description': return self.meta_description
- if metaname == 'user1name': return self.meta_user1_name
- if metaname == 'user2name': return self.meta_user2_name
- if metaname == 'user3name': return self.meta_user3_name
- if metaname == 'user4name': return self.meta_user4_name
- if metaname == 'user1value': return self.meta_user1_value
- if metaname == 'user2value': return self.meta_user2_value
- if metaname == 'user3value': return self.meta_user3_value
- if metaname == 'user4value': return self.meta_user4_value
- if metaname == 'keyword': return self.meta_keywords
-
- def meta_time(self):
- "Return time string in meta data format"
- t = time.localtime()
- stamp = "%04d-%02d-%02dT%02d:%02d:%02d" % (t[0], t[1], t[2], t[3], t[4], t[5])
- return stamp
-
- def parse_start_element(self, name, attrs):
- if self.debug: print '* Start element:', name
- self.parser_element_list.append(name)
- self.parser_element = self.parser_element_list[-1]
-
- # Need the meta name from the user-defined tags
- if (self.parser_element == "meta:user-defined"):
- self.parser_count += 1
- # Set user-defined name
- self.set_meta("user%dname" % self.parser_count, attrs['meta:name'])
-
- # Debugging statements
- if self.debug: print " List: ", self.parser_element_list
- if self.debug: print " Attributes: ", attrs
-
-
- def parse_end_element(self, name):
- if self.debug: print '* End element:', name
- if name != self.parser_element:
- print "Tag Mismatch: '%s' != '%s'" % (name, self.parser_element)
- self.parser_element_list.pop()
-
- # Readjust parser_element_list and parser_element
- if (self.parser_element_list):
- self.parser_element = self.parser_element_list[-1]
- else:
- self.parser_element = ""
-
- def parse_char_data(self, data):
- if self.debug: print " Character data: ", repr(data)
-
- # Collect Meta data fields
- if (self.parser_element == "dc:title"):
- self.set_meta("title", data)
- if (self.parser_element == "dc:description"):
- self.set_meta("description", data)
- if (self.parser_element == "dc:subject"):
- self.set_meta("subject", data)
- if (self.parser_element == "meta:initial-creator"):
- self.set_meta("creator", data)
-
- # Try to maintain the same creation date
- if (self.parser_element == "meta:creation-date"):
- self.meta_creation_date = data
-
- # The user defined fields need to be kept track of, parser_count does that
- if (self.parser_element == "meta:user-defined"):
- self.set_meta("user%dvalue" % self.parser_count, data)
-
- def meta_parse(self, data):
- "Parse Meta Data from a meta.xml file"
-
- # Debugging statements
- if self.debug:
- # Sometimes it helps to see the document that was read from
- print data
- print "\n\n\n"
-
- # Create parser
- parser = xml.parsers.expat.ParserCreate()
- # Set up parser callback functions
- parser.StartElementHandler = self.parse_start_element
- parser.EndElementHandler = self.parse_end_element
- parser.CharacterDataHandler = self.parse_char_data
-
- # Actually parse the data
- parser.Parse(data, 1)
-
- def get_meta(self):
- "Generate meta.xml file data"
- self.meta_date = self.meta_time()
- self.data = ['tag', 'office:document-meta',
- ['element', 'xmlns:office', 'urn:oasis:names:tc:opendocument:xmlns:office:1.0'],
- ['element', 'xmlns:xlink', 'http://www.w3.org/1999/xlink'],
- ['element', 'xmlns:dc', 'http://purl.org/dc/elements/1.1/'],
- ['element', 'xmlns:meta', 'urn:oasis:names:tc:opendocument:xmlns:meta:1.0'],
- ['element', 'xmlns:ooo', 'http://openoffice.org/2004/office'],
- ['element', 'office:version', '1.0'],
- ['tag', 'office:meta',
- ['tag', 'meta:generator', # Was: 'OpenOffice.org/2.0$Linux OpenOffice.org_project/680m5$Build-9011'
- ['data', self.meta_generator]], # Generator is set the ooolib-python version.
- ['tag', 'dc:title',
- ['data', self.meta_title]], # This data is the document title
- ['tag', 'dc:description',
- ['data', self.meta_description]], # This data is the document description
- ['tag', 'dc:subject',
- ['data', self.meta_subject]], # This data is the document subject
- ['tag', 'meta:initial-creator',
- ['data', self.meta_creator]], # This data is the document creator
- ['tag', 'meta:creation-date',
- ['data', self.meta_creation_date]], # This is the original creation date of the document
- ['tag', 'dc:creator',
- ['data', self.meta_editor]], # This data is the document editor
- ['tag', 'dc:date',
- ['data', self.meta_date]], # This is the last modified date of the document
- ['tag', 'dc:language',
- ['data', 'en-US']], # We will probably always use en-US for language
- ['tag', 'meta:editing-cycles',
- ['data', '1']], # Edit cycles will probably always be 1 for generated documents
- ['tag', 'meta:editing-duration',
- ['data', 'PT0S']], # Editing duration is modified - creation date
- ['tag', 'meta:user-defined',
- ['element', 'meta:name', self.meta_user1_name],
- ['data', self.meta_user1_value]],
- ['tag', 'meta:user-defined',
- ['element', 'meta:name', self.meta_user2_name],
- ['data', self.meta_user2_value]],
- ['tag', 'meta:user-defined',
- ['element', 'meta:name', self.meta_user3_name],
- ['data', self.meta_user3_value]],
- ['tag', 'meta:user-defined',
- ['element', 'meta:name', self.meta_user4_name],
- ['data', self.meta_user4_value]]]]
-# ['tagline', 'meta:document-statistic',
-# ['element', 'meta:table-count', len(self.sheets)], # len(self.sheets) ?
-# ['element', 'meta:cell-count', '15']]]] # Not sure how to keep track
-
- # Generate content.xml XML data
- xml = XML()
- self.lines = xml.convert(self.data)
- self.filedata = '\n'.join(self.lines)
- # Return generated data
- return self.filedata
-
-
-
-class CalcStyles:
- "Calc Style Management - Used to keep track of created styles."
-
- def __init__(self):
- self.style_config = {}
- # Style Counters
- self.style_table = 1
- self.style_column = 1
- self.style_row = 1
- self.style_cell = 1
- # Style Properties (Defaults) - To be used later
- self.property_column_width_default = '0.8925in' # Default Column Width
- self.property_row_height_default = '0.189in' # Default Row Height
- # Set Defaults
- self.property_column_width = '0.8925in' # Default Column Width
- self.property_row_height = '0.189in' # Default Row Height
- self.property_cell_bold = False # Bold off be default
- self.property_cell_italic = False # Italic off be default
- self.property_cell_underline = False # Underline off be default
- self.property_cell_fg_color = 'default' # Text Color Default
- self.property_cell_bg_color = 'default' # Cell Background Default
- self.property_cell_bg_image = 'none' # Cell Background Default
- self.property_cell_fontsize = '10' # Cell Font Size Default
- self.property_cell_valign = 'default' # Vertical Alignment Default
- self.property_cell_halign = 'default' # Horizantal Alignment Default
-
- def get_next_style(self, style):
- "Returns the next style code for the given style"
- style_code = ""
- if style == 'table':
- style_code = 'ta%d' % self.style_table
- self.style_table+=1
- if style == 'column':
- style_code = 'co%d' % self.style_column
- self.style_column+=1
- if style == 'row':
- style_code = 'ro%d' % self.style_row
- self.style_row+=1
- if style == 'cell':
- style_code = 'ce%d' % self.style_cell
- self.style_cell+=1
- return style_code
-
- def set_property(self, style, name, value):
- "Sets a property which will later be turned into a code"
- if style == 'table':
- pass
- if style == 'column':
- if name == 'style:column-width': self.property_column_width = value
- if style == 'row':
- if name == 'style:row-height': self.property_row_height = value
- if style == 'cell':
- if name == 'bold' and type(value) == type(True): self.property_cell_bold = value
- if name == 'italic' and type(value) == type(True): self.property_cell_italic = value
- if name == 'underline' and type(value) == type(True): self.property_cell_underline = value
- if name == 'fontsize': self.property_cell_fontsize = value
- if name == 'color':
- self.property_cell_fg_color = 'default'
- redata = re.search("^(#[\da-fA-F]{6})$", value)
- if redata: self.property_cell_fg_color = value.lower()
- if name == 'background':
- self.property_cell_bg_color = 'default'
- redata = re.search("^(#[\da-fA-F]{6})$", value)
- if redata: self.property_cell_bg_color = value.lower()
- if name == 'backgroundimage':
- self.property_cell_bg_image = value
- if name == 'valign':
- self.property_cell_valign = value
- if name == 'halign':
- self.property_cell_halign = value
-
- def get_style_code(self, style):
- style_code = ""
- if style == 'table':
- style_code = "ta1"
- if style == 'column':
- style_data = tuple([style,
- ('style:column-width', self.property_column_width)])
- if style_data in self.style_config:
- # Style Exists, return code
- style_code = self.style_config[style_data]
- else:
- # Style does not exist, create code and return it
- style_code = self.get_next_style(style)
- self.style_config[style_data] = style_code
- if style == 'row':
- style_data = tuple([style,
- ('style:row-height', self.property_row_height)])
- if style_data in self.style_config:
- # Style Exists, return code
- style_code = self.style_config[style_data]
- else:
- # Style does not exist, create code and return it
- style_code = self.get_next_style(style)
- self.style_config[style_data] = style_code
- if style == 'cell':
- style_data = [style]
- # Add additional styles
- if self.property_cell_bold: style_data.append(('bold', True))
- if self.property_cell_italic: style_data.append(('italic', True))
- if self.property_cell_underline: style_data.append(('underline', True))
- if self.property_cell_fontsize != '10':
- style_data.append(('fontsize', self.property_cell_fontsize))
- if self.property_cell_fg_color != 'default':
- style_data.append(('color', self.property_cell_fg_color))
- if self.property_cell_bg_color != 'default':
- style_data.append(('background', self.property_cell_bg_color))
- if self.property_cell_bg_image != 'none':
- style_data.append(('backgroundimage', self.property_cell_bg_image))
- if self.property_cell_valign != 'default':
- style_data.append(('valign', self.property_cell_valign))
- if self.property_cell_halign != 'default':
- style_data.append(('halign', self.property_cell_halign))
-
- style_data = tuple(style_data)
- if style_data in self.style_config:
- # Style Exists, return code
- style_code = self.style_config[style_data]
- else:
- # Style does not exist, create code and return it
- style_code = self.get_next_style(style)
- self.style_config[style_data] = style_code
- return style_code
-
- def get_automatic_styles(self):
- "Return 'office:automatic-styles' lists"
- automatic_styles = ['tag', 'office:automatic-styles']
-
- for style_data in self.style_config:
- style_code = self.style_config[style_data]
- style_data = list(style_data)
- style = style_data.pop(0)
-
- if style == 'column':
- style_list = ['tag', 'style:style',
- ['element', 'style:name', style_code], # Column 'co1' properties
- ['element', 'style:family', 'table-column']]
- tagline = ['tagline', 'style:table-column-properties',
- ['element', 'fo:break-before', 'auto']] # unsure what break before means
-
- for set in style_data:
- name, value = set
- if name == 'style:column-width':
- tagline.append(['element', 'style:column-width', value])
- style_list.append(tagline)
- automatic_styles.append(style_list)
-
- if style == 'row':
- style_list = ['tag', 'style:style',
- ['element', 'style:name', style_code], # Column 'ro1' properties
- ['element', 'style:family', 'table-row']]
- tagline = ['tagline', 'style:table-row-properties']
-
- for set in style_data:
- name, value = set
- if name == 'style:row-height':
- tagline.append(['element', 'style:row-height', value])
- tagline.append(['element', 'fo:break-before', 'auto'])
-# tagline.append(['element', 'style:use-optimal-row-height', 'true']) # Overrides settings
- style_list.append(tagline)
- automatic_styles.append(style_list)
-
- if style == 'pagebreak':
- style_list = ['tag', 'style:style',
- ['element', 'style:name', style_code], # Column 'ro1' properties
- ['element', 'style:family', 'table-row']]
- tagline = ['tagline', 'style:table-row-properties']
-
- for set in style_data:
- name, value = set
- if name == 'style:row-height':
- tagline.append(['element', 'style:row-height', value])
- tagline.append(['element', 'fo:break-before', 'page'])
-# tagline.append(['element', 'style:use-optimal-row-height', 'true']) # Overrides settings
- style_list.append(tagline)
- automatic_styles.append(style_list)
-
- if style == 'cell':
- style_list = ['tag', 'style:style',
- ['element', 'style:name', style_code], # ce1 style
- ['element', 'style:family', 'table-cell'], # cell
- ['element', 'style:parent-style-name', 'Default']] # parent is Default
- # hack for currency
- if style_code == 'ce1':
- style_list.append(['element',
- 'style:data-style-name',
- 'N104'])
-
- # Cell Properties
- tagline = ['tag', 'style:table-cell-properties']
- tagline_additional = []
- for set in style_data:
- name, value = set
- if name == 'background':
- tagline.append(['element', 'fo:background-color', value])
- if name == 'backgroundimage':
- tagline.append(['element', 'fo:background-color', 'transparent'])
- # Additional tags added later
- bgimagetag = ['tagline', 'style:background-image']
- bgimagetag.append(['element', 'xlink:href', value])
- bgimagetag.append(['element', 'xlink:type', 'simple'])
- bgimagetag.append(['element', 'xlink:actuate', 'onLoad'])
- tagline_additional.append(bgimagetag)
- if name == 'valign':
- if value in ['top', 'bottom', 'middle']:
- tagline.append(['element', 'style:vertical-align', value])
- if name == 'halign':
- tagline.append(['element', 'style:text-align-source', 'fix'])
- if value in ['filled']:
- tagline.append(['element', 'style:repeat-content', 'true'])
- else:
- tagline.append(['element', 'style:repeat-content', 'false'])
-
- # Add any additional internal tags
- while tagline_additional:
- tagadd = tagline_additional.pop(0)
- tagline.append(tagadd)
-
- style_list.append(tagline)
-
- # Paragraph Properties
- tagline = ['tagline', 'style:paragraph-properties']
- tagline_valid = False
- for set in style_data:
- name, value = set
- if name == 'halign':
- tagline_valid = True
- if value in ['center']:
- tagline.append(['element', 'fo:text-align', 'center'])
- if value in ['end', 'right']:
- tagline.append(['element', 'fo:text-align', 'end'])
- if value in ['start', 'filled', 'left']:
- tagline.append(['element', 'fo:text-align', 'start'])
- if value in ['justify']:
- tagline.append(['element', 'fo:text-align', 'justify'])
- # Conditionally add the tagline
- if tagline_valid: style_list.append(tagline)
-
-
- # Text Properties
- tagline = ['tagline', 'style:text-properties']
- for set in style_data:
- name, value = set
- if name == 'bold':
- tagline.append(['element', 'fo:font-weight', 'bold'])
- if name == 'italic':
- tagline.append(['element', 'fo:font-style', 'italic'])
- if name == 'underline':
- tagline.append(['element', 'style:text-underline-style', 'solid'])
- tagline.append(['element', 'style:text-underline-width', 'auto'])
- tagline.append(['element', 'style:text-underline-color', 'font-color'])
- if name == 'color':
- tagline.append(['element', 'fo:color', value])
- if name == 'fontsize':
- tagline.append(['element', 'fo:font-size', '%spt' % value])
- style_list.append(tagline)
-
- automatic_styles.append(style_list)
-
-
- # Attach ta1 style
- automatic_styles.append(['tag', 'style:style',
- ['element', 'style:name', 'ta1'],
- ['element', 'style:family', 'table'],
- ['element', 'style:master-page-name', 'Default'],
- ['tagline', 'style:table-properties',
- ['element', 'table:display', 'true'],
- ['element', 'style:writing-mode', 'lr-tb']]])
-
-
- return automatic_styles
-
-
-
-class CalcSheet:
- "Calc Sheet Class - Used to keep track of the data for an individual sheet."
-
- def __init__(self, sheetname):
- "Initialize a sheet"
- self.sheet_name = sheetname
- self.sheet_values = {}
- self.sheet_config = {}
- self.max_col = 0
- self.max_row = 0
-
- def get_sheet_dimensions(self):
- "Returns the max column and row"
- return (self.max_col, self.max_row)
-
- def clean_formula(self, data):
- "Returns a formula for use in ODF"
- # Example Translations
- # '=SUM(A1:A2)'
- # datavalue = 'oooc:=SUM([.A1:.A2])'
- # '=IF((A5>A4);A4;"")'
- # datavalue = 'oooc:=IF(([.A5]&gt;[.A4]);[.A4];&quot;&quot;)'
- data = str(data)
- data = clean_string(data)
- redata = re.search('^=([A-Z]+)(\(.*)$', data)
- if redata:
- # funct is the function name. The rest if the string will be the functArgs
- funct = redata.group(1)
- functArgs = redata.group(2)
- # Search for cell lebels and replace them
- reList = re.findall('([A-Z]+\d+)', functArgs)
- # sort and keep track so we do not do a cell more than once
- reList.sort()
- lastVar = ''
- while reList:
- # Replace each cell label
- curVar = reList.pop()
- if curVar == lastVar: continue
- lastVar = curVar
- functArgs = functArgs.replace(curVar, '[.%s]' % curVar)
- data = 'oooc:=%s%s' % (funct, functArgs)
- return data
-
- def get_name(self):
- "Returns the sheet name"
- return self.sheet_name
-
- def set_name(self, sheetname):
- "Resets the sheet name"
- self.sheet_name = sheetname
-
- def get_sheet_values(self):
- "Returns the sheet cell values"
- return self.sheet_values
-
- def get_sheet_value(self, col, row):
- "Get the value contents of a cell"
- cell = (col, row)
- if cell in self.sheet_values:
- return self.sheet_values[cell]
- else:
- return None
-
- def get_sheet_config(self):
- "Returns the sheet cell properties"
- return self.sheet_config
-
- def set_sheet_config(self, location, style_code):
- "Sets Style Code for a given location"
- self.sheet_config[location] = style_code
-
- def set_sheet_value(self, cell, datatype, datavalue):
- """Sets the value for a specific cell
-
- cell must be in the format (col, row) where row and col are int.
- Example: B5 would be written as (2, 5)
- datatype must be one of 'string', 'float', 'formula', 'currency'
- datavalue should be a string
- """
- # Catch invalid data
- if type(cell) != type(()) or len(cell) != 2:
- print "Invalid Cell"
- return
- (col, row) = cell
- if type(col) != type(1):
- print "Invalid Cell"
- return
- if type(row) != type(1):
- print "Invalid Cell"
- return
- # Fix String Data
- if datatype in ['string', 'annotation']:
- datavalue = clean_string(datavalue)
- # Fix Link Data. Link's value is a tuple containing (url, description)
- if (datatype == 'link'):
- url = clean_string(datavalue[0])
- desc = clean_string(datavalue[1])
- datavalue = (url, desc)
- # Fix Formula Data
- if datatype == 'formula':
- datavalue = self.clean_formula(datavalue)
- # Adjust maximum sizes
- if col > self.max_col: self.max_col = col
- if row > self.max_row: self.max_row = row
- datatype = str(datatype)
- if (datatype not in ['string', 'float', 'currency', 'formula', 'annotation', 'link']):
- # Set all unknown cell types to string
- datatype = 'string'
- datavalue = str(datavalue)
-
- # The following lines are taken directly from HPS
- # self.sheet_values[cell] = (datatype, datavalue)
- # HPS: Cell content is now a list of tuples instead of a tuple
- # While storing here, store the cell contents first and the annotation next. While generating the XML reverse this
- contents = self.sheet_values.get(cell, {'annotation':None,'link':None, 'value':None})
- if datatype == 'annotation':
- contents['annotation'] = (datatype, datavalue)
- elif datatype == 'link':
- contents['link'] = (datatype, datavalue)
- else:
- contents['value'] = (datatype, datavalue)
-
- self.sheet_values[cell] = contents
-
-
- def get_lists(self):
- "Returns nested lists for XML processing"
- if (self.max_col == 0 and self.max_row == 0):
- sheet_lists = ['tag', 'table:table',
- ['element', 'table:name', self.sheet_name], # Set the Sheet Name
- ['element', 'table:style-name', 'ta1'],
- ['element', 'table:print', 'false'],
- ['tagline', 'table:table-column',
- ['element', 'table:style-name', 'co1'],
- ['element', 'table:default-cell-style-name', 'Default']],
- ['tag', 'table:table-row',
- ['element', 'table:style-name', 'ro1'],
- ['tagline', 'table:table-cell']]]
- else:
- # Base Information
- sheet_lists = ['tag', 'table:table',
- ['element', 'table:name', self.sheet_name], # Set the sheet name
- ['element', 'table:style-name', 'ta1'],
- ['element', 'table:print', 'false']]
-
-# ['tagline', 'table:table-column',
-# ['element', 'table:style-name', 'co1'],
-# ['element', 'table:number-columns-repeated', self.max_col], # max_col? '2'
-# ['element', 'table:default-cell-style-name', 'Default']],
-
- # Need to add column information
- for col in range(1, self.max_col+1):
- location = ('col', col)
- style_code = 'co1'
- if location in self.sheet_config:
- style_code = self.sheet_config[location]
- sheet_lists.append(['tagline', 'table:table-column',
- ['element', 'table:style-name', style_code],
- ['element', 'table:default-cell-style-name', 'Default']])
-
-
- # Need to create each row
- for row in range(1, self.max_row + 1):
- location = ('row', row)
- style_code = 'ro1'
- if location in self.sheet_config:
- style_code = self.sheet_config[location]
- rowlist = ['tag', 'table:table-row',
- ['element', 'table:style-name', style_code]]
- for col in range(1, self.max_col + 1):
- cell = (col, row)
- style_code = 'ce1' # Default all cells to ce1
- if cell in self.sheet_config:
- style_code = self.sheet_config[cell] # Lookup cell if available
- if cell in self.sheet_values:
- # (datatype, datavalue) = self.sheet_values[cell] # Marked for removal
- collist = ['tag', 'table:table-cell']
- if style_code != 'ce1':
- collist.append(['element', 'table:style-name', style_code])
-
- # Contents, annotations, and links added by HPS
- contents = self.sheet_values[cell] # cell contents is a dictionary
- if contents['value']:
- (datatype, datavalue) = contents['value']
- if datatype == 'float':
- collist.append(['element', 'office:value-type', datatype])
- collist.append(['element', 'office:value', datavalue])
- if datatype == 'currency':
- collist.append(['element', 'table:style-name', "ce1"])
- collist.append(['element', 'office:value-type', datatype])
- collist.append(['element', 'office:currency', 'USD'])
- collist.append(['element', 'office:value', datavalue])
-
- if datatype == 'string':
- collist.append(['element', 'office:value-type', datatype])
- if datatype == 'formula':
- collist.append(['element', 'table:formula', datavalue])
- collist.append(['element', 'office:value-type', 'float'])
- collist.append(['element', 'office:value', '0'])
- datavalue = '0'
- else:
- datavalue = None
-
- if contents['annotation']:
- (annotype, annoval) = contents['annotation']
- collist.append(['tag', 'office:annotation',
- ['tag', 'text:p', ['data', annoval]]])
-
- if contents['link']:
- (linktype, linkval) = contents['link']
- if datavalue:
- collist.append(['tag', 'text:p', ['data', datavalue],
- ['tag', 'text:a', ['element', 'xlink:href', linkval[0]],
- ['data', linkval[1]]]])
- else: # no value; just fill the link
- collist.append(['tag', 'text:p',
- ['tag', 'text:a', ['element', 'xlink:href', linkval[0]],
- ['data', linkval[1]]]])
- else:
- if datavalue:
- collist.append(['tag', 'text:p', ['data', datavalue]])
-
-
-
- else:
- collist = ['tagline', 'table:table-cell']
- rowlist.append(collist)
- sheet_lists.append(rowlist)
- return sheet_lists
-
-class Calc:
- "Calc Class - Used to create OpenDocument Format Calc Spreadsheets."
- def __init__(self, sheetname=None, opendoc=None, debug=False):
- "Initialize ooolib Calc instance"
- # Default to no debugging
- self.debug = debug
- if not sheetname: sheetname = "Sheet1"
- self.sheets = [CalcSheet(sheetname)] # The main sheet will be initially called 'Sheet1'
- self.sheet_index = 0 # We initially start on the first sheet
- self.styles = CalcStyles()
- self.meta = Meta('ods')
- self.styles.get_style_code('column') # Force generation of default column
- self.styles.get_style_code('row') # Force generation of default row
- self.styles.get_style_code('table') # Force generation of default table
- self.styles.get_style_code('cell') # Force generation of default cell
- self.manifest_files = [] # List of extra files included
- self.manifest_index = 1 # Index of added manifest files
-
- # Data Parsing
- self.parser_element_list = []
- self.parser_element = ""
- self.parser_sheet_num = 0
- self.parser_sheet_row = 0
- self.parser_sheet_column = 0
- self.parser_cell_repeats = 0
- self.parser_cell_string_pending = False
- self.parser_cell_string_line = ""
-
- # See if we need to read a document
- if opendoc:
- # Verify that the document exists
- if self.debug: print "Opening Document: %s" % opendoc
-
- # Okay, now we load the file
- self.load(opendoc)
-
- def debug_level(self, level):
- """Set debug level:
- True if you want debugging messages
- False if you do not.
- """
- self.debug = level
-
- def file_mimetype(self, filename):
- "Determine the filetype from the filename"
- parts = filename.lower().split('.')
- ext = parts[-1]
- if (ext == 'png'): return (ext, "image/png")
- if (ext == 'gif'): return (ext, "image/gif")
- return (ext, "image/unknown")
-
- def add_file(self, filename):
- """Prepare a file for loading into ooolib
-
- The filename should be the local filesystem name for
- the file. The file is then prepared to be included in
- the creation of the final document. The file needs to
- remain in place so that it is available when the actual
- document creation happens.
- """
- # mimetype set to (ext, filetype)
- mimetype = self.file_mimetype(filename)
- newname = "Pictures/%08d.%s" % (self.manifest_index, mimetype[0])
- self.manifest_index += 1
- filetype = mimetype[1]
- self.manifest_files.append((filename, filetype, newname))
- return newname
-
- def set_meta(self, metaname, value):
- "Set meta data in your document."
- self.meta.set_meta(metaname, value)
-
- def get_meta_value(self, metaname):
- "Get meta data value for a given metaname"
- return self.meta.get_meta_value(metaname)
-
- def get_sheet_name(self):
- "Returns the sheet name"
- return self.sheets[self.sheet_index].get_name()
-
- def get_sheet_dimensions(self):
- "Returns the sheet dimensions in (cols, rows)"
- return self.sheets[self.sheet_index].get_sheet_dimensions()
-
- def set_column_property(self, column, name, value):
- "Set Column Properties"
- if name == 'width':
- # column number column needs column-width set to value
- self.styles.set_property('column', 'style:column-width', value)
- style_code = self.styles.get_style_code('column')
- self.sheets[self.sheet_index].set_sheet_config(('col', column), style_code)
-
- def set_row_property(self, row, name, value):
- "Set row Properties"
- if name == 'height':
- # row number row needs row-height set to value
- self.styles.set_property('row', 'style:row-height', value)
- style_code = self.styles.get_style_code('row')
- self.sheets[self.sheet_index].set_sheet_config(('row', row), style_code)
-
- def set_cell_property(self, name, value):
- """Turn and off cell properties
-
- Actual application of properties is handled by setting a value."""
- # background images need to be handled a little differently
- # because they need to also be inserted into the final document
- if (name == 'backgroundimage'):
- # Add file and modify value
- value = self.add_file(value)
- self.styles.set_property('cell', name, value)
-
- def get_sheet_index(self):
- "Return the current sheet index number"
- return self.sheet_index
-
- def set_sheet_index(self, index):
- "Set the sheet index"
- if type(index) == type(1):
- if index >= 0 and index < len(self.sheets):
- self.sheet_index = index
- return self.sheet_index
-
- def get_sheet_count(self):
- "Returns the number of existing sheets"
- return len(self.sheets)
-
- def new_sheet(self, sheetname):
- "Create a new sheet"
- self.sheet_index = len(self.sheets)
- self.sheets.append(CalcSheet(sheetname))
- return self.sheet_index
-
- def set_cell_value(self, col, row, datatype, value):
- "Set the value for a given cell"
- self.sheets[self.sheet_index].set_sheet_value((col, row), datatype, value)
- style_code = self.styles.get_style_code('cell')
- self.sheets[self.sheet_index].set_sheet_config((col, row), style_code)
-
- def get_cell_value(self, col, row):
- "Get a cell value tuple (type, value) for a given cell"
- sheetvalue = self.sheets[self.sheet_index].get_sheet_value(col, row)
- # We stop here if there is no value for sheetvalue
- if sheetvalue == None: return sheetvalue
- # Now check to see if we have a value tuple
- if 'value' in sheetvalue:
- return sheetvalue['value']
- else:
- return None
-
- def load(self, filename):
- """Load .ods spreadsheet.
-
- The load function loads data from a document into the current cells.
- """
- # Read in the important files
-
- # meta.xml
- data = self._zip_read(filename, "meta.xml")
- self.meta.meta_parse(data)
-
- # content.xml
- data = self._zip_read(filename, "content.xml")
- self.content_parse(data)
-
- # settings.xml - I do not remember putting anything here
- # styles.xml - I do not remember putting anything here
-
- def parse_content_start_element(self, name, attrs):
- if self.debug: print '* Start element:', name
- self.parser_element_list.append(name)
- self.parser_element = self.parser_element_list[-1]
-
- # Keep track of the current sheet number
- if (self.parser_element == 'table:table'):
- # Move to starting cell
- self.parser_sheet_row = 0
- self.parser_sheet_column = 0
- # Increment the sheet number count
- self.parser_sheet_num += 1
- if (self.parser_sheet_num - 1 != self.sheet_index):
- # We are not on the first sheet and need to create a new sheet.
- # We will automatically move to the new sheet
- sheetname = "Sheet%d" % self.parser_sheet_num
- if 'table:name' in attrs: sheetname = attrs['table:name']
- self.new_sheet(sheetname)
- else:
- # We are on the first sheet and will need to overwrite the default name
- sheetname = "Sheet%d" % self.parser_sheet_num
- if 'table:name' in attrs: sheetname = attrs['table:name']
- self.sheets[self.sheet_index].set_name(sheetname)
-
- # Update the row numbers
- if (self.parser_element == 'table:table-row'):
- self.parser_sheet_row += 1
- self.parser_sheet_column = 0
-
- # Okay, now keep track of the sheet cell data
- if (self.parser_element == 'table:table-cell'):
- # By default it will repeat zero times
- self.parser_cell_repeats = 0
- # We must be in a new column
- self.parser_sheet_column += 1
- # Set some default values
- datatype = ""
- value = ""
- # Get values from attrs hash
- if 'office:value-type' in attrs: datatype = attrs['office:value-type']
- if 'office:value' in attrs: value = attrs['office:value']
- if 'table:formula' in attrs:
- datatype = 'formula'
- value = attrs['table:formula']
- if datatype == 'string':
- datatype = ""
- self.parser_cell_string_pending = True
- self.parser_cell_string_line = ""
- if 'table:number-columns-repeated' in attrs:
- self.parser_cell_repeats = int(attrs['table:number-columns-repeated']) - 1
- # Set the cell value
- if datatype:
- # I should do this once per cell repeat above 0
- for i in range(0, self.parser_cell_repeats+1):
- self.set_cell_value(self.parser_sheet_column+i, self.parser_sheet_row, datatype, value)
-
- # There are lots of interesting cases with table:table-cell data. One problem is
- # reading the number of embedded spaces correctly. This code should help us get
- # the number of spaces out.
-
- if (self.parser_element == 'text:s'):
- # This means we have a number of spaces
- count_num = 0
- if 'text:c' in attrs:
- count_alpha = attrs['text:c']
- if (count_alpha.isdigit()):
- count_num = int(count_alpha)
- # I am not sure what to do if we do not have a string pending
- if (self.parser_cell_string_pending == True):
- # Append the currect number of spaces to the end
- self.parser_cell_string_line = "%s%s" % (self.parser_cell_string_line, ' '*count_num)
-
- if (self.parser_element == 'text:tab-stop'):
- if (self.parser_cell_string_pending == True):
- self.parser_cell_string_line = "%s\t" % (self.parser_cell_string_line)
-
- if (self.parser_element == 'text:line-break'):
- if (self.parser_cell_string_pending == True):
- self.parser_cell_string_line = "%s\n" % (self.parser_cell_string_line)
-
- # Debugging statements
- if self.debug: print " List: ", self.parser_element_list
- if self.debug: print " Attributes: ", attrs
-
-
- def parse_content_end_element(self, name):
- if self.debug: print '* End element:', name
- if name != self.parser_element:
- print "Tag Mismatch: '%s' != '%s'" % (name, self.parser_element)
- self.parser_element_list.pop()
-
- # If the element was text:p and we are in string mode
- if (self.parser_element == 'text:p'):
- if (self.parser_cell_string_pending):
- self.parser_cell_string_pending = False
-
- # Take care of repeated cells
- if (self.parser_element == 'table:table-cell'):
- self.parser_sheet_column += self.parser_cell_repeats
-
- # Readjust parser_element_list and parser_element
- if (self.parser_element_list):
- self.parser_element = self.parser_element_list[-1]
- else:
- self.parser_element = ""
-
- def parse_content_char_data(self, data):
- if self.debug: print " Character data: ", repr(data)
-
- if (self.parser_element == 'text:p' or self.parser_element == 'text:span'):
- if (self.parser_cell_string_pending):
- # Set the string and leave string pending mode
- # This does feel a little kludgy, but it does the job
- self.parser_cell_string_line = "%s%s" % (self.parser_cell_string_line, data)
-
- # I should do this once per cell repeat above 0
- for i in range(0, self.parser_cell_repeats+1):
- self.set_cell_value(self.parser_sheet_column+i, self.parser_sheet_row,
- 'string', self.parser_cell_string_line)
-
-
- def content_parse(self, data):
- "Parse Content Data from a content.xml file"
-
- # Debugging statements
- if self.debug:
- # Sometimes it helps to see the document that was read from
- print data
- print "\n\n\n"
-
- # Create parser
- parser = xml.parsers.expat.ParserCreate()
- # Set up parser callback functions
- parser.StartElementHandler = self.parse_content_start_element
- parser.EndElementHandler = self.parse_content_end_element
- parser.CharacterDataHandler = self.parse_content_char_data
-
- # Actually parse the data
- parser.Parse(data, 1)
-
- def save(self, filename):
- """Save .ods spreadsheet.
-
- The save function saves the current cells and settings into a document.
- """
- if self.debug: print "Writing %s" % filename
- self.savefile = zipfile.ZipFile(filename, "w")
- if self.debug: print " meta.xml"
- self._zip_insert(self.savefile, "meta.xml", self.meta.get_meta())
- if self.debug: print " mimetype"
- self._zip_insert(self.savefile, "mimetype", "application/vnd.oasis.opendocument.spreadsheet")
- if self.debug: print " Configurations2/accelerator/current.xml"
- self._zip_insert(self.savefile, "Configurations2/accelerator/current.xml", "")
- if self.debug: print " META-INF/manifest.xml"
- self._zip_insert(self.savefile, "META-INF/manifest.xml", self._ods_manifest())
- if self.debug: print " content.xml"
- self._zip_insert(self.savefile, "content.xml", self._ods_content())
- if self.debug: print " settings.xml"
- self._zip_insert(self.savefile, "settings.xml", self._ods_settings())
- if self.debug: print " styles.xml"
- self._zip_insert(self.savefile, "styles.xml", self._ods_styles())
-
- # Add additional files if needed
- for fileset in self.manifest_files:
- (filename, filetype, newname) = fileset
- # Read in the file
- data = self._file_load(filename)
- if self.debug: print " Inserting '%s' as '%s'" % (filename, newname)
- self._zip_insert_binary(self.savefile, newname, data)
-
- def _file_load(self, filename):
- "Load a file"
- file = open(filename, "rb")
- data = file.read()
- file.close()
- return data
-
- def _zip_insert_binary(self, file, filename, data):
- "Insert a binary file into the zip archive"
- now = time.localtime(time.time())[:6]
- info = zipfile.ZipInfo(filename)
- info.date_time = now
- info.compress_type = zipfile.ZIP_DEFLATED
- file.writestr(info, data)
-
-
- def _zip_insert(self, file, filename, data):
- "Insert a file into the zip archive"
-
- # zip seems to struggle with non-ascii characters
- data = data.encode('utf-8')
-
- now = time.localtime(time.time())[:6]
- info = zipfile.ZipInfo(filename)
- info.date_time = now
- info.compress_type = zipfile.ZIP_DEFLATED
- file.writestr(info, data)
-
- def _zip_read(self, file, filename):
- "Get the data from a file in the zip archive by filename"
- file = zipfile.ZipFile(file, "r")
- data = file.read(filename)
- # Need to close the file
- file.close()
- return data
-
- def _ods_content(self):
- "Generate ods content.xml data"
-
- # This will list all of the sheets in the document
- self.sheetdata = ['tag', 'office:spreadsheet']
- for sheet in self.sheets:
- if self.debug:
- sheet_name = sheet.get_name()
- print " Creating Sheet '%s'" % sheet_name
- sheet_list = sheet.get_lists()
- self.sheetdata.append(sheet_list)
- # Automatic Styles
- self.automatic_styles = self.styles.get_automatic_styles()
-
- self.data = ['tag', 'office:document-content',
- ['element', 'xmlns:office', 'urn:oasis:names:tc:opendocument:xmlns:office:1.0'],
- ['element', 'xmlns:style', 'urn:oasis:names:tc:opendocument:xmlns:style:1.0'],
- ['element', 'xmlns:text', 'urn:oasis:names:tc:opendocument:xmlns:text:1.0'],
- ['element', 'xmlns:table', 'urn:oasis:names:tc:opendocument:xmlns:table:1.0'],
- ['element', 'xmlns:draw', 'urn:oasis:names:tc:opendocument:xmlns:drawing:1.0'],
- ['element', 'xmlns:fo', 'urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0'],
- ['element', 'xmlns:xlink', 'http://www.w3.org/1999/xlink'],
- ['element', 'xmlns:dc', 'http://purl.org/dc/elements/1.1/'],
- ['element', 'xmlns:meta', 'urn:oasis:names:tc:opendocument:xmlns:meta:1.0'],
- ['element', 'xmlns:number', 'urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0'],
- ['element', 'xmlns:svg', 'urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0'],
- ['element', 'xmlns:chart', 'urn:oasis:names:tc:opendocument:xmlns:chart:1.0'],
- ['element', 'xmlns:dr3d', 'urn:oasis:names:tc:opendocument:xmlns:dr3d:1.0'],
- ['element', 'xmlns:math', 'http://www.w3.org/1998/Math/MathML'],
- ['element', 'xmlns:form', 'urn:oasis:names:tc:opendocument:xmlns:form:1.0'],
- ['element', 'xmlns:script', 'urn:oasis:names:tc:opendocument:xmlns:script:1.0'],
- ['element', 'xmlns:ooo', 'http://openoffice.org/2004/office'],
- ['element', 'xmlns:ooow', 'http://openoffice.org/2004/writer'],
- ['element', 'xmlns:oooc', 'http://openoffice.org/2004/calc'],
- ['element', 'xmlns:dom', 'http://www.w3.org/2001/xml-events'],
- ['element', 'xmlns:xforms', 'http://www.w3.org/2002/xforms'],
- ['element', 'xmlns:xsd', 'http://www.w3.org/2001/XMLSchema'],
- ['element', 'xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance'],
- ['element', 'office:version', '1.0'],
- ['tagline', 'office:scripts'],
- ['tag', 'office:font-face-decls',
- ['tagline', 'style:font-face',
- ['element', 'style:name', 'DejaVu Sans'],
- ['element', 'svg:font-family', '&apos;DejaVu Sans&apos;'],
- ['element', 'style:font-pitch', 'variable']],
- ['tagline', 'style:font-face',
- ['element', 'style:name', 'Nimbus Sans L'],
- ['element', 'svg:font-family', '&apos;Nimbus Sans L&apos;'],
- ['element', 'style:font-family-generic', 'swiss'],
- ['element', 'style:font-pitch', 'variable']]],
-
- # Automatic Styles
- self.automatic_styles,
-
- ['tag', 'office:body',
- self.sheetdata]] # Sheets are generated from the CalcSheet class
-
- # Generate content.xml XML data
- xml = XML()
- self.lines = xml.convert(self.data)
- self.filedata = '\n'.join(self.lines)
- # Return generated data
- return self.filedata
-
- def _ods_manifest(self):
- "Generate ods manifest.xml data"
- self.data = ['tag', 'manifest:manifest',
- ['element', 'xmlns:manifest', 'urn:oasis:names:tc:opendocument:xmlns:manifest:1.0'],
- ['tagline', 'manifest:file-entry',
- ['element', 'manifest:media-type', 'application/vnd.oasis.opendocument.spreadsheet'],
- ['element', 'manifest:full-path', '/']],
- ['tagline', 'manifest:file-entry',
- ['element', 'manifest:media-type', 'application/vnd.sun.xml.ui.configuration'],
- ['element', 'manifest:full-path', 'Configurations2/']],
- ['tagline', 'manifest:file-entry',
- ['element', 'manifest:media-type', ''],
- ['element', 'manifest:full-path', 'Configurations2/accelerator/']],
- ['tagline', 'manifest:file-entry',
- ['element', 'manifest:media-type', ''],
- ['element', 'manifest:full-path', 'Configurations2/accelerator/current.xml']],
- ['tagline', 'manifest:file-entry',
- ['element', 'manifest:media-type', 'text/xml'],
- ['element', 'manifest:full-path', 'content.xml']],
- ['tagline', 'manifest:file-entry',
- ['element', 'manifest:media-type', 'text/xml'],
- ['element', 'manifest:full-path', 'styles.xml']],
- ['tagline', 'manifest:file-entry',
- ['element', 'manifest:media-type', 'text/xml'],
- ['element', 'manifest:full-path', 'meta.xml']],
- ['tagline', 'manifest:file-entry',
- ['element', 'manifest:media-type', 'text/xml'],
- ['element', 'manifest:full-path', 'settings.xml']]]
-
- # Add additional files to manifest list
- for fileset in self.manifest_files:
- (filename, filetype, newname) = fileset
- addfile = ['tagline', 'manifest:file-entry',
- ['element', 'manifest:media-type', filetype],
- ['element', 'manifest:full-path', newname]]
- self.data.append(addfile)
-
- # Generate content.xml XML data
- xml = XML()
- self.lines = xml.convert(self.data)
- self.filedata = '\n'.join(self.lines)
- # Return generated data
- return self.filedata
-
-
- def _ods_settings(self):
- "Generate ods settings.xml data"
- self.data = ['tag', 'office:document-settings',
- ['element', 'xmlns:office', 'urn:oasis:names:tc:opendocument:xmlns:office:1.0'],
- ['element', 'xmlns:xlink', 'http://www.w3.org/1999/xlink'],
- ['element', 'xmlns:config', 'urn:oasis:names:tc:opendocument:xmlns:config:1.0'],
- ['element', 'xmlns:ooo', 'http://openoffice.org/2004/office'],
- ['element', 'office:version', '1.0'],
- ['tag', 'office:settings',
- ['tag', 'config:config-item-set',
- ['element', 'config:name', 'ooo:view-settings'],
- ['tag', 'config:config-item',
- ['element', 'config:name', 'VisibleAreaTop'],
- ['element', 'config:type', 'int'],
- ['data', '0']],
- ['tag', 'config:config-item',
- ['element', 'config:name', 'VisibleAreaLeft'],
- ['element', 'config:type', 'int'],
- ['data', '0']],
- ['tag', 'config:config-item',
- ['element', 'config:name', 'VisibleAreaWidth'],
- ['element', 'config:type', 'int'],
- ['data', '6774']],
- ['tag', 'config:config-item',
- ['element', 'config:name', 'VisibleAreaHeight'],
- ['element', 'config:type', 'int'],
- ['data', '2389']],
- ['tag', 'config:config-item-map-indexed',
- ['element', 'config:name', 'Views'],
- ['tag', 'config:config-item-map-entry',
- ['tag', 'config:config-item',
- ['element', 'config:name', 'ViewId'],
- ['element', 'config:type', 'string'],
- ['data', 'View1']],
- ['tag', 'config:config-item-map-named',
- ['element', 'config:name', 'Tables'],
- ['tag', 'config:config-item-map-entry',
- ['element', 'config:name', 'Sheet1'],
- ['tag', 'config:config-item',
- ['element', 'config:name', 'CursorPositionX'], # Cursor Position A
- ['element', 'config:type', 'int'],
- ['data', '0']],
- ['tag', 'config:config-item',
- ['element', 'config:name', 'CursorPositionY'], # Cursor Position 1
- ['element', 'config:type', 'int'],
- ['data', '0']],
- ['tag', 'config:config-item',
- ['element', 'config:name', 'HorizontalSplitMode'],
- ['element', 'config:type', 'short'],
- ['data', '0']],
- ['tag', 'config:config-item',
- ['element', 'config:name', 'VerticalSplitMode'],
- ['element', 'config:type', 'short'],
- ['data', '0']],
- ['tag', 'config:config-item',
- ['element', 'config:name', 'HorizontalSplitPosition'],
- ['element', 'config:type', 'int'],
- ['data', '0']],
- ['tag', 'config:config-item',
- ['element', 'config:name', 'VerticalSplitPosition'],
- ['element', 'config:type', 'int'],
- ['data', '0']],
- ['tag', 'config:config-item',
- ['element', 'config:name', 'ActiveSplitRange'],
- ['element', 'config:type', 'short'],
- ['data', '2']],
- ['tag', 'config:config-item',
- ['element', 'config:name', 'PositionLeft'],
- ['element', 'config:type', 'int'],
- ['data', '0']],
- ['tag', 'config:config-item',
- ['element', 'config:name', 'PositionRight'],
- ['element', 'config:type', 'int'],
- ['data', '0']],
- ['tag', 'config:config-item',
- ['element', 'config:name', 'PositionTop'],
- ['element', 'config:type', 'int'],
- ['data', '0']],
- ['tag', 'config:config-item',
- ['element', 'config:name', 'PositionBottom'],
- ['element', 'config:type', 'int'],
- ['data', '0']]]],
- ['tag', 'config:config-item',
- ['element', 'config:name', 'ActiveTable'],
- ['element', 'config:type', 'string'],
- ['data', 'Sheet1']],
- ['tag', 'config:config-item',
- ['element', 'config:name', 'HorizontalScrollbarWidth'],
- ['element', 'config:type', 'int'],
- ['data', '270']],
- ['tag', 'config:config-item',
- ['element', 'config:name', 'ZoomType'],
- ['element', 'config:type', 'short'],
- ['data', '0']],
- ['tag', 'config:config-item',
- ['element', 'config:name', 'ZoomValue'],
- ['element', 'config:type', 'int'],
- ['data', '100']],
- ['tag', 'config:config-item',
- ['element', 'config:name', 'PageViewZoomValue'],
- ['element', 'config:type', 'int'],
- ['data', '60']],
- ['tag', 'config:config-item',
- ['element', 'config:name', 'ShowPageBreakPreview'],
- ['element', 'config:type', 'boolean'],
- ['data', 'false']],
- ['tag', 'config:config-item',
- ['element', 'config:name', 'ShowZeroValues'],
- ['element', 'config:type', 'boolean'],
- ['data', 'true']],
- ['tag', 'config:config-item',
- ['element', 'config:name', 'ShowNotes'],
- ['element', 'config:type', 'boolean'],
- ['data', 'true']],
- ['tag', 'config:config-item',
- ['element', 'config:name', 'ShowGrid'],
- ['element', 'config:type', 'boolean'],
- ['data', 'true']],
- ['tag', 'config:config-item',
- ['element', 'config:name', 'GridColor'],
- ['element', 'config:type', 'long'],
- ['data', '12632256']],
- ['tag', 'config:config-item',
- ['element', 'config:name', 'ShowPageBreaks'],
- ['element', 'config:type', 'boolean'],
- ['data', 'true']],
- ['tag', 'config:config-item',
- ['element', 'config:name', 'HasColumnRowHeaders'],
- ['element', 'config:type', 'boolean'],
- ['data', 'true']],
- ['tag', 'config:config-item',
- ['element', 'config:name', 'HasSheetTabs'],
- ['element', 'config:type', 'boolean'],
- ['data', 'true']],
- ['tag', 'config:config-item',
- ['element', 'config:name', 'IsOutlineSymbolsSet'],
- ['element', 'config:type', 'boolean'],
- ['data', 'true']],
- ['tag', 'config:config-item',
- ['element', 'config:name', 'IsSnapToRaster'],
- ['element', 'config:type', 'boolean'],
- ['data', 'false']],
- ['tag', 'config:config-item',
- ['element', 'config:name', 'RasterIsVisible'],
- ['element', 'config:type', 'boolean'],
- ['data', 'false']],
- ['tag', 'config:config-item',
- ['element', 'config:name', 'RasterResolutionX'],
- ['element', 'config:type', 'int'],
- ['data', '1270']],
- ['tag', 'config:config-item',
- ['element', 'config:name', 'RasterResolutionY'],
- ['element', 'config:type', 'int'],
- ['data', '1270']],
- ['tag', 'config:config-item',
- ['element', 'config:name', 'RasterSubdivisionX'],
- ['element', 'config:type', 'int'],
- ['data', '1']],
- ['tag', 'config:config-item',
- ['element', 'config:name', 'RasterSubdivisionY'],
- ['element', 'config:type', 'int'],
- ['data', '1']],
- ['tag', 'config:config-item',
- ['element', 'config:name', 'IsRasterAxisSynchronized'],
- ['element', 'config:type', 'boolean'],
- ['data', 'true']]]]],
- ['tag', 'config:config-item-set',
- ['element', 'config:name', 'ooo:configuration-settings'],
- ['tag', 'config:config-item',
- ['element', 'config:name', 'ShowZeroValues'],
- ['element', 'config:type', 'boolean'],
- ['data', 'true']],
- ['tag', 'config:config-item',
- ['element', 'config:name', 'ShowNotes'],
- ['element', 'config:type', 'boolean'],
- ['data', 'true']],
- ['tag', 'config:config-item',
- ['element', 'config:name', 'ShowGrid'],
- ['element', 'config:type', 'boolean'],
- ['data', 'true']],
- ['tag', 'config:config-item',
- ['element', 'config:name', 'GridColor'],
- ['element', 'config:type', 'long'],
- ['data', '12632256']],
- ['tag', 'config:config-item',
- ['element', 'config:name', 'ShowPageBreaks'],
- ['element', 'config:type', 'boolean'],
- ['data', 'true']],
- ['tag', 'config:config-item',
- ['element', 'config:name', 'LinkUpdateMode'],
- ['element', 'config:type', 'short'],
- ['data', '3']],
- ['tag', 'config:config-item',
- ['element', 'config:name', 'HasColumnRowHeaders'],
- ['element', 'config:type', 'boolean'],
- ['data', 'true']],
- ['tag', 'config:config-item',
- ['element', 'config:name', 'HasSheetTabs'],
- ['element', 'config:type', 'boolean'],
- ['data', 'true']],
- ['tag', 'config:config-item',
- ['element', 'config:name', 'IsOutlineSymbolsSet'],
- ['element', 'config:type', 'boolean'],
- ['data', 'true']],
- ['tag', 'config:config-item',
- ['element', 'config:name', 'IsSnapToRaster'],
- ['element', 'config:type', 'boolean'],
- ['data', 'false']],
- ['tag', 'config:config-item',
- ['element', 'config:name', 'RasterIsVisible'],
- ['element', 'config:type', 'boolean'],
- ['data', 'false']],
- ['tag', 'config:config-item',
- ['element', 'config:name', 'RasterResolutionX'],
- ['element', 'config:type', 'int'],
- ['data', '1270']],
- ['tag', 'config:config-item',
- ['element', 'config:name', 'RasterResolutionY'],
- ['element', 'config:type', 'int'],
- ['data', '1270']],
- ['tag', 'config:config-item',
- ['element', 'config:name', 'RasterSubdivisionX'],
- ['element', 'config:type', 'int'],
- ['data', '1']],
- ['tag', 'config:config-item',
- ['element', 'config:name', 'RasterSubdivisionY'],
- ['element', 'config:type', 'int'],
- ['data', '1']],
- ['tag', 'config:config-item',
- ['element', 'config:name', 'IsRasterAxisSynchronized'],
- ['element', 'config:type', 'boolean'],
- ['data', 'true']],
- ['tag', 'config:config-item',
- ['element', 'config:name', 'AutoCalculate'],
- ['element', 'config:type', 'boolean'],
- ['data', 'true']],
- ['tag', 'config:config-item',
- ['element', 'config:name', 'PrinterName'],
- ['element', 'config:type', 'string'],
- ['data', 'Generic Printer']],
- ['tag', 'config:config-item',
- ['element', 'config:name', 'PrinterSetup'],
- ['element', 'config:type', 'base64Binary'],
- ['data', 'YgH+/0dlbmVyaWMgUHJpbnRlcgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAU0dFTlBSVAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWAAMAqAAAAAAA//8FAFZUAAAkbQAASm9iRGF0YSAxCnByaW50ZXI9R2VuZXJpYyBQcmludGVyCm9yaWVudGF0aW9uPVBvcnRyYWl0CmNvcGllcz0xCnNjYWxlPTEwMAptYXJnaW5kYWp1c3RtZW50PTAsMCwwLDAKY29sb3JkZXB0aD0yNApwc2xldmVsPTAKY29sb3JkZXZpY2U9MApQUERDb250ZXhEYXRhClBhZ2VTaXplOkxldHRlcgAA']],
- ['tag', 'config:config-item',
- ['element', 'config:name', 'ApplyUserData'],
- ['element', 'config:type', 'boolean'],
- ['data', 'true']],
- ['tag', 'config:config-item',
- ['element', 'config:name', 'CharacterCompressionType'],
- ['element', 'config:type', 'short'],
- ['data', '0']],
- ['tag', 'config:config-item',
- ['element', 'config:name', 'IsKernAsianPunctuation'],
- ['element', 'config:type', 'boolean'],
- ['data', 'false']],
- ['tag', 'config:config-item',
- ['element', 'config:name', 'SaveVersionOnClose'],
- ['element', 'config:type', 'boolean'],
- ['data', 'false']],
- ['tag', 'config:config-item',
- ['element', 'config:name', 'UpdateFromTemplate'],
- ['element', 'config:type', 'boolean'],
- ['data', 'false']],
- ['tag', 'config:config-item',
- ['element', 'config:name', 'AllowPrintJobCancel'],
- ['element', 'config:type', 'boolean'],
- ['data', 'true']],
- ['tag', 'config:config-item',
- ['element', 'config:name', 'LoadReadonly'],
- ['element', 'config:type', 'boolean'],
- ['data', 'false']]]]]
-
- # Generate content.xml XML data
- xml = XML()
- self.lines = xml.convert(self.data)
- self.filedata = '\n'.join(self.lines)
- # Return generated data
- return self.filedata
-
-
- def _ods_styles(self):
- "Generate ods styles.xml data"
- self.data = ['tag', 'office:document-styles',
- ['element', 'xmlns:office', 'urn:oasis:names:tc:opendocument:xmlns:office:1.0'],
- ['element', 'xmlns:style', 'urn:oasis:names:tc:opendocument:xmlns:style:1.0'],
- ['element', 'xmlns:text', 'urn:oasis:names:tc:opendocument:xmlns:text:1.0'],
- ['element', 'xmlns:table', 'urn:oasis:names:tc:opendocument:xmlns:table:1.0'],
- ['element', 'xmlns:draw', 'urn:oasis:names:tc:opendocument:xmlns:drawing:1.0'],
- ['element', 'xmlns:fo', 'urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0'],
- ['element', 'xmlns:xlink', 'http://www.w3.org/1999/xlink'],
- ['element', 'xmlns:dc', 'http://purl.org/dc/elements/1.1/'],
- ['element', 'xmlns:meta', 'urn:oasis:names:tc:opendocument:xmlns:meta:1.0'],
- ['element', 'xmlns:number', 'urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0'],
- ['element', 'xmlns:svg', 'urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0'],
- ['element', 'xmlns:chart', 'urn:oasis:names:tc:opendocument:xmlns:chart:1.0'],
- ['element', 'xmlns:dr3d', 'urn:oasis:names:tc:opendocument:xmlns:dr3d:1.0'],
- ['element', 'xmlns:math', 'http://www.w3.org/1998/Math/MathML'],
- ['element', 'xmlns:form', 'urn:oasis:names:tc:opendocument:xmlns:form:1.0'],
- ['element', 'xmlns:script', 'urn:oasis:names:tc:opendocument:xmlns:script:1.0'],
- ['element', 'xmlns:ooo', 'http://openoffice.org/2004/office'],
- ['element', 'xmlns:ooow', 'http://openoffice.org/2004/writer'],
- ['element', 'xmlns:oooc', 'http://openoffice.org/2004/calc'],
- ['element', 'xmlns:dom', 'http://www.w3.org/2001/xml-events'],
- ['element', 'office:version', '1.0'],
- ['tag', 'office:font-face-decls',
- ['tagline', 'style:font-face',
- ['element', 'style:name', 'DejaVu Sans'],
- ['element', 'svg:font-family', '&apos;DejaVu Sans&apos;'],
- ['element', 'style:font-pitch', 'variable']],
- ['tagline', 'style:font-face',
- ['element', 'style:name', 'Nimbus Sans L'],
- ['element', 'svg:font-family', '&apos;Nimbus Sans L&apos;'],
- ['element', 'style:font-family-generic', 'swiss'],
- ['element', 'style:font-pitch', 'variable']]],
- ['tag', 'office:styles',
- ['tag', 'style:default-style',
- ['element', 'style:family', 'table-cell'],
- ['tagline', 'style:table-cell-properties',
- ['element', 'style:decimal-places', '2']],
- ['tagline', 'style:paragraph-properties',
- ['element', 'style:tab-stop-distance', '0.5in']],
- ['tagline', 'style:text-properties',
- ['element', 'style:font-name', 'Nimbus Sans L'],
- ['element', 'fo:language', 'en'],
- ['element', 'fo:country', 'US'],
- ['element', 'style:font-name-asian', 'DejaVu Sans'],
- ['element', 'style:language-asian', 'none'],
- ['element', 'style:country-asian', 'none'],
- ['element', 'style:font-name-complex', 'DejaVu Sans'],
- ['element', 'style:language-complex', 'none'],
- ['element', 'style:country-complex', 'none']]],
- ['tag', 'number:number-style',
- ['element', 'style:name', 'N0'],
- ['tagline', 'number:number',
- ['element', 'number:min-integer-digits', '1']]],
- ['tag', 'number:currency-style',
- ['element', 'style:name', 'N104P0'],
- ['element', 'style:volatile', 'true'],
- ['tag', 'number:currency-symbol',
- ['element', 'number:language', 'en'],
- ['element', 'number:country', 'US'],
- ['data', '$']],
- ['tagline', 'number:number',
- ['element', 'number:decimal-places', '2'],
- ['element', 'number:min-integer-digits', '1'],
- ['element', 'number:grouping', 'true']]],
- ['tag', 'number:currency-style',
- ['element', 'style:name', 'N104'],
- ['tagline', 'style:text-properties',
- ['element', 'fo:color', '#ff0000']],
- ['tag', 'number:text',
- ['data', '-']],
- ['tag', 'number:currency-symbol',
- ['element', 'number:language', 'en'],
- ['element', 'number:country', 'US'],
- ['data', '$']],
- ['tagline', 'number:number',
- ['element', 'number:decimal-places', '2'],
- ['element', 'number:min-integer-digits', '1'],
- ['element', 'number:grouping', 'true']],
- ['tagline', 'style:map',
- ['element', 'style:condition', 'value()&gt;=0'],
- ['element', 'style:apply-style-name', 'N104P0']]],
- ['tagline', 'style:style',
- ['element', 'style:name', 'Default'],
- ['element', 'style:family', 'table-cell']],
- ['tag', 'style:style',
- ['element', 'style:name', 'Result'],
- ['element', 'style:family', 'table-cell'],
- ['element', 'style:parent-style-name', 'Default'],
- ['tagline', 'style:text-properties',
- ['element', 'fo:font-style', 'italic'],
- ['element', 'style:text-underline-style', 'solid'],
- ['element', 'style:text-underline-width', 'auto'],
- ['element', 'style:text-underline-color', 'font-color'],
- ['element', 'fo:font-weight', 'bold']]],
- ['tagline', 'style:style',
- ['element', 'style:name', 'Result2'],
- ['element', 'style:family', 'table-cell'],
- ['element', 'style:parent-style-name', 'Result'],
- ['element', 'style:data-style-name', 'N104']],
- ['tag', 'style:style',
- ['element', 'style:name', 'Heading'],
- ['element', 'style:family', 'table-cell'],
- ['element', 'style:parent-style-name', 'Default'],
- ['tagline', 'style:table-cell-properties',
- ['element', 'style:text-align-source', 'fix'],
- ['element', 'style:repeat-content', 'false']],
- ['tagline', 'style:paragraph-properties',
- ['element', 'fo:text-align', 'center']],
- ['tagline', 'style:text-properties',
- ['element', 'fo:font-size', '16pt'],
- ['element', 'fo:font-style', 'italic'],
- ['element', 'fo:font-weight', 'bold']]],
- ['tag', 'style:style',
- ['element', 'style:name', 'Heading1'],
- ['element', 'style:family', 'table-cell'],
- ['element', 'style:parent-style-name', 'Heading'],
- ['tagline', 'style:table-cell-properties',
- ['element', 'style:rotation-angle', '90']]]],
- ['tag', 'office:automatic-styles',
- ['tag', 'style:page-layout',
- ['element', 'style:name', 'pm1'],
- ['tagline', 'style:page-layout-properties',
- ['element', 'style:writing-mode', 'lr-tb']],
- ['tag', 'style:header-style',
- ['tagline', 'style:header-footer-properties',
- ['element', 'fo:min-height', '0.2957in'],
- ['element', 'fo:margin-left', '0in'],
- ['element', 'fo:margin-right', '0in'],
- ['element', 'fo:margin-bottom', '0.0984in']]],
- ['tag', 'style:footer-style',
- ['tagline', 'style:header-footer-properties',
- ['element', 'fo:min-height', '0.2957in'],
- ['element', 'fo:margin-left', '0in'],
- ['element', 'fo:margin-right', '0in'],
- ['element', 'fo:margin-top', '0.0984in']]]],
- ['tag', 'style:page-layout',
- ['element', 'style:name', 'pm2'],
- ['tagline', 'style:page-layout-properties',
- ['element', 'style:writing-mode', 'lr-tb']],
- ['tag', 'style:header-style',
- ['tag', 'style:header-footer-properties',
- ['element', 'fo:min-height', '0.2957in'],
- ['element', 'fo:margin-left', '0in'],
- ['element', 'fo:margin-right', '0in'],
- ['element', 'fo:margin-bottom', '0.0984in'],
- ['element', 'fo:border', '0.0346in solid #000000'],
- ['element', 'fo:padding', '0.0071in'],
- ['element', 'fo:background-color', '#c0c0c0'],
- ['tagline', 'style:background-image']]],
- ['tag', 'style:footer-style',
- ['tag', 'style:header-footer-properties',
- ['element', 'fo:min-height', '0.2957in'],
- ['element', 'fo:margin-left', '0in'],
- ['element', 'fo:margin-right', '0in'],
- ['element', 'fo:margin-top', '0.0984in'],
- ['element', 'fo:border', '0.0346in solid #000000'],
- ['element', 'fo:padding', '0.0071in'],
- ['element', 'fo:background-color', '#c0c0c0'],
- ['tagline', 'style:background-image']]]]],
- ['tag', 'office:master-styles',
- ['tag', 'style:master-page',
- ['element', 'style:name', 'Default'],
- ['element', 'style:page-layout-name', 'pm1'],
- ['tag', 'style:header',
- ['tag', 'text:p',
- ['data', '<text:sheet-name>???</text:sheet-name>']]],
- ['tagline', 'style:header-left',
- ['element', 'style:display', 'false']],
- ['tag', 'style:footer',
- ['tag', 'text:p',
- ['data', 'Page <text:page-number>1</text:page-number>']]],
- ['tagline', 'style:footer-left',
- ['element', 'style:display', 'false']]],
- ['tag', 'style:master-page',
- ['element', 'style:name', 'Report'],
- ['element', 'style:page-layout-name', 'pm2'],
- ['tag', 'style:header',
- ['tag', 'style:region-left',
- ['tag', 'text:p',
- ['data', '<text:sheet-name>???</text:sheet-name> (<text:title>???</text:title>)']]],
- ['tag', 'style:region-right',
- ['tag', 'text:p',
- ['data', '<text:date style:data-style-name="N2" text:date-value="2006-09-29">09/29/2006</text:date>, <text:time>13:02:56</text:time>']]]],
- ['tagline', 'style:header-left',
- ['element', 'style:display', 'false']],
- ['tag', 'style:footer',
- ['tag', 'text:p',
- ['data', 'Page <text:page-number>1</text:page-number> / <text:page-count>99</text:page-count>']]],
- ['tagline', 'style:footer-left',
- ['element', 'style:display', 'false']]]]]
-
-
- # Generate content.xml XML data
- xml = XML()
- self.lines = xml.convert(self.data)
- self.filedata = '\n'.join(self.lines)
- # Return generated data
- return self.filedata
-
-class Writer:
- "Writer Class - Used to create OpenDocument Format Writer Documents."
- def __init__(self):
- "Initialize ooolib Writer instance"
- # Default to no debugging
- self.debug = False
- self.meta = Meta('odt')
-
- def set_meta(self, metaname, value):
- "Set meta data in your document."
- self.meta.set_meta(metaname, value)
-
- def save(self, filename):
- """Save .odt document
-
- The save function saves the current .odt document.
- """
- if self.debug: print "Writing %s" % filename
- self.savefile = zipfile.ZipFile(filename, "w")
- if self.debug: print " meta.xml"
- self._zip_insert(self.savefile, "meta.xml", self.meta.get_meta())
- if self.debug: print " mimetype"
- self._zip_insert(self.savefile, "mimetype", "application/vnd.oasis.opendocument.text")
- if self.debug: print " META-INF/manifest.xml"
- self._zip_insert(self.savefile, "META-INF/manifest.xml", self._odt_manifest())
- if self.debug: print " content.xml"
- self._zip_insert(self.savefile, "content.xml", self._odt_content())
- if self.debug: print " settings.xml"
- # self._zip_insert(self.savefile, "settings.xml", self._odt_settings())
- if self.debug: print " styles.xml"
- # self._zip_insert(self.savefile, "styles.xml", self._odt_styles())
-
- # We need to close the file now that we are done creating it.
- self.savefile.close()
-
- def _zip_insert(self, file, filename, data):
- now = time.localtime(time.time())[:6]
- info = zipfile.ZipInfo(filename)
- info.date_time = now
- info.compress_type = zipfile.ZIP_DEFLATED
- file.writestr(info, data)
-
- def _odt_manifest(self):
- "Generate odt manifest.xml data"
-
- self.data = ['tag', 'manifest:manifest',
- ['element', 'xmlns:manifest', 'urn:oasis:names:tc:opendocument:xmlns:manifest:1.0'],
- ['tagline', 'manifest:file-entry',
- ['element', 'manifest:media-type', 'application/vnd.oasis.opendocument.text'],
- ['element', 'manifest:full-path', '/']],
- ['tagline', 'manifest:file-entry',
- ['element', 'manifest:media-type', 'text/xml'],
- ['element', 'manifest:full-path', 'content.xml']],
- ['tagline', 'manifest:file-entry',
- ['element', 'manifest:media-type', 'text/xml'],
- ['element', 'manifest:full-path', 'styles.xml']],
- ['tagline', 'manifest:file-entry',
- ['element', 'manifest:media-type', 'text/xml'],
- ['element', 'manifest:full-path', 'meta.xml']],
- ['tagline', 'manifest:file-entry',
- ['element', 'manifest:media-type', 'text/xml'],
- ['element', 'manifest:full-path', 'settings.xml']]]
-
- # Generate content.xml XML data
- xml = XML()
- self.lines = xml.convert(self.data)
- self.lines.insert(1, '<!DOCTYPE manifest:manifest PUBLIC "-//OpenOffice.org//DTD Manifest 1.0//EN" "Manifest.dtd">')
- self.filedata = '\n'.join(self.lines)
- # Return generated data
- return self.filedata
-
- def _odt_content(self):
- "Generate odt content.xml data"
-
- self.data = ['tag', 'office:document-content',
- ['element', 'xmlns:office', 'urn:oasis:names:tc:opendocument:xmlns:office:1.0'],
- ['element', 'xmlns:style', 'urn:oasis:names:tc:opendocument:xmlns:style:1.0'],
- ['element', 'xmlns:text', 'urn:oasis:names:tc:opendocument:xmlns:text:1.0'],
- ['element', 'xmlns:table', 'urn:oasis:names:tc:opendocument:xmlns:table:1.0'],
- ['element', 'xmlns:draw', 'urn:oasis:names:tc:opendocument:xmlns:drawing:1.0'],
- ['element', 'xmlns:fo', 'urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0'],
- ['element', 'xmlns:xlink', 'http://www.w3.org/1999/xlink'],
- ['element', 'xmlns:dc', 'http://purl.org/dc/elements/1.1/'],
- ['element', 'xmlns:meta', 'urn:oasis:names:tc:opendocument:xmlns:meta:1.0'],
- ['element', 'xmlns:number', 'urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0'],
- ['element', 'xmlns:svg', 'urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0'],
- ['element', 'xmlns:chart', 'urn:oasis:names:tc:opendocument:xmlns:chart:1.0'],
- ['element', 'xmlns:dr3d', 'urn:oasis:names:tc:opendocument:xmlns:dr3d:1.0'],
- ['element', 'xmlns:math', 'http://www.w3.org/1998/Math/MathML'],
- ['element', 'xmlns:form', 'urn:oasis:names:tc:opendocument:xmlns:form:1.0'],
- ['element', 'xmlns:script', 'urn:oasis:names:tc:opendocument:xmlns:script:1.0'],
- ['element', 'xmlns:ooo', 'http://openoffice.org/2004/office'],
- ['element', 'xmlns:ooow', 'http://openoffice.org/2004/writer'],
- ['element', 'xmlns:oooc', 'http://openoffice.org/2004/calc'],
- ['element', 'xmlns:dom', 'http://www.w3.org/2001/xml-events'],
- ['element', 'xmlns:xforms', 'http://www.w3.org/2002/xforms'],
- ['element', 'xmlns:xsd', 'http://www.w3.org/2001/XMLSchema'],
- ['element', 'xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance'],
- ['element', 'office:version', '1.0'],
- ['tagline', 'office:scripts'],
- ['tag', 'office:font-face-decls',
- ['tagline', 'style:font-face',
- ['element', 'style:name', 'DejaVu Sans'],
- ['element', 'svg:font-family', '&apos;DejaVu Sans&apos;'],
- ['element', 'style:font-pitch', 'variable']],
+ "Returns an XML friendly copy of the data string"
+
+ data = u"{}".format(data) # This line thanks to Chris Ender (and updated for Py2/Py3 by Zdenek Bohm)
+
+ data = data.replace('&', '&amp;')
+ data = data.replace("'", '&apos;')
+ data = data.replace('"', '&quot;')
+ data = data.replace('<', '&lt;')
+ data = data.replace('>', '&gt;')
+ data = data.replace('\t', '<text:tab-stop/>')
+ data = data.replace('\n', '<text:line-break/>')
+ return data
+
+
+class XML(object):
+ "XML Class - Used to convert nested lists into XML"
+
+ def _xmldata(self, data):
+ data.pop(0) # data type
+ datavalue = data.pop(0)
+ outstring = '%s' % datavalue
+ return outstring
+
+ def _xmltag(self, data):
+ outstring = ''
+ # First two
+ data.pop(0) # datatype
+ dataname = data.pop(0)
+ outstring = '<%s' % dataname
+ # Element Section
+ element = 1
+ while(data):
+ # elements
+ newdata = data.pop(0)
+ if newdata[0] == 'element' and element:
+ newstring = self._xmlelement(newdata)
+ outstring = '%s %s' % (outstring, newstring)
+ continue
+ if newdata[0] != 'element' and element:
+ element = 0
+ outstring = '%s>' % outstring
+ if newdata[0] == 'tag' or newdata[0] == 'tagline':
+ outstring = '%s\n' % outstring
+ if newdata[0] == 'tag':
+ newstring = self._xmltag(newdata)
+ outstring = '%s%s' % (outstring, newstring)
+ continue
+ if newdata[0] == 'tagline':
+ newstring = self._xmltagline(newdata)
+ outstring = '%s%s' % (outstring, newstring)
+ continue
+ if newdata[0] == 'data':
+ newstring = self._xmldata(newdata)
+ outstring = '%s%s' % (outstring, newstring)
+ continue
+ if element:
+ element = 0
+ outstring = '%s>\n' % outstring
+ outstring = '%s</%s>\n' % (outstring, dataname)
+ return outstring
+
+ def _xmltagline(self, data):
+ outstring = ''
+ # First two
+ data.pop(0) # datatype
+ dataname = data.pop(0)
+ outstring = '<%s' % dataname
+ # Element Section
+ while(data):
+ # elements
+ newdata = data.pop(0)
+ if newdata[0] != 'element':
+ break
+ newstring = self._xmlelement(newdata)
+ outstring = '%s %s' % (outstring, newstring)
+ outstring = '%s/>\n' % outstring
+ # Non-Element Section should not exist
+ return outstring
+
+ def _xmlelement(self, data):
+ data.pop(0) # datatype
+ dataname = data.pop(0)
+ datavalue = data.pop(0)
+ outstring = '%s="%s"' % (dataname, datavalue)
+ return outstring
+
+ def convert(self, data):
+ """Convert nested lists into XML
+
+ The convert method takes a nested lists and converts them
+ into XML to be used in Open Document Format documents.
+ There are three types of lists that are recognized at this
+ time. They are as follows:
+
+ 'tag' - Tag opens a set of data that is eventually closed
+ with a similar tag.
+ List: ['tag', 'xml']
+ XML: <xml></xml>
+
+ 'tagline' - Taglines are similar to tags, except they open
+ and close themselves.
+ List: ['tagline', 'xml']
+ XML: <xml/>
+
+ 'element' - Elements are pieces of information stored in an
+ opening tag or tagline.
+ List: ['element', 'color', 'blue']
+ XML: color="blue"
+
+ 'data' - Data is plain text directly inserted into the XML
+ document.
+ List: ['data', 'hello']
+ XML: hello
+
+ Bring them all together for something like this.
+
+ Lists:
+ ['tag', 'xml', ['element', 'a', 'b'], ['tagline', 'xml2'],
+ ['data', 'asdf']]
+
+ XML:
+ <xml a="b"><xml2/>asdf</xml>
+ """
+ outlines = []
+ outlines.append('<?xml version="1.0" encoding="UTF-8"?>')
+ if isinstance(data, (list, tuple)) and len(data) > 0:
+ if data[0] == 'tag':
+ outlines.append(self._xmltag(data))
+ return outlines
+
+
+class Meta(object):
+ "Meta Data Class"
+
+ def __init__(self, doctype, debug=False):
+ self.doctype = doctype
+
+ # Set the debug mode
+ self.debug = debug
+
+ # The generator should always default to the version number
+ self.meta_generator = version()
+ self.meta_title = ''
+ self.meta_subject = ''
+ self.meta_description = ''
+ self.meta_keywords = []
+ self.meta_creator = 'ooolib-python'
+ self.meta_editor = ''
+ self.meta_user1_name = 'Info 1'
+ self.meta_user2_name = 'Info 2'
+ self.meta_user3_name = 'Info 3'
+ self.meta_user4_name = 'Info 4'
+ self.meta_user1_value = ''
+ self.meta_user2_value = ''
+ self.meta_user3_value = ''
+ self.meta_user4_value = ''
+ self.meta_creation_date = self.meta_time()
+
+ # Parser data
+ self.parser_element_list = []
+ self.parser_element = ""
+ self.parser_count = 0
+
+ def set_meta(self, metaname, value):
+ """Set meta data in your document.
+
+ Currently implemented metaname options are as follows:
+ 'creator' - The document author
+ """
+ if metaname == 'creator': self.meta_creator = value
+ if metaname == 'editor': self.meta_editor = value
+ if metaname == 'title': self.meta_title = value
+ if metaname == 'subject': self.meta_subject = value
+ if metaname == 'description': self.meta_description = value
+ if metaname == 'user1name': self.meta_user1_name = value
+ if metaname == 'user2name': self.meta_user2_name = value
+ if metaname == 'user3name': self.meta_user3_name = value
+ if metaname == 'user4name': self.meta_user4_name = value
+ if metaname == 'user1value': self.meta_user1_value = value
+ if metaname == 'user2value': self.meta_user2_value = value
+ if metaname == 'user3value': self.meta_user3_value = value
+ if metaname == 'user4value': self.meta_user4_value = value
+ if metaname == 'keyword':
+ if value not in self.meta_keywords:
+ self.meta_keywords.append(value)
+
+ def get_meta_value(self, metaname):
+ "Get meta data value for a given metaname."
+
+ if metaname == 'creator': return self.meta_creator
+ if metaname == 'editor': return self.meta_editor
+ if metaname == 'title': return self.meta_title
+ if metaname == 'subject': return self.meta_subject
+ if metaname == 'description': return self.meta_description
+ if metaname == 'user1name': return self.meta_user1_name
+ if metaname == 'user2name': return self.meta_user2_name
+ if metaname == 'user3name': return self.meta_user3_name
+ if metaname == 'user4name': return self.meta_user4_name
+ if metaname == 'user1value': return self.meta_user1_value
+ if metaname == 'user2value': return self.meta_user2_value
+ if metaname == 'user3value': return self.meta_user3_value
+ if metaname == 'user4value': return self.meta_user4_value
+ if metaname == 'keyword': return self.meta_keywords
+
+ def meta_time(self):
+ "Return time string in meta data format"
+ return "%04d-%02d-%02dT%02d:%02d:%02d" % time.localtime()[:6]
+
+ def parse_start_element(self, name, attrs):
+ if self.debug: print('* Start element: {}'.format(name))
+ self.parser_element_list.append(name)
+ self.parser_element = self.parser_element_list[-1]
+
+ # Need the meta name from the user-defined tags
+ if self.parser_element == "meta:user-defined":
+ self.parser_count += 1
+ # Set user-defined name
+ self.set_meta("user%dname" % self.parser_count, attrs['meta:name'])
+
+ # Debugging statements
+ if self.debug: print(" List: {}".format(self.parser_element_list))
+ if self.debug: print(" Attributes: {}".format(attrs))
+
+ def parse_end_element(self, name):
+ if self.debug: print('* End element: {}'.format(name))
+ if name != self.parser_element:
+ print("Tag Mismatch: '%s' != '%s'" % (name, self.parser_element))
+ self.parser_element_list.pop()
+
+ # Readjust parser_element_list and parser_element
+ if self.parser_element_list:
+ self.parser_element = self.parser_element_list[-1]
+ else:
+ self.parser_element = ""
+
+ def parse_char_data(self, data):
+ if self.debug: print(" Character data: {!r}".format(data))
+
+ # Collect Meta data fields
+ if self.parser_element == "dc:title":
+ self.set_meta("title", data)
+ if self.parser_element == "dc:description":
+ self.set_meta("description", data)
+ if self.parser_element == "dc:subject":
+ self.set_meta("subject", data)
+ if self.parser_element == "meta:initial-creator":
+ self.set_meta("creator", data)
+
+ # Try to maintain the same creation date
+ if self.parser_element == "meta:creation-date":
+ self.meta_creation_date = data
+
+ # The user defined fields need to be kept track of, parser_count does that
+ if self.parser_element == "meta:user-defined":
+ self.set_meta("user%dvalue" % self.parser_count, data)
+
+ def meta_parse(self, data):
+ "Parse Meta Data from a meta.xml file"
+
+ # Debugging statements
+ if self.debug:
+ # Sometimes it helps to see the document that was read from
+ print(data)
+ print("\n\n\n")
+
+ # Create parser
+ parser = xml.parsers.expat.ParserCreate()
+ # Set up parser callback functions
+ parser.StartElementHandler = self.parse_start_element
+ parser.EndElementHandler = self.parse_end_element
+ parser.CharacterDataHandler = self.parse_char_data
+
+ # Actually parse the data
+ parser.Parse(data, 1)
+
+ def get_meta(self):
+ "Generate meta.xml file data"
+ self.meta_date = self.meta_time()
+ self.data = ['tag', 'office:document-meta',
+ ['element', 'xmlns:office', 'urn:oasis:names:tc:opendocument:xmlns:office:1.0'],
+ ['element', 'xmlns:xlink', 'http://www.w3.org/1999/xlink'],
+ ['element', 'xmlns:dc', 'http://purl.org/dc/elements/1.1/'],
+ ['element', 'xmlns:meta', 'urn:oasis:names:tc:opendocument:xmlns:meta:1.0'],
+ ['element', 'xmlns:ooo', 'http://openoffice.org/2004/office'],
+ ['element', 'office:version', '1.0'],
+ ['tag', 'office:meta',
+ ['tag', 'meta:generator', # Was: 'OpenOffice.org/2.0$Linux OpenOffice.org_project/680m5$Build-9011'
+ ['data', self.meta_generator]], # Generator is set the the ooolib-python version.
+ ['tag', 'dc:title',
+ ['data', self.meta_title]], # This data is the document title
+ ['tag', 'dc:description',
+ ['data', self.meta_description]], # This data is the document description
+ ['tag', 'dc:subject',
+ ['data', self.meta_subject]], # This data is the document subject
+ ['tag', 'meta:initial-creator',
+ ['data', self.meta_creator]], # This data is the document creator
+ ['tag', 'meta:creation-date',
+ ['data', self.meta_creation_date]], # This is the original creation date of the document
+ ['tag', 'dc:creator',
+ ['data', self.meta_editor]], # This data is the document editor
+ ['tag', 'dc:date',
+ ['data', self.meta_date]], # This is the last modified date of the document
+ ['tag', 'dc:language',
+ ['data', 'en-US']], # We will probably always use en-US for language
+ ['tag', 'meta:editing-cycles',
+ ['data', '1']], # Edit cycles will probably always be 1 for generated documents
+ ['tag', 'meta:editing-duration',
+ ['data', 'PT0S']], # Editing duration is modified - creation date
+ ['tag', 'meta:user-defined',
+ ['element', 'meta:name', self.meta_user1_name],
+ ['data', self.meta_user1_value]],
+ ['tag', 'meta:user-defined',
+ ['element', 'meta:name', self.meta_user2_name],
+ ['data', self.meta_user2_value]],
+ ['tag', 'meta:user-defined',
+ ['element', 'meta:name', self.meta_user3_name],
+ ['data', self.meta_user3_value]],
+ ['tag', 'meta:user-defined',
+ ['element', 'meta:name', self.meta_user4_name],
+ ['data', self.meta_user4_value]]]]
+# ['tagline', 'meta:document-statistic',
+# ['element', 'meta:table-count', len(self.sheets)], # len(self.sheets) ?
+# ['element', 'meta:cell-count', '15']]]] # Not sure how to keep track
+
+ # Generate content.xml XML data
+ xmldoc = XML()
+ self.lines = xmldoc.convert(self.data)
+ self.filedata = '\n'.join(self.lines)
+ # Return generated data
+ return self.filedata
+
+
+class CalcStyles(object):
+ "Calc Style Management - Used to keep track of created styles."
+
+ def __init__(self):
+ self.style_config = {}
+ # Style Counters
+ self.style_table = 1
+ self.style_column = 1
+ self.style_row = 1
+ self.style_cell = 1
+ # Style Properties (Defaults) - To be used later
+ self.property_column_width_default = '0.8925in' # Default Column Width
+ self.property_row_height_default = '0.189in' # Default Row Height
+ # Set Defaults
+ self.property_column_width = '0.8925in' # Default Column Width
+ self.property_row_height = '0.189in' # Default Row Height
+ self.property_cell_bold = False # Bold off be default
+ self.property_cell_italic = False # Italic off be default
+ self.property_cell_underline = False # Underline off be default
+ self.property_cell_fg_color = 'default' # Text Color Default
+ self.property_cell_bg_color = 'default' # Cell Background Default
+ self.property_cell_bg_image = 'none' # Cell Background Default
+ self.property_cell_fontsize = '10' # Cell Font Size Default
+ self.property_cell_valign = 'default' # Vertial Alignment Default
+ self.property_cell_halign = 'default' # Horizantal Alignment Default
+ self.cardinal = ['top', 'right', 'bottom', 'left'] # four cardinal
+ self.property_cell_border = [False, False, False, False]
+ self.property_cell_padding = [False, False, False, False]
+ self.property_cell_wrap_option = False
+ self.property_cell_hyphenate = False
+
+ def get_next_style(self, style):
+ "Returns the next style code for the given style"
+ style_code = ""
+ if style == 'table':
+ style_code = 'ta%d' % self.style_table
+ self.style_table += 1
+ if style == 'column':
+ style_code = 'co%d' % self.style_column
+ self.style_column += 1
+ if style == 'row':
+ style_code = 'ro%d' % self.style_row
+ self.style_row += 1
+ if style == 'cell':
+ style_code = 'ce%d' % self.style_cell
+ self.style_cell += 1
+ return style_code
+
+ def set_property(self, style, name, value):
+ "Sets a property which will later be turned into a code"
+ if style == 'column':
+ if name == 'style:column-width': self.property_column_width = value
+ elif style == 'row':
+ if name == 'style:row-height': self.property_row_height = value
+ elif style == 'cell':
+ if isinstance(value, bool):
+ if name == 'bold':
+ self.property_cell_bold = value
+ elif name == 'italic':
+ self.property_cell_italic = value
+ elif name == 'underline':
+ self.property_cell_underline = value
+ if name == 'fontsize':
+ self.property_cell_fontsize = value
+ elif name == 'hyphenate':
+ self.property_cell_hyphenate = value
+ elif name == 'color':
+ self.property_cell_fg_color = 'default'
+ redata = re.search("^(#[\da-fA-F]{6})$", value)
+ if redata: self.property_cell_fg_color = value.lower()
+ elif name == 'background':
+ self.property_cell_bg_color = 'default'
+ redata = re.search("^(#[\da-fA-F]{6})$", value)
+ if redata: self.property_cell_bg_color = value.lower()
+ elif name == 'backgroundimage':
+ self.property_cell_bg_image = value
+ elif name == 'valign':
+ self.property_cell_valign = value
+ elif name == 'halign':
+ self.property_cell_halign = value
+ elif name == 'wrap-option':
+ self.property_cell_wrap_option = value
+
+ self.init_cell_cardinal('border', name, value)
+ self.init_cell_cardinal('padding', name, value)
+
+ def init_cell_cardinal(self, key, name, value):
+ "Init property border, padding"
+ property = getattr(self, 'property_cell_%s' % key)
+ if name == key:
+ setattr(self, 'property_cell_%s' % key, [value] * 4)
+ match = re.match('%s-(top|right|bottom|left)' % key, name)
+ if match:
+ property[self.cardinal.index(match.group(1))] = value
+
+ def get_style_code(self, style):
+ style_code = ""
+ if style == 'table':
+ style_code = "ta1"
+ if style == 'column':
+ style_data = tuple([style,
+ ('style:column-width', self.property_column_width)])
+ if style_data in self.style_config:
+ # Style Exists, return code
+ style_code = self.style_config[style_data]
+ else:
+ # Style does not exist, create code and return it
+ style_code = self.get_next_style(style)
+ self.style_config[style_data] = style_code
+ if style == 'row':
+ style_data = tuple([style,
+ ('style:row-height', self.property_row_height)])
+ if style_data in self.style_config:
+ # Style Exists, return code
+ style_code = self.style_config[style_data]
+ else:
+ # Style does not exist, create code and return it
+ style_code = self.get_next_style(style)
+ self.style_config[style_data] = style_code
+ if style == 'cell':
+ style_data = [style]
+ # Add additional styles
+ if self.property_cell_bold: style_data.append(('bold', True))
+ if self.property_cell_italic: style_data.append(('italic', True))
+ if self.property_cell_underline: style_data.append(('underline', True))
+ if self.property_cell_fontsize != '10':
+ style_data.append(('fontsize', self.property_cell_fontsize))
+ if self.property_cell_hyphenate:
+ style_data.append(('hyphenate', 'true'))
+ if self.property_cell_fg_color != 'default':
+ style_data.append(('color', self.property_cell_fg_color))
+ if self.property_cell_bg_color != 'default':
+ style_data.append(('background', self.property_cell_bg_color))
+ if self.property_cell_bg_image != 'none':
+ style_data.append(('backgroundimage', self.property_cell_bg_image))
+ if self.property_cell_valign != 'default':
+ style_data.append(('valign', self.property_cell_valign))
+ if self.property_cell_halign != 'default':
+ style_data.append(('halign', self.property_cell_halign))
+ if self.property_cell_wrap_option:
+ style_data.append(('wrap-option', self.property_cell_wrap_option))
+
+ style_data.extend(self.get_cardinal_style('border'))
+ style_data.extend(self.get_cardinal_style('padding'))
+
+ style_data = tuple(style_data)
+ if style_data in self.style_config:
+ # Style Exists, return code
+ style_code = self.style_config[style_data]
+ else:
+ # Style does not exist, create code and return it
+ style_code = self.get_next_style(style)
+ self.style_config[style_data] = style_code
+ return style_code
+
+ def get_cardinal_style(self, key):
+ "Get cardinal styles"
+ style_data = []
+ property = getattr(self, 'property_cell_%s' % key)
+ borders = tuple(set(property))
+ if not (borders[0] is False and len(borders) == 1):
+ if len(borders) == 1:
+ style_data.append((key, borders[0]))
+ else:
+ for pos in range(4):
+ value = property[pos]
+ if value:
+ style_data.append(('%s-%s' % (key, self.cardinal[pos]), value))
+ return style_data
+
+ def get_automatic_styles(self):
+ "Return 'office:automatic-styles' lists"
+ automatic_styles = ['tag', 'office:automatic-styles']
+
+ for style_data in self.style_config:
+ style_code = self.style_config[style_data]
+ style_data = list(style_data)
+ style = style_data.pop(0)
+
+ if style == 'column':
+ style_list = ['tag', 'style:style',
+ ['element', 'style:name', style_code], # Column 'co1' properties
+ ['element', 'style:family', 'table-column']]
+ tagline = ['tagline', 'style:table-column-properties',
+ ['element', 'fo:break-before', 'auto']] # unsure what break before means
+
+ for name, value in style_data:
+ if name == 'style:column-width':
+ tagline.append(['element', 'style:column-width', value])
+ style_list.append(tagline)
+ automatic_styles.append(style_list)
+
+ if style == 'row':
+ style_list = ['tag', 'style:style',
+ ['element', 'style:name', style_code], # Column 'ro1' properties
+ ['element', 'style:family', 'table-row']]
+ tagline = ['tagline', 'style:table-row-properties']
+
+ for name, value in style_data:
+ if name == 'style:row-height':
+ tagline.append(['element', 'style:row-height', value])
+ tagline.append(['element', 'fo:break-before', 'auto'])
+# tagline.append(['element', 'style:use-optimal-row-height', 'true']) # Overrides settings
+ style_list.append(tagline)
+ automatic_styles.append(style_list)
+
+ if style == 'cell':
+ style_list = ['tag', 'style:style',
+ ['element', 'style:name', style_code], # ce1 style
+ ['element', 'style:family', 'table-cell'], # cell
+ ['element', 'style:parent-style-name', 'Default']] # parent is Default
+
+ # Cell Properties
+ tagline = ['tag', 'style:table-cell-properties']
+ tagline_additional = []
+ for name, value in style_data:
+ if name == 'background':
+ tagline.append(['element', 'fo:background-color', value])
+ elif name == 'backgroundimage':
+ tagline.append(['element', 'fo:background-color', 'transparent'])
+ # Additional tags added later
+ bgimagetag = ['tagline', 'style:background-image']
+ bgimagetag.append(['element', 'xlink:href', value])
+ bgimagetag.append(['element', 'xlink:type', 'simple'])
+ bgimagetag.append(['element', 'xlink:actuate', 'onLoad'])
+ tagline_additional.append(bgimagetag)
+ elif name == 'valign':
+ if value in ['top', 'bottom', 'middle']:
+ tagline.append(['element', 'style:vertical-align', value])
+ elif name == 'halign':
+ tagline.append(['element', 'style:text-align-source', 'fix'])
+ if value in ['filled']:
+ tagline.append(['element', 'style:repeat-content', 'true'])
+ else:
+ tagline.append(['element', 'style:repeat-content', 'false'])
+ elif name == 'wrap-option':
+ tagline.append(['element', 'fo:wrap-option', value])
+ elif re.match('border', name):
+ tagline.append(['element', 'fo:%s' % name, value])
+ elif re.match('padding', name):
+ tagline.append(['element', 'fo:%s' % name, value])
+
+ # Add any additional internal tags
+ while tagline_additional:
+ tagadd = tagline_additional.pop(0)
+ tagline.append(tagadd)
+
+ style_list.append(tagline)
+
+ # Paragraph Properties
+ tagline = ['tagline', 'style:paragraph-properties']
+ tagline_valid = False
+ for name, value in style_data:
+ if name == 'halign':
+ tagline_valid = True
+ if value in ['center']:
+ tagline.append(['element', 'fo:text-align', 'center'])
+ if value in ['end', 'right']:
+ tagline.append(['element', 'fo:text-align', 'end'])
+ if value in ['start', 'filled', 'left']:
+ tagline.append(['element', 'fo:text-align', 'start'])
+ if value in ['justify']:
+ tagline.append(['element', 'fo:text-align', 'justify'])
+ # Conditionally add the tagline
+ if tagline_valid: style_list.append(tagline)
+
+ # Text Properties
+ tagline = ['tagline', 'style:text-properties']
+ for name, value in style_data:
+ if name == 'bold':
+ tagline.append(['element', 'fo:font-weight', 'bold'])
+ elif name == 'italic':
+ tagline.append(['element', 'fo:font-style', 'italic'])
+ elif name == 'underline':
+ tagline.append(['element', 'style:text-underline-style', 'solid'])
+ tagline.append(['element', 'style:text-underline-width', 'auto'])
+ tagline.append(['element', 'style:text-underline-color', 'font-color'])
+ elif name == 'color':
+ tagline.append(['element', 'fo:color', value])
+ elif name == 'fontsize':
+ tagline.append(['element', 'fo:font-size', '%spt' % value])
+ if name == 'hyphenate':
+ tagline.append(['element', 'fo:hyphenate', value])
+ style_list.append(tagline)
+
+ automatic_styles.append(style_list)
+
+ # Attach ta1 style
+ automatic_styles.append(['tag', 'style:style',
+ ['element', 'style:name', 'ta1'],
+ ['element', 'style:family', 'table'],
+ ['element', 'style:master-page-name', 'Default'],
+ ['tagline', 'style:table-properties',
+ ['element', 'table:display', 'true'],
+ ['element', 'style:writing-mode', 'lr-tb']]])
+
+ return automatic_styles
+
+
+class CalcSheet(object):
+ "Calc Sheet Class - Used to keep track of the data for an individual sheet."
+
+ def __init__(self, sheetname):
+ "Initialize a sheet"
+ self.sheet_name = sheetname
+ self.sheet_values = {}
+ self.sheet_config = {}
+ self.max_col = 0
+ self.max_row = 0
+
+ def get_sheet_dimensions(self):
+ "Returns the max column and row"
+ return (self.max_col, self.max_row)
+
+ def clean_formula(self, data):
+ "Returns a formula for use in ODF"
+ # Example Translations
+ # '=SUM(A1:A2)'
+ # datavalue = 'oooc:=SUM([.A1:.A2])'
+ # '=IF((A5>A4);A4;"")'
+ # datavalue = 'oooc:=IF(([.A5]&gt;[.A4]);[.A4];&quot;&quot;)'
+ data = clean_string(data)
+ redata = re.search('^=([A-Z]+)(\(.*)$', data)
+ if redata:
+ # funct is the function name. The rest if the string will be the funct_args
+ funct = redata.group(1)
+ funct_args = re.sub('([A-Z]+\d+)', '[.\\1]', redata.group(2))
+ data = 'oooc:=%s%s' % (funct, funct_args)
+ return data
+
+ def get_name(self):
+ "Returns the sheet name"
+ return self.sheet_name
+
+ def set_name(self, sheetname):
+ "Resets the sheet name"
+ self.sheet_name = sheetname
+
+ def get_sheet_values(self):
+ "Returns the sheet cell values"
+ return self.sheet_values
+
+ def get_sheet_value(self, col, row):
+ "Get the value contents of a cell"
+ cell = (col, row)
+ if cell in self.sheet_values:
+ return self.sheet_values[cell]
+ else:
+ return None
+
+ def get_sheet_config(self):
+ "Returns the sheet cell properties"
+ return self.sheet_config
+
+ def set_sheet_config(self, location, style_code):
+ "Sets Style Code for a given location"
+ self.sheet_config[location] = style_code
+
+ def set_sheet_value(self, cell, datatype, datavalue, formula_value='0'):
+ """Sets the value for a specific cell
+
+ cell must be in the format (col, row) where row and col are int.
+ Example: B5 would be written as (2, 5)
+ datatype must be one of 'string', 'float', 'formula'
+ datavalue should be a string
+ """
+ # Catch invalid data
+ if not isinstance(cell, tuple) or len(cell) != 2:
+ print("Invalid Cell")
+ return
+ (col, row) = cell
+ if not isinstance(col, int):
+ print("Invalid Cell")
+ return
+ if not isinstance(row, int):
+ print("Invalid Cell")
+ return
+ # Fix String Data
+ if datatype in ['string', 'annotation']:
+ datavalue = clean_string(datavalue)
+ # Fix Link Data. Link's value is a tuple containing (url, description)
+ if datatype == 'link':
+ url = clean_string(datavalue[0])
+ desc = clean_string(datavalue[1])
+ datavalue = (url, desc)
+ # Fix Formula Data
+ if datatype == 'formula':
+ datavalue = self.clean_formula(datavalue)
+ # Adjust maximum sizes
+ if col > self.max_col: self.max_col = col
+ if row > self.max_row: self.max_row = row
+ if datatype not in ('string', 'float', 'formula', 'annotation', 'link'):
+ # Set all unknown cell types to string
+ datatype = 'string'
+ datavalue = u"{}".format(datavalue)
+
+ # The following lines are taken directly from HPS
+ # self.sheet_values[cell] = (datatype, datavalue)
+ # HPS: Cell content is now a list of tuples instead of a tuple
+ # While storing here, store the cell contents first and the annotation next. While generating the XML reverse this
+ contents = self.sheet_values.get(cell, {'annotation': None, 'links': None, 'value': None})
+ if datatype == 'annotation':
+ contents['annotation'] = (datatype, datavalue)
+ elif datatype == 'link':
+ if contents['links']:
+ contents['links'][1].append(datavalue)
+ else:
+ contents['links'] = (datatype, [datavalue])
+ else:
+ contents['value'] = (datatype, datavalue)
+ if datatype == 'formula':
+ contents['formula_value'] = formula_value
+
+ self.sheet_values[cell] = contents
+
+ def get_lists(self):
+ "Returns nested lists for XML processing"
+ if self.max_col == 0 and self.max_row == 0:
+ sheet_lists = ['tag', 'table:table',
+ ['element', 'table:name', self.sheet_name], # Set the Sheet Name
+ ['element', 'table:style-name', 'ta1'],
+ ['element', 'table:print', 'false'],
+ ['tagline', 'table:table-column',
+ ['element', 'table:style-name', 'co1'],
+ ['element', 'table:default-cell-style-name', 'Default']],
+ ['tag', 'table:table-row',
+ ['element', 'table:style-name', 'ro1'],
+ ['tagline', 'table:table-cell']]]
+ else:
+ # Base Information
+ sheet_lists = ['tag', 'table:table',
+ ['element', 'table:name', self.sheet_name], # Set the sheet name
+ ['element', 'table:style-name', 'ta1'],
+ ['element', 'table:print', 'false']]
+
+# ['tagline', 'table:table-column',
+# ['element', 'table:style-name', 'co1'],
+# ['element', 'table:number-columns-repeated', self.max_col], # max_col? '2'
+# ['element', 'table:default-cell-style-name', 'Default']],
+
+ # Need to add column information
+ for col in range(1, self.max_col + 1):
+ location = ('col', col)
+ style_code = 'co1'
+ if location in self.sheet_config:
+ style_code = self.sheet_config[location]
+ sheet_lists.append(['tagline', 'table:table-column',
+ ['element', 'table:style-name', style_code],
+ ['element', 'table:default-cell-style-name', 'Default']])
+
+ # Need to create each row
+ for row in range(1, self.max_row + 1):
+ location = ('row', row)
+ style_code = 'ro1'
+ if location in self.sheet_config:
+ style_code = self.sheet_config[location]
+ rowlist = ['tag', 'table:table-row',
+ ['element', 'table:style-name', style_code]]
+ for col in range(1, self.max_col + 1):
+ cell = (col, row)
+ style_code = 'ce1' # Default all cells to ce1
+ if cell in self.sheet_config:
+ style_code = self.sheet_config[cell] # Lookup cell if available
+ if cell in self.sheet_values:
+ # (datatype, datavalue) = self.sheet_values[cell] # Marked for removal
+ collist = ['tag', 'table:table-cell']
+ if style_code != 'ce1':
+ collist.append(['element', 'table:style-name', style_code])
+
+ # Contents, annotations, and links added by HPS
+ contents = self.sheet_values[cell] # cell contents is a dictionary
+ if contents['value']:
+ (datatype, datavalue) = contents['value']
+ if datatype == 'float':
+ collist.append(['element', 'office:value-type', datatype])
+ collist.append(['element', 'office:value', datavalue])
+
+ if datatype == 'string':
+ collist.append(['element', 'office:value-type', datatype])
+ if datatype == 'formula':
+ collist.append(['element', 'table:formula', datavalue])
+ collist.append(['element', 'office:value-type', 'float'])
+ collist.append(['element', 'office:value', contents['formula_value']])
+ datavalue = contents['formula_value']
+ else:
+ datavalue = None
+
+ if contents['annotation']:
+ (annotype, annoval) = contents['annotation']
+ collist.append(['tag', 'office:annotation',
+ ['tag', 'text:p', ['data', annoval]]])
+
+ if contents['links']:
+ (linktype, linkvals) = contents['links']
+ # TODO: parse all urls, not only linkvals[0].
+ if datavalue:
+ collist.append(['tag', 'text:p', ['data', datavalue],
+ ['tag', 'text:a', ['element', 'xlink:href', linkvals[0][0]],
+ ['data', linkvals[0][1]]]])
+ else: # no value; just fill the link
+ collist.append(['tag', 'text:p',
+ ['tag', 'text:a', ['element', 'xlink:href', linkvals[0][0]],
+ ['data', linkvals[0][1]]]])
+ else:
+ if datavalue:
+ collist.append(['tag', 'text:p', ['data', datavalue]])
+ else:
+ collist = ['tagline', 'table:table-cell']
+ rowlist.append(collist)
+ sheet_lists.append(rowlist)
+ return sheet_lists
+
+
+class Calc(object):
+ "Calc Class - Used to create OpenDocument Format Calc Spreadsheets."
+
+ def __init__(self, sheetname=None, opendoc=None, debug=False):
+ "Initialize ooolib Calc instance"
+ # Default to no debugging
+ self.debug = debug
+ if not sheetname: sheetname = "Sheet1"
+ self.sheets = [CalcSheet(sheetname)] # The main sheet will be initially called 'Sheet1'
+ self.sheet_index = 0 # We initially start on the first sheet
+ self.styles = CalcStyles()
+ self.meta = Meta('ods')
+ self.styles.get_style_code('column') # Force generation of default column
+ self.styles.get_style_code('row') # Force generation of default row
+ self.styles.get_style_code('table') # Force generation of default table
+ self.styles.get_style_code('cell') # Force generation of default cell
+ self.manifest_files = [] # List of extra files included
+ self.manifest_index = 1 # Index of added manifest files
+
+ # Data Parsing
+ self.parser_element_list = []
+ self.parser_element = ""
+ self.parser_element_attrs = {}
+ self.parser_sheet_num = 0
+ self.parser_sheet_row = 0
+ self.parser_sheet_column = 0
+ self.parser_cell_repeats = 0
+ self.parser_cell_string_pending = False
+ self.parser_cell_string_line = ""
+
+ # See if we need to read a document
+ if opendoc:
+ # Verify that the document exists
+ if self.debug: print("Opening Document: %s" % opendoc)
+
+ # Okay, now we load the file
+ self.load(opendoc)
+
+ def debug_level(self, level):
+ """Set debug level:
+ True if you want debugging messages
+ False if you do not.
+ """
+ self.debug = level
+
+ def file_mimetype(self, filename):
+ "Determine the filetype from the filename"
+ parts = filename.lower().split('.')
+ ext = parts[-1]
+ if ext == 'png':
+ return ext, "image/png"
+ if ext == 'gif':
+ return ext, "image/gif"
+ return ext, "image/unknown"
+
+ def add_file(self, filename):
+ """Prepare a file for loading into ooolib
+
+ The filename should be the local filesystem name for
+ the file. The file is then prepared to be included in
+ the creation of the final document. The file needs to
+ remain in place so that it is available when the actual
+ document creation happens.
+ """
+ # mimetype set to (ext, filetype)
+ mimetype = self.file_mimetype(filename)
+ newname = "Pictures/%08d.%s" % (self.manifest_index, mimetype[0])
+ self.manifest_index += 1
+ filetype = mimetype[1]
+ self.manifest_files.append((filename, filetype, newname))
+ return newname
+
+ def set_meta(self, metaname, value):
+ "Set meta data in your document."
+ self.meta.set_meta(metaname, value)
+
+ def get_meta_value(self, metaname):
+ "Get meta data value for a given metaname"
+ return self.meta.get_meta_value(metaname)
+
+ def get_sheet_name(self):
+ "Returns the sheet name"
+ return self.sheets[self.sheet_index].get_name()
+
+ def get_sheet_dimensions(self):
+ "Returns the sheet dimensions in (cols, rows)"
+ return self.sheets[self.sheet_index].get_sheet_dimensions()
+
+ def set_column_property(self, column, name, value):
+ "Set Column Properties"
+ if name == 'width':
+ # column number column needs column-width set to value
+ self.styles.set_property('column', 'style:column-width', value)
+ style_code = self.styles.get_style_code('column')
+ self.sheets[self.sheet_index].set_sheet_config(('col', column), style_code)
+
+ def set_row_property(self, row, name, value):
+ "Set row Properties"
+ if name == 'height':
+ # row number row needs row-height set to value
+ self.styles.set_property('row', 'style:row-height', value)
+ style_code = self.styles.get_style_code('row')
+ self.sheets[self.sheet_index].set_sheet_config(('row', row), style_code)
+
+ def set_cell_property(self, name, value):
+ """Turn and off cell properties
+
+ Actual application of properties is handled by setting a value."""
+ # background images need to be handled a little differently
+ # because they need to also be inserted into the final document
+ if name == 'backgroundimage':
+ # Add file and modify value
+ value = self.add_file(value)
+ self.styles.set_property('cell', name, value)
+
+ def get_sheet_index(self):
+ "Return the current sheet index number"
+ return self.sheet_index
+
+ def set_sheet_index(self, index):
+ "Set the sheet index"
+ if isinstance(index, int):
+ if index >= 0 and index < len(self.sheets):
+ self.sheet_index = index
+ return self.sheet_index
+
+ def get_sheet_count(self):
+ "Returns the number of existing sheets"
+ return len(self.sheets)
+
+ def new_sheet(self, sheetname):
+ "Create a new sheet"
+ self.sheet_index = len(self.sheets)
+ self.sheets.append(CalcSheet(sheetname))
+ return self.sheet_index
+
+ def set_cell_value(self, col, row, datatype, value, formula_value='0'):
+ "Set the value for a given cell"
+ self.sheets[self.sheet_index].set_sheet_value((col, row), datatype, value, formula_value)
+ style_code = self.styles.get_style_code('cell')
+ self.sheets[self.sheet_index].set_sheet_config((col, row), style_code)
+
+ def get_cell_content(self, col, row):
+ "Get a cell content for a given cell. Content is a dict with keys annotation, value, link."
+ return self.sheets[self.sheet_index].get_sheet_value(col, row)
+
+ def get_cell_annotation(self, col, row):
+ "Get a cell annotation."
+ sheetvalue = self.get_cell_content(col, row)
+ return sheetvalue.get("annotation") if isinstance(sheetvalue, dict) else sheetvalue
+
+ def get_cell_links(self, col, row):
+ "Get a cell links. The links is list of tuples [(url, label), ...]."
+ sheetvalue = self.get_cell_content(col, row)
+ if isinstance(sheetvalue, dict):
+ link = sheetvalue.get("links")
+ return None if link is None else link[1]
+ return sheetvalue
+
+ def get_cell_value(self, col, row):
+ "Get a cell value tuple (type, value) for a given cell"
+ sheetvalue = self.get_cell_content(col, row)
+ return sheetvalue.get("value") if isinstance(sheetvalue, dict) else sheetvalue
+
+ def load(self, filename):
+ """Load .ods spreadsheet.
+
+ The load function loads data from a document into the current cells.
+ """
+ # Read in the important files
+
+ # meta.xml
+ data = self._zip_read(filename, "meta.xml")
+ self.meta.meta_parse(data)
+
+ # content.xml
+ data = self._zip_read(filename, "content.xml")
+ self.content_parse(data)
+
+ # settings.xml - I do not remember putting anything here
+ # styles.xml - I do not remember putting anything here
+
+ def parse_content_start_element(self, name, attrs):
+ if self.debug: print('* Start element: {} {}'.format(name, attrs))
+ self.parser_element_list.append(name)
+ self.parser_element = self.parser_element_list[-1]
+ self.parser_element_attrs = attrs
+
+ # Keep track of the current sheet number
+ if self.parser_element == 'table:table':
+ # Move to starting cell
+ self.parser_sheet_row = 0
+ self.parser_sheet_column = 0
+ # Increment the sheet number count
+ self.parser_sheet_num += 1
+ if self.parser_sheet_num - 1 != self.sheet_index:
+ # We are not on the first sheet and need to create a new sheet.
+ # We will automatically move to the new sheet
+ sheetname = "Sheet%d" % self.parser_sheet_num
+ if 'table:name' in attrs: sheetname = attrs['table:name']
+ self.new_sheet(sheetname)
+ else:
+ # We are on the first sheet and will need to overwrite the default name
+ sheetname = "Sheet%d" % self.parser_sheet_num
+ if 'table:name' in attrs: sheetname = attrs['table:name']
+ self.sheets[self.sheet_index].set_name(sheetname)
+
+ # Update the row numbers
+ if self.parser_element == 'table:table-row':
+ self.parser_sheet_row += 1
+ self.parser_sheet_column = 0
+
+ # Okay, now keep track of the sheet cell data
+ if self.parser_element == 'table:table-cell':
+ # By default it will repeat zero times
+ self.parser_cell_repeats = 0
+ # We must be in a new column
+ self.parser_sheet_column += 1
+ # Set some default values
+ datatype = ""
+ value = ""
+ # Get values from attrs hash
+ if 'office:value-type' in attrs: datatype = attrs['office:value-type']
+ if 'office:value' in attrs: value = attrs['office:value']
+ if 'table:formula' in attrs:
+ datatype = 'formula'
+ value = attrs['table:formula']
+ if datatype == 'string':
+ datatype = ""
+ self.parser_cell_string_pending = True
+ self.parser_cell_string_line = ""
+ if 'table:number-columns-repeated' in attrs:
+ self.parser_cell_repeats = int(attrs['table:number-columns-repeated']) - 1
+ # Set the cell value
+ if datatype:
+ # I should do this once per cell repeat above 0
+ for i in range(0, self.parser_cell_repeats + 1):
+ self.set_cell_value(self.parser_sheet_column + i, self.parser_sheet_row, datatype, value)
+
+ # There are lots of interesting cases with table:table-cell data. One problem is
+ # reading the number of embedded spaces correctly. This code should help us get
+ # the number of spaces out.
+
+ if self.parser_element == 'text:s':
+ # This means we have a number of spaces
+ count_num = 0
+ if 'text:c' in attrs:
+ count_alpha = attrs['text:c']
+ if count_alpha.isdigit():
+ count_num = int(count_alpha)
+ # I am not sure what to do if we do not have a string pending
+ if self.parser_cell_string_pending:
+ # Append the currect number of spaces to the end
+ self.parser_cell_string_line = "%s%s" % (self.parser_cell_string_line, ' ' * count_num)
+
+ if self.parser_element == 'text:tab-stop':
+ if self.parser_cell_string_pending:
+ self.parser_cell_string_line = "%s\t" % (self.parser_cell_string_line)
+
+ if self.parser_element == 'text:line-break':
+ if self.parser_cell_string_pending:
+ self.parser_cell_string_line = "%s\n" % (self.parser_cell_string_line)
+
+ # Debugging statements
+ if self.debug: print(" List: {}".format(self.parser_element_list))
+ if self.debug: print(" Attributes: {}".format(attrs))
+
+ def parse_content_end_element(self, name):
+ if self.debug: print('* End element: {}'.format(name))
+ if name != self.parser_element:
+ print("Tag Mismatch: '%s' != '%s'" % (name, self.parser_element))
+ self.parser_element_list.pop()
+
+ # If the element was text:p and we are in string mode
+ if self.parser_element == 'text:p':
+ if self.parser_cell_string_pending:
+ self.parser_cell_string_pending = False
+
+ # Take care of repeated cells
+ if self.parser_element == 'table:table-cell':
+ self.parser_sheet_column += self.parser_cell_repeats
+
+ # Readjust parser_element_list and parser_element
+ if self.parser_element_list:
+ self.parser_element = self.parser_element_list[-1]
+ else:
+ self.parser_element = ""
+
+ def parse_content_char_data(self, data):
+ if self.debug: print(" Character data: {!r}".format(data))
+
+ if self.parser_element in ('text:p', 'text:span', 'text:a'):
+ if self.parser_cell_string_pending:
+ # Set the string and leave string pending mode
+ # This does feel a little kludgy, but it does the job
+ text = self.parser_element_attrs['xlink:href'] if self.parser_element == 'text:a' else data
+ self.parser_cell_string_line = "%s%s" % (self.parser_cell_string_line, text)
+
+ # I should do this once per cell repeat above 0
+ for i in range(0, self.parser_cell_repeats + 1):
+ self.set_cell_value(
+ self.parser_sheet_column + i,
+ self.parser_sheet_row,
+ 'string',
+ self.parser_cell_string_line
+ )
+ if self.parser_element == 'text:a':
+ self.set_cell_value(
+ self.parser_sheet_column + i,
+ self.parser_sheet_row,
+ 'link',
+ (self.parser_element_attrs['xlink:href'], data)
+ )
+
+ def content_parse(self, data):
+ "Parse Content Data from a content.xml file"
+
+ # Debugging statements
+ if self.debug:
+ # Sometimes it helps to see the document that was read from
+ print(data)
+ print("\n\n\n")
+
+ # Create parser
+ parser = xml.parsers.expat.ParserCreate()
+ # Set up parser callback functions
+ parser.StartElementHandler = self.parse_content_start_element
+ parser.EndElementHandler = self.parse_content_end_element
+ parser.CharacterDataHandler = self.parse_content_char_data
+
+ # Actually parse the data
+ parser.Parse(data, 1)
+
+ def save(self, filename):
+ """Save .ods spreadsheet.
+
+ The save function saves the current cells and settings into a document.
+ """
+ if self.debug: print("Writing %s" % filename)
+ self.savefile = zipfile.ZipFile(filename, "w")
+ if self.debug: print(" meta.xml")
+ self._zip_insert(self.savefile, "meta.xml", self.meta.get_meta())
+ if self.debug: print(" mimetype")
+ self._zip_insert(self.savefile, "mimetype", "application/vnd.oasis.opendocument.spreadsheet")
+ if self.debug: print(" Configurations2/accelerator/current.xml")
+ self._zip_insert(self.savefile, "Configurations2/accelerator/current.xml", "")
+ if self.debug: print(" META-INF/manifest.xml")
+ self._zip_insert(self.savefile, "META-INF/manifest.xml", self._ods_manifest())
+ if self.debug: print(" content.xml")
+ self._zip_insert(self.savefile, "content.xml", self._ods_content())
+ if self.debug: print(" settings.xml")
+ self._zip_insert(self.savefile, "settings.xml", self._ods_settings())
+ if self.debug: print(" styles.xml")
+ self._zip_insert(self.savefile, "styles.xml", self._ods_styles())
+
+ # Add additional files if needed
+ for fileset in self.manifest_files:
+ (filename, filetype, newname) = fileset
+ # Read in the file
+ data = self._file_load(filename)
+ if self.debug: print(" Inserting '%s' as '%s'" % (filename, newname))
+ self._zip_insert_binary(self.savefile, newname, data)
+ self.savefile.close()
+
+ def _file_load(self, filename):
+ "Load a file"
+ return open(filename, "rb").read()
+
+ def _zip_insert_binary(self, fileobj, filename, data):
+ "Insert a binary file into the zip archive"
+ now = time.localtime(time.time())[:6]
+ info = zipfile.ZipInfo(filename)
+ info.date_time = now
+ info.compress_type = zipfile.ZIP_DEFLATED
+ fileobj.writestr(info, data)
+
+ def _zip_insert(self, fileobj, filename, data):
+ "Insert a file into the zip archive"
+
+ # zip seems to struggle with non-ascii characters
+ data = data.encode('utf-8')
+
+ now = time.localtime(time.time())[:6]
+ info = zipfile.ZipInfo(filename)
+ info.date_time = now
+ info.compress_type = zipfile.ZIP_DEFLATED
+ fileobj.writestr(info, data)
+
+ def _zip_read(self, fileobj, filename):
+ "Get the data from a file in the zip archive by filename"
+ zipdoc = zipfile.ZipFile(fileobj, "r")
+ data = zipdoc.read(filename)
+ # Need to close the file
+ zipdoc.close()
+ return data
+
+ def _ods_content(self):
+ "Generate ods content.xml data"
+
+ # This will list all of the sheets in the document
+ self.sheetdata = ['tag', 'office:spreadsheet']
+ for sheet in self.sheets:
+ if self.debug:
+ sheet_name = sheet.get_name()
+ print(" Creating Sheet '%s'" % sheet_name)
+ sheet_list = sheet.get_lists()
+ self.sheetdata.append(sheet_list)
+ # Automatic Styles
+ self.automatic_styles = self.styles.get_automatic_styles()
+
+ self.data = ['tag', 'office:document-content',
+ ['element', 'xmlns:office', 'urn:oasis:names:tc:opendocument:xmlns:office:1.0'],
+ ['element', 'xmlns:style', 'urn:oasis:names:tc:opendocument:xmlns:style:1.0'],
+ ['element', 'xmlns:text', 'urn:oasis:names:tc:opendocument:xmlns:text:1.0'],
+ ['element', 'xmlns:table', 'urn:oasis:names:tc:opendocument:xmlns:table:1.0'],
+ ['element', 'xmlns:draw', 'urn:oasis:names:tc:opendocument:xmlns:drawing:1.0'],
+ ['element', 'xmlns:fo', 'urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0'],
+ ['element', 'xmlns:xlink', 'http://www.w3.org/1999/xlink'],
+ ['element', 'xmlns:dc', 'http://purl.org/dc/elements/1.1/'],
+ ['element', 'xmlns:meta', 'urn:oasis:names:tc:opendocument:xmlns:meta:1.0'],
+ ['element', 'xmlns:number', 'urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0'],
+ ['element', 'xmlns:svg', 'urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0'],
+ ['element', 'xmlns:chart', 'urn:oasis:names:tc:opendocument:xmlns:chart:1.0'],
+ ['element', 'xmlns:dr3d', 'urn:oasis:names:tc:opendocument:xmlns:dr3d:1.0'],
+ ['element', 'xmlns:math', 'http://www.w3.org/1998/Math/MathML'],
+ ['element', 'xmlns:form', 'urn:oasis:names:tc:opendocument:xmlns:form:1.0'],
+ ['element', 'xmlns:script', 'urn:oasis:names:tc:opendocument:xmlns:script:1.0'],
+ ['element', 'xmlns:ooo', 'http://openoffice.org/2004/office'],
+ ['element', 'xmlns:ooow', 'http://openoffice.org/2004/writer'],
+ ['element', 'xmlns:oooc', 'http://openoffice.org/2004/calc'],
+ ['element', 'xmlns:dom', 'http://www.w3.org/2001/xml-events'],
+ ['element', 'xmlns:xforms', 'http://www.w3.org/2002/xforms'],
+ ['element', 'xmlns:xsd', 'http://www.w3.org/2001/XMLSchema'],
+ ['element', 'xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance'],
+ ['element', 'office:version', '1.0'],
+ ['tagline', 'office:scripts'],
+ ['tag', 'office:font-face-decls',
+ ['tagline', 'style:font-face',
+ ['element', 'style:name', 'DejaVu Sans'],
+ ['element', 'svg:font-family', '&apos;DejaVu Sans&apos;'],
+ ['element', 'style:font-pitch', 'variable']],
+ ['tagline', 'style:font-face',
+ ['element', 'style:name', 'Nimbus Sans L'],
+ ['element', 'svg:font-family', '&apos;Nimbus Sans L&apos;'],
+ ['element', 'style:font-family-generic', 'swiss'],
+ ['element', 'style:font-pitch', 'variable']]],
+
+ # Automatic Styles
+ self.automatic_styles,
+
+ ['tag', 'office:body',
+ self.sheetdata]] # Sheets are generated from the CalcSheet class
+
+ # Generate content.xml XML data
+ xmldoc = XML()
+ self.lines = xmldoc.convert(self.data)
+ self.filedata = '\n'.join(self.lines)
+ # Return generated data
+ return self.filedata
+
+ def _ods_manifest(self):
+ "Generate ods manifest.xml data"
+ self.data = ['tag', 'manifest:manifest',
+ ['element', 'xmlns:manifest', 'urn:oasis:names:tc:opendocument:xmlns:manifest:1.0'],
+ ['tagline', 'manifest:file-entry',
+ ['element', 'manifest:media-type', 'application/vnd.oasis.opendocument.spreadsheet'],
+ ['element', 'manifest:full-path', '/']],
+ ['tagline', 'manifest:file-entry',
+ ['element', 'manifest:media-type', 'application/vnd.sun.xml.ui.configuration'],
+ ['element', 'manifest:full-path', 'Configurations2/']],
+ ['tagline', 'manifest:file-entry',
+ ['element', 'manifest:media-type', ''],
+ ['element', 'manifest:full-path', 'Configurations2/accelerator/']],
+ ['tagline', 'manifest:file-entry',
+ ['element', 'manifest:media-type', ''],
+ ['element', 'manifest:full-path', 'Configurations2/accelerator/current.xml']],
+ ['tagline', 'manifest:file-entry',
+ ['element', 'manifest:media-type', 'text/xml'],
+ ['element', 'manifest:full-path', 'content.xml']],
+ ['tagline', 'manifest:file-entry',
+ ['element', 'manifest:media-type', 'text/xml'],
+ ['element', 'manifest:full-path', 'styles.xml']],
+ ['tagline', 'manifest:file-entry',
+ ['element', 'manifest:media-type', 'text/xml'],
+ ['element', 'manifest:full-path', 'meta.xml']],
+ ['tagline', 'manifest:file-entry',
+ ['element', 'manifest:media-type', 'text/xml'],
+ ['element', 'manifest:full-path', 'settings.xml']]]
+
+ # Add additional files to manifest list
+ for fileset in self.manifest_files:
+ (filename, filetype, newname) = fileset
+ addfile = ['tagline', 'manifest:file-entry',
+ ['element', 'manifest:media-type', filetype],
+ ['element', 'manifest:full-path', newname]]
+ self.data.append(addfile)
+
+ # Generate content.xml XML data
+ xmldoc = XML()
+ self.lines = xmldoc.convert(self.data)
+ self.filedata = '\n'.join(self.lines)
+ # Return generated data
+ return self.filedata
+
+ def _ods_settings(self):
+ "Generate ods settings.xml data"
+ self.data = ['tag', 'office:document-settings',
+ ['element', 'xmlns:office', 'urn:oasis:names:tc:opendocument:xmlns:office:1.0'],
+ ['element', 'xmlns:xlink', 'http://www.w3.org/1999/xlink'],
+ ['element', 'xmlns:config', 'urn:oasis:names:tc:opendocument:xmlns:config:1.0'],
+ ['element', 'xmlns:ooo', 'http://openoffice.org/2004/office'],
+ ['element', 'office:version', '1.0'],
+ ['tag', 'office:settings',
+ ['tag', 'config:config-item-set',
+ ['element', 'config:name', 'ooo:view-settings'],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'VisibleAreaTop'],
+ ['element', 'config:type', 'int'],
+ ['data', '0']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'VisibleAreaLeft'],
+ ['element', 'config:type', 'int'],
+ ['data', '0']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'VisibleAreaWidth'],
+ ['element', 'config:type', 'int'],
+ ['data', '6774']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'VisibleAreaHeight'],
+ ['element', 'config:type', 'int'],
+ ['data', '2389']],
+ ['tag', 'config:config-item-map-indexed',
+ ['element', 'config:name', 'Views'],
+ ['tag', 'config:config-item-map-entry',
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'ViewId'],
+ ['element', 'config:type', 'string'],
+ ['data', 'View1']],
+ ['tag', 'config:config-item-map-named',
+ ['element', 'config:name', 'Tables'],
+ ['tag', 'config:config-item-map-entry',
+ ['element', 'config:name', 'Sheet1'],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'CursorPositionX'], # Cursor Position A
+ ['element', 'config:type', 'int'],
+ ['data', '0']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'CursorPositionY'], # Cursor Position 1
+ ['element', 'config:type', 'int'],
+ ['data', '0']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'HorizontalSplitMode'],
+ ['element', 'config:type', 'short'],
+ ['data', '0']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'VerticalSplitMode'],
+ ['element', 'config:type', 'short'],
+ ['data', '0']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'HorizontalSplitPosition'],
+ ['element', 'config:type', 'int'],
+ ['data', '0']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'VerticalSplitPosition'],
+ ['element', 'config:type', 'int'],
+ ['data', '0']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'ActiveSplitRange'],
+ ['element', 'config:type', 'short'],
+ ['data', '2']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'PositionLeft'],
+ ['element', 'config:type', 'int'],
+ ['data', '0']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'PositionRight'],
+ ['element', 'config:type', 'int'],
+ ['data', '0']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'PositionTop'],
+ ['element', 'config:type', 'int'],
+ ['data', '0']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'PositionBottom'],
+ ['element', 'config:type', 'int'],
+ ['data', '0']]]],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'ActiveTable'],
+ ['element', 'config:type', 'string'],
+ ['data', 'Sheet1']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'HorizontalScrollbarWidth'],
+ ['element', 'config:type', 'int'],
+ ['data', '270']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'ZoomType'],
+ ['element', 'config:type', 'short'],
+ ['data', '0']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'ZoomValue'],
+ ['element', 'config:type', 'int'],
+ ['data', '100']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'PageViewZoomValue'],
+ ['element', 'config:type', 'int'],
+ ['data', '60']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'ShowPageBreakPreview'],
+ ['element', 'config:type', 'boolean'],
+ ['data', 'false']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'ShowZeroValues'],
+ ['element', 'config:type', 'boolean'],
+ ['data', 'true']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'ShowNotes'],
+ ['element', 'config:type', 'boolean'],
+ ['data', 'true']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'ShowGrid'],
+ ['element', 'config:type', 'boolean'],
+ ['data', 'true']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'GridColor'],
+ ['element', 'config:type', 'long'],
+ ['data', '12632256']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'ShowPageBreaks'],
+ ['element', 'config:type', 'boolean'],
+ ['data', 'true']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'HasColumnRowHeaders'],
+ ['element', 'config:type', 'boolean'],
+ ['data', 'true']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'HasSheetTabs'],
+ ['element', 'config:type', 'boolean'],
+ ['data', 'true']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'IsOutlineSymbolsSet'],
+ ['element', 'config:type', 'boolean'],
+ ['data', 'true']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'IsSnapToRaster'],
+ ['element', 'config:type', 'boolean'],
+ ['data', 'false']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'RasterIsVisible'],
+ ['element', 'config:type', 'boolean'],
+ ['data', 'false']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'RasterResolutionX'],
+ ['element', 'config:type', 'int'],
+ ['data', '1270']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'RasterResolutionY'],
+ ['element', 'config:type', 'int'],
+ ['data', '1270']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'RasterSubdivisionX'],
+ ['element', 'config:type', 'int'],
+ ['data', '1']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'RasterSubdivisionY'],
+ ['element', 'config:type', 'int'],
+ ['data', '1']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'IsRasterAxisSynchronized'],
+ ['element', 'config:type', 'boolean'],
+ ['data', 'true']]]]],
+ ['tag', 'config:config-item-set',
+ ['element', 'config:name', 'ooo:configuration-settings'],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'ShowZeroValues'],
+ ['element', 'config:type', 'boolean'],
+ ['data', 'true']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'ShowNotes'],
+ ['element', 'config:type', 'boolean'],
+ ['data', 'true']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'ShowGrid'],
+ ['element', 'config:type', 'boolean'],
+ ['data', 'true']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'GridColor'],
+ ['element', 'config:type', 'long'],
+ ['data', '12632256']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'ShowPageBreaks'],
+ ['element', 'config:type', 'boolean'],
+ ['data', 'true']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'LinkUpdateMode'],
+ ['element', 'config:type', 'short'],
+ ['data', '3']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'HasColumnRowHeaders'],
+ ['element', 'config:type', 'boolean'],
+ ['data', 'true']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'HasSheetTabs'],
+ ['element', 'config:type', 'boolean'],
+ ['data', 'true']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'IsOutlineSymbolsSet'],
+ ['element', 'config:type', 'boolean'],
+ ['data', 'true']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'IsSnapToRaster'],
+ ['element', 'config:type', 'boolean'],
+ ['data', 'false']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'RasterIsVisible'],
+ ['element', 'config:type', 'boolean'],
+ ['data', 'false']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'RasterResolutionX'],
+ ['element', 'config:type', 'int'],
+ ['data', '1270']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'RasterResolutionY'],
+ ['element', 'config:type', 'int'],
+ ['data', '1270']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'RasterSubdivisionX'],
+ ['element', 'config:type', 'int'],
+ ['data', '1']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'RasterSubdivisionY'],
+ ['element', 'config:type', 'int'],
+ ['data', '1']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'IsRasterAxisSynchronized'],
+ ['element', 'config:type', 'boolean'],
+ ['data', 'true']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'AutoCalculate'],
+ ['element', 'config:type', 'boolean'],
+ ['data', 'true']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'PrinterName'],
+ ['element', 'config:type', 'string'],
+ ['data', 'Generic Printer']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'PrinterSetup'],
+ ['element', 'config:type', 'base64Binary'],
+ ['data', 'YgH+/0dlbmVyaWMgUHJpbnRlcgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAU0dFTlBSVAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWAAMAqAAAAAAA//8FAFZUAAAkbQAASm9iRGF0YSAxCnByaW50ZXI9R2VuZXJpYyBQcmludGVyCm9yaWVudGF0aW9uPVBvcnRyYWl0CmNvcGllcz0xCnNjYWxlPTEwMAptYXJnaW5kYWp1c3RtZW50PTAsMCwwLDAKY29sb3JkZXB0aD0yNApwc2xldmVsPTAKY29sb3JkZXZpY2U9MApQUERDb250ZXhEYXRhClBhZ2VTaXplOkxldHRlcgAA']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'ApplyUserData'],
+ ['element', 'config:type', 'boolean'],
+ ['data', 'true']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'CharacterCompressionType'],
+ ['element', 'config:type', 'short'],
+ ['data', '0']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'IsKernAsianPunctuation'],
+ ['element', 'config:type', 'boolean'],
+ ['data', 'false']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'SaveVersionOnClose'],
+ ['element', 'config:type', 'boolean'],
+ ['data', 'false']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'UpdateFromTemplate'],
+ ['element', 'config:type', 'boolean'],
+ ['data', 'false']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'AllowPrintJobCancel'],
+ ['element', 'config:type', 'boolean'],
+ ['data', 'true']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'LoadReadonly'],
+ ['element', 'config:type', 'boolean'],
+ ['data', 'false']]]]]
+
+ # Generate content.xml XML data
+ xmldoc = XML()
+ self.lines = xmldoc.convert(self.data)
+ self.filedata = '\n'.join(self.lines)
+ # Return generated data
+ return self.filedata
+
+ def _ods_styles(self):
+ "Generate ods styles.xml data"
+ self.data = ['tag', 'office:document-styles',
+ ['element', 'xmlns:office', 'urn:oasis:names:tc:opendocument:xmlns:office:1.0'],
+ ['element', 'xmlns:style', 'urn:oasis:names:tc:opendocument:xmlns:style:1.0'],
+ ['element', 'xmlns:text', 'urn:oasis:names:tc:opendocument:xmlns:text:1.0'],
+ ['element', 'xmlns:table', 'urn:oasis:names:tc:opendocument:xmlns:table:1.0'],
+ ['element', 'xmlns:draw', 'urn:oasis:names:tc:opendocument:xmlns:drawing:1.0'],
+ ['element', 'xmlns:fo', 'urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0'],
+ ['element', 'xmlns:xlink', 'http://www.w3.org/1999/xlink'],
+ ['element', 'xmlns:dc', 'http://purl.org/dc/elements/1.1/'],
+ ['element', 'xmlns:meta', 'urn:oasis:names:tc:opendocument:xmlns:meta:1.0'],
+ ['element', 'xmlns:number', 'urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0'],
+ ['element', 'xmlns:svg', 'urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0'],
+ ['element', 'xmlns:chart', 'urn:oasis:names:tc:opendocument:xmlns:chart:1.0'],
+ ['element', 'xmlns:dr3d', 'urn:oasis:names:tc:opendocument:xmlns:dr3d:1.0'],
+ ['element', 'xmlns:math', 'http://www.w3.org/1998/Math/MathML'],
+ ['element', 'xmlns:form', 'urn:oasis:names:tc:opendocument:xmlns:form:1.0'],
+ ['element', 'xmlns:script', 'urn:oasis:names:tc:opendocument:xmlns:script:1.0'],
+ ['element', 'xmlns:ooo', 'http://openoffice.org/2004/office'],
+ ['element', 'xmlns:ooow', 'http://openoffice.org/2004/writer'],
+ ['element', 'xmlns:oooc', 'http://openoffice.org/2004/calc'],
+ ['element', 'xmlns:dom', 'http://www.w3.org/2001/xml-events'],
+ ['element', 'office:version', '1.0'],
+ ['tag', 'office:font-face-decls',
+ ['tagline', 'style:font-face',
+ ['element', 'style:name', 'DejaVu Sans'],
+ ['element', 'svg:font-family', '&apos;DejaVu Sans&apos;'],
+ ['element', 'style:font-pitch', 'variable']],
+ ['tagline', 'style:font-face',
+ ['element', 'style:name', 'Nimbus Sans L'],
+ ['element', 'svg:font-family', '&apos;Nimbus Sans L&apos;'],
+ ['element', 'style:font-family-generic', 'swiss'],
+ ['element', 'style:font-pitch', 'variable']]],
+ ['tag', 'office:styles',
+ ['tag', 'style:default-style',
+ ['element', 'style:family', 'table-cell'],
+ ['tagline', 'style:table-cell-properties',
+ ['element', 'style:decimal-places', '2']],
+ ['tagline', 'style:paragraph-properties',
+ ['element', 'style:tab-stop-distance', '0.5in']],
+ ['tagline', 'style:text-properties',
+ ['element', 'style:font-name', 'Nimbus Sans L'],
+ ['element', 'fo:language', 'en'],
+ ['element', 'fo:country', 'US'],
+ ['element', 'style:font-name-asian', 'DejaVu Sans'],
+ ['element', 'style:language-asian', 'none'],
+ ['element', 'style:country-asian', 'none'],
+ ['element', 'style:font-name-complex', 'DejaVu Sans'],
+ ['element', 'style:language-complex', 'none'],
+ ['element', 'style:country-complex', 'none']]],
+ ['tag', 'number:number-style',
+ ['element', 'style:name', 'N0'],
+ ['tagline', 'number:number',
+ ['element', 'number:min-integer-digits', '1']]],
+ ['tag', 'number:currency-style',
+ ['element', 'style:name', 'N104P0'],
+ ['element', 'style:volatile', 'true'],
+ ['tag', 'number:currency-symbol',
+ ['element', 'number:language', 'en'],
+ ['element', 'number:country', 'US'],
+ ['data', '$']],
+ ['tagline', 'number:number',
+ ['element', 'number:decimal-places', '2'],
+ ['element', 'number:min-integer-digits', '1'],
+ ['element', 'number:grouping', 'true']]],
+ ['tag', 'number:currency-style',
+ ['element', 'style:name', 'N104'],
+ ['tagline', 'style:text-properties',
+ ['element', 'fo:color', '#ff0000']],
+ ['tag', 'number:text',
+ ['data', '-']],
+ ['tag', 'number:currency-symbol',
+ ['element', 'number:language', 'en'],
+ ['element', 'number:country', 'US'],
+ ['data', '$']],
+ ['tagline', 'number:number',
+ ['element', 'number:decimal-places', '2'],
+ ['element', 'number:min-integer-digits', '1'],
+ ['element', 'number:grouping', 'true']],
+ ['tagline', 'style:map',
+ ['element', 'style:condition', 'value()&gt;=0'],
+ ['element', 'style:apply-style-name', 'N104P0']]],
+ ['tagline', 'style:style',
+ ['element', 'style:name', 'Default'],
+ ['element', 'style:family', 'table-cell']],
+ ['tag', 'style:style',
+ ['element', 'style:name', 'Result'],
+ ['element', 'style:family', 'table-cell'],
+ ['element', 'style:parent-style-name', 'Default'],
+ ['tagline', 'style:text-properties',
+ ['element', 'fo:font-style', 'italic'],
+ ['element', 'style:text-underline-style', 'solid'],
+ ['element', 'style:text-underline-width', 'auto'],
+ ['element', 'style:text-underline-color', 'font-color'],
+ ['element', 'fo:font-weight', 'bold']]],
+ ['tagline', 'style:style',
+ ['element', 'style:name', 'Result2'],
+ ['element', 'style:family', 'table-cell'],
+ ['element', 'style:parent-style-name', 'Result'],
+ ['element', 'style:data-style-name', 'N104']],
+ ['tag', 'style:style',
+ ['element', 'style:name', 'Heading'],
+ ['element', 'style:family', 'table-cell'],
+ ['element', 'style:parent-style-name', 'Default'],
+ ['tagline', 'style:table-cell-properties',
+ ['element', 'style:text-align-source', 'fix'],
+ ['element', 'style:repeat-content', 'false']],
+ ['tagline', 'style:paragraph-properties',
+ ['element', 'fo:text-align', 'center']],
+ ['tagline', 'style:text-properties',
+ ['element', 'fo:font-size', '16pt'],
+ ['element', 'fo:font-style', 'italic'],
+ ['element', 'fo:font-weight', 'bold']]],
+ ['tag', 'style:style',
+ ['element', 'style:name', 'Heading1'],
+ ['element', 'style:family', 'table-cell'],
+ ['element', 'style:parent-style-name', 'Heading'],
+ ['tagline', 'style:table-cell-properties',
+ ['element', 'style:rotation-angle', '90']]]],
+ ['tag', 'office:automatic-styles',
+ ['tag', 'style:page-layout',
+ ['element', 'style:name', 'pm1'],
+ ['tagline', 'style:page-layout-properties',
+ ['element', 'style:writing-mode', 'lr-tb']],
+ ['tag', 'style:header-style',
+ ['tagline', 'style:header-footer-properties',
+ ['element', 'fo:min-height', '0.2957in'],
+ ['element', 'fo:margin-left', '0in'],
+ ['element', 'fo:margin-right', '0in'],
+ ['element', 'fo:margin-bottom', '0.0984in']]],
+ ['tag', 'style:footer-style',
+ ['tagline', 'style:header-footer-properties',
+ ['element', 'fo:min-height', '0.2957in'],
+ ['element', 'fo:margin-left', '0in'],
+ ['element', 'fo:margin-right', '0in'],
+ ['element', 'fo:margin-top', '0.0984in']]]],
+ ['tag', 'style:page-layout',
+ ['element', 'style:name', 'pm2'],
+ ['tagline', 'style:page-layout-properties',
+ ['element', 'style:writing-mode', 'lr-tb']],
+ ['tag', 'style:header-style',
+ ['tag', 'style:header-footer-properties',
+ ['element', 'fo:min-height', '0.2957in'],
+ ['element', 'fo:margin-left', '0in'],
+ ['element', 'fo:margin-right', '0in'],
+ ['element', 'fo:margin-bottom', '0.0984in'],
+ ['element', 'fo:border', '0.0346in solid #000000'],
+ ['element', 'fo:padding', '0.0071in'],
+ ['element', 'fo:background-color', '#c0c0c0'],
+ ['tagline', 'style:background-image']]],
+ ['tag', 'style:footer-style',
+ ['tag', 'style:header-footer-properties',
+ ['element', 'fo:min-height', '0.2957in'],
+ ['element', 'fo:margin-left', '0in'],
+ ['element', 'fo:margin-right', '0in'],
+ ['element', 'fo:margin-top', '0.0984in'],
+ ['element', 'fo:border', '0.0346in solid #000000'],
+ ['element', 'fo:padding', '0.0071in'],
+ ['element', 'fo:background-color', '#c0c0c0'],
+ ['tagline', 'style:background-image']]]]],
+ ['tag', 'office:master-styles',
+ ['tag', 'style:master-page',
+ ['element', 'style:name', 'Default'],
+ ['element', 'style:page-layout-name', 'pm1'],
+ ['tag', 'style:header',
+ ['tag', 'text:p',
+ ['data', '<text:sheet-name>???</text:sheet-name>']]],
+ ['tagline', 'style:header-left',
+ ['element', 'style:display', 'false']],
+ ['tag', 'style:footer',
+ ['tag', 'text:p',
+ ['data', 'Page <text:page-number>1</text:page-number>']]],
+ ['tagline', 'style:footer-left',
+ ['element', 'style:display', 'false']]],
+ ['tag', 'style:master-page',
+ ['element', 'style:name', 'Report'],
+ ['element', 'style:page-layout-name', 'pm2'],
+ ['tag', 'style:header',
+ ['tag', 'style:region-left',
+ ['tag', 'text:p',
+ ['data', '<text:sheet-name>???</text:sheet-name> (<text:title>???</text:title>)']]],
+ ['tag', 'style:region-right',
+ ['tag', 'text:p',
+ ['data', '<text:date style:data-style-name="N2" text:date-value="2006-09-29">09/29/2006</text:date>, <text:time>13:02:56</text:time>']]]],
+ ['tagline', 'style:header-left',
+ ['element', 'style:display', 'false']],
+ ['tag', 'style:footer',
+ ['tag', 'text:p',
+ ['data', 'Page <text:page-number>1</text:page-number> / <text:page-count>99</text:page-count>']]],
+ ['tagline', 'style:footer-left',
+ ['element', 'style:display', 'false']]]]]
+
+ # Generate content.xml XML data
+ xmldoc = XML()
+ self.lines = xmldoc.convert(self.data)
+ self.filedata = '\n'.join(self.lines)
+ # Return generated data
+ return self.filedata
+
+
+class Writer(object):
+ "Writer Class - Used to create OpenDocument Format Writer Documents."
+
+ def __init__(self):
+ "Initialize ooolib Writer instance"
+ # Default to no debugging
+ self.debug = False
+ self.meta = Meta('odt')
+
+ def set_meta(self, metaname, value):
+ "Set meta data in your document."
+ self.meta.set_meta(metaname, value)
+
+ def save(self, filename):
+ """Save .odt document
+
+ The save function saves the current .odt document.
+ """
+ if self.debug: print("Writing %s" % filename)
+ self.savefile = zipfile.ZipFile(filename, "w")
+ if self.debug: print(" meta.xml")
+ self._zip_insert(self.savefile, "meta.xml", self.meta.get_meta())
+ if self.debug: print(" mimetype")
+ self._zip_insert(self.savefile, "mimetype", "application/vnd.oasis.opendocument.text")
+ if self.debug: print(" META-INF/manifest.xml")
+ self._zip_insert(self.savefile, "META-INF/manifest.xml", self._odt_manifest())
+ if self.debug: print(" content.xml")
+ self._zip_insert(self.savefile, "content.xml", self._odt_content())
+ if self.debug: print(" settings.xml")
+ # self._zip_insert(self.savefile, "settings.xml", self._odt_settings())
+ if self.debug: print(" styles.xml")
+ # self._zip_insert(self.savefile, "styles.xml", self._odt_styles())
+
+ # We need to close the file now that we are done creating it.
+ self.savefile.close()
+
+ def _zip_insert(self, fileobj, filename, data):
+ now = time.localtime(time.time())[:6]
+ info = zipfile.ZipInfo(filename)
+ info.date_time = now
+ info.compress_type = zipfile.ZIP_DEFLATED
+ fileobj.writestr(info, data)
+
+ def _odt_manifest(self):
+ "Generate odt manifest.xml data"
+
+ self.data = ['tag', 'manifest:manifest',
+ ['element', 'xmlns:manifest', 'urn:oasis:names:tc:opendocument:xmlns:manifest:1.0'],
+ ['tagline', 'manifest:file-entry',
+ ['element', 'manifest:media-type', 'application/vnd.oasis.opendocument.text'],
+ ['element', 'manifest:full-path', '/']],
+ ['tagline', 'manifest:file-entry',
+ ['element', 'manifest:media-type', 'text/xml'],
+ ['element', 'manifest:full-path', 'content.xml']],
+ ['tagline', 'manifest:file-entry',
+ ['element', 'manifest:media-type', 'text/xml'],
+ ['element', 'manifest:full-path', 'styles.xml']],
+ ['tagline', 'manifest:file-entry',
+ ['element', 'manifest:media-type', 'text/xml'],
+ ['element', 'manifest:full-path', 'meta.xml']],
+ ['tagline', 'manifest:file-entry',
+ ['element', 'manifest:media-type', 'text/xml'],
+ ['element', 'manifest:full-path', 'settings.xml']]]
+
+ # Generate content.xml XML data
+ xmldoc = XML()
+ self.lines = xmldoc.convert(self.data)
+ self.lines.insert(1, '<!DOCTYPE manifest:manifest PUBLIC "-//OpenOffice.org//DTD Manifest 1.0//EN" "Manifest.dtd">')
+ self.filedata = '\n'.join(self.lines)
+ # Return generated data
+ return self.filedata
+
+ def _odt_content(self):
+ "Generate odt content.xml data"
+
+ self.data = ['tag', 'office:document-content',
+ ['element', 'xmlns:office', 'urn:oasis:names:tc:opendocument:xmlns:office:1.0'],
+ ['element', 'xmlns:style', 'urn:oasis:names:tc:opendocument:xmlns:style:1.0'],
+ ['element', 'xmlns:text', 'urn:oasis:names:tc:opendocument:xmlns:text:1.0'],
+ ['element', 'xmlns:table', 'urn:oasis:names:tc:opendocument:xmlns:table:1.0'],
+ ['element', 'xmlns:draw', 'urn:oasis:names:tc:opendocument:xmlns:drawing:1.0'],
+ ['element', 'xmlns:fo', 'urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0'],
+ ['element', 'xmlns:xlink', 'http://www.w3.org/1999/xlink'],
+ ['element', 'xmlns:dc', 'http://purl.org/dc/elements/1.1/'],
+ ['element', 'xmlns:meta', 'urn:oasis:names:tc:opendocument:xmlns:meta:1.0'],
+ ['element', 'xmlns:number', 'urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0'],
+ ['element', 'xmlns:svg', 'urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0'],
+ ['element', 'xmlns:chart', 'urn:oasis:names:tc:opendocument:xmlns:chart:1.0'],
+ ['element', 'xmlns:dr3d', 'urn:oasis:names:tc:opendocument:xmlns:dr3d:1.0'],
+ ['element', 'xmlns:math', 'http://www.w3.org/1998/Math/MathML'],
+ ['element', 'xmlns:form', 'urn:oasis:names:tc:opendocument:xmlns:form:1.0'],
+ ['element', 'xmlns:script', 'urn:oasis:names:tc:opendocument:xmlns:script:1.0'],
+ ['element', 'xmlns:ooo', 'http://openoffice.org/2004/office'],
+ ['element', 'xmlns:ooow', 'http://openoffice.org/2004/writer'],
+ ['element', 'xmlns:oooc', 'http://openoffice.org/2004/calc'],
+ ['element', 'xmlns:dom', 'http://www.w3.org/2001/xml-events'],
+ ['element', 'xmlns:xforms', 'http://www.w3.org/2002/xforms'],
+ ['element', 'xmlns:xsd', 'http://www.w3.org/2001/XMLSchema'],
+ ['element', 'xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance'],
+ ['element', 'office:version', '1.0'],
+ ['tagline', 'office:scripts'],
+ ['tag', 'office:font-face-decls',
+ ['tagline', 'style:font-face',
+ ['element', 'style:name', 'DejaVu Sans'],
+ ['element', 'svg:font-family', '&apos;DejaVu Sans&apos;'],
+ ['element', 'style:font-pitch', 'variable']],
['tagline', 'style:font-face',
- ['element', 'style:name', 'Nimbus Roman No9 L'],
- ['element', 'svg:font-family', '&apos;Nimbus Roman No9 L&apos;'],
- ['element', 'style:font-family-generic', 'roman'],
- ['element', 'style:font-pitch', 'variable']],
- ['tagline', 'style:font-face',
- ['element', 'style:name', 'Nimbus Sans L'],
- ['element', 'svg:font-family', '&apos;Nimbus Sans L&apos;'],
- ['element', 'style:font-family-generic', 'swiss'],
- ['element', 'style:font-pitch', 'variable']]],
- ['tagline', 'office:automatic-styles'],
- ['tag', 'office:body',
- ['tag', 'office:text',
- ['tagline', 'office:forms',
- ['element', 'form:automatic-focus', 'false'],
+ ['element', 'style:name', 'Nimbus Roman No9 L'],
+ ['element', 'svg:font-family', '&apos;Nimbus Roman No9 L&apos;'],
+ ['element', 'style:font-family-generic', 'roman'],
+ ['element', 'style:font-pitch', 'variable']],
+ ['tagline', 'style:font-face',
+ ['element', 'style:name', 'Nimbus Sans L'],
+ ['element', 'svg:font-family', '&apos;Nimbus Sans L&apos;'],
+ ['element', 'style:font-family-generic', 'swiss'],
+ ['element', 'style:font-pitch', 'variable']]],
+ ['tagline', 'office:automatic-styles'],
+ ['tag', 'office:body',
+ ['tag', 'office:text',
+ ['tagline', 'office:forms',
+ ['element', 'form:automatic-focus', 'false'],
['element', 'form:apply-design-mode', 'false']],
- ['tag', 'text:sequence-decls',
- ['tagline', 'text:sequence-decl',
- ['element', 'text:display-outline-level', '0'],
- ['element', 'text:name', 'Illustration']],
- ['tagline', 'text:sequence-decl',
- ['element', 'text:display-outline-level', '0'],
+ ['tag', 'text:sequence-decls',
+ ['tagline', 'text:sequence-decl',
+ ['element', 'text:display-outline-level', '0'],
+ ['element', 'text:name', 'Illustration']],
+ ['tagline', 'text:sequence-decl',
+ ['element', 'text:display-outline-level', '0'],
['element', 'text:name', 'Table']],
- ['tagline', 'text:sequence-decl',
- ['element', 'text:display-outline-level', '0'],
- ['element', 'text:name', 'Text']],
- ['tagline', 'text:sequence-decl',
- ['element', 'text:display-outline-level', '0'],
- ['element', 'text:name', 'Drawing']]],
- ['tagline', 'text:p',
- ['element', 'text:style-name', 'Standard']]]]]
-
- # Generate content.xml XML data
- xml = XML()
- self.lines = xml.convert(self.data)
- self.filedata = '\n'.join(self.lines)
- # Return generated data
- return self.filedata
-
-
+ ['tagline', 'text:sequence-decl',
+ ['element', 'text:display-outline-level', '0'],
+ ['element', 'text:name', 'Text']],
+ ['tagline', 'text:sequence-decl',
+ ['element', 'text:display-outline-level', '0'],
+ ['element', 'text:name', 'Drawing']]],
+ ['tagline', 'text:p',
+ ['element', 'text:style-name', 'Standard']]]]]
+
+ # Generate content.xml XML data
+ xmldoc = XML()
+ self.lines = xmldoc.convert(self.data)
+ self.filedata = '\n'.join(self.lines)
+ # Return generated data
+ return self.filedata
diff --git a/contrib/non-profit-audit-reports/readcsv.py b/contrib/non-profit-audit-reports/readcsv.py
index 67fc5663..932b4df9 100755
--- a/contrib/non-profit-audit-reports/readcsv.py
+++ b/contrib/non-profit-audit-reports/readcsv.py
@@ -1,4 +1,4 @@
-#!/usr/bin/python
+#!/usr/bin/env python3
# readcsv.py
# CSV reading technical study
#
@@ -23,9 +23,9 @@ import csv
dialects = csv.list_dialects()
for dialect in dialects:
- print 'dialect %s' % str(dialect)
+ print(f'dialect {dialect}')
-csvfile = open('tests/general-ledger.csv', 'rb')
+csvfile = open('test/general-ledger.csv', 'rb')
reader = csv.reader(csvfile, delimiter=',', quotechar='"')
for row in reader:
- print row
+ print(row)
diff --git a/doc/ledger.1 b/doc/ledger.1
index 82b03b46..f64be720 100644
--- a/doc/ledger.1
+++ b/doc/ledger.1
@@ -138,7 +138,7 @@ The prices are reported with the granularity of a single day.
Report prices for all commodities in postings matching the
.Ar report-query .
Prices are reported down to the second, using the same format as the
-.Pa ~/.pricedb
+.Pa \[ti]/.pricedb
file.
.It Ic print Oo Ar report-query Oc
Print out the full transactions of any matching postings using the same
@@ -214,10 +214,10 @@ Sort postings by evaluating the given
Note that a comma-separated list of expressions is allowed, in which case each
sorting term is used in order to determine the final ordering. For example,
to search by date and then amount, one would use:
-.Dl ledger reg --sort 'date, amount'
+.Dl ledger reg --sort \[aq]date, amount\[aq]
The sort order may be controlled with the '-' sign. For example, to sort in
reverse chronological order:
-.Dl ledger reg --sort '-date'
+.Dl ledger reg --sort \[aq]-date\[aq]
.It Fl \-tail Ar number
Only show the last
.Ar number
@@ -261,7 +261,7 @@ are also accepted.
List all postings matching the
.Ar sql-query .
This command allows to generate SQL-like queries, e.g.:
-.Dl Li ledger select date,amount from posts where account=~/Income/
+.Dl Li ledger select date,amount from posts where account=\[ti]/Income/
.It Ic source
Parse a journal file and checks it for errors.
.Nm
@@ -319,11 +319,11 @@ desired width.
Prepend
.Ar EXPR
to all accounts reported. That is, the option
-.Fl \-account Ar \*q'Personal'\*q
+.Fl \-account Ar \*q\[aq]Personal\[aq]\*q
would tack
.Ar Personal:
and
-.Fl \-account Ar \*qtag('VAT')\*q
+.Fl \-account Ar \*qtag(\[aq]VAT\[aq])\*q
would tack the value of the VAT tag to the beginning of every account
reported in a
.Ic balance
@@ -453,7 +453,7 @@ according to
.Ar FMT .
.It Fl \-current Pq Fl c
Shorthand for
-.Fl \-limit Ar "'date <= today'" .
+.Fl \-limit Ar "\[aq]date <= today\[aq]" .
.It Fl \-daily Pq Fl D
Shorthand for
.Fl \-period Ar daily .
@@ -1434,14 +1434,14 @@ may be set using an environment variable if the option has a long name.
For example setting the environment variable
.Ev LEDGER_DATE_FORMAT="%d.%m.%Y"
will have the same effect as specifying
-.Fl \-date-format Ar '%d.%m.%Y'
+.Fl \-date-format Ar \[aq]%d.%m.%Y\[aq]
on the command-line. Options on the command-line always take precedence over
environment variable settings, however.
.Sh FILES
.Bl -tag -width -indent
.It Pa $XDG_CONFIG_HOME/ledger/ledgerrc
-.It Pa ~/.config/ledger/ledgerrc
-.It Pa ~/.ledgerrc
+.It Pa \[ti]/.config/ledger/ledgerrc
+.It Pa \[ti]/.ledgerrc
Your personal
.Nm
initializations.
diff --git a/flake.lock b/flake.lock
index 78280130..b6441f74 100644
--- a/flake.lock
+++ b/flake.lock
@@ -2,11 +2,11 @@
"nodes": {
"nixpkgs": {
"locked": {
- "lastModified": 1675153841,
- "narHash": "sha256-EWvU3DLq+4dbJiukfhS7r6sWZyJikgXn6kNl7eHljW8=",
+ "lastModified": 1701484532,
+ "narHash": "sha256-zC6a3b7zw7+1DfQt1p+GZ/5Mk19mI1dxnjZHi+5hNiM=",
"owner": "NixOS",
"repo": "nixpkgs",
- "rev": "ea692c2ad1afd6384e171eabef4f0887d2b882d3",
+ "rev": "21ee79ad8cff9638ec0edaa6d2f1574dd237e1df",
"type": "github"
},
"original": {
diff --git a/flake.nix b/flake.nix
index de591136..384de947 100644
--- a/flake.nix
+++ b/flake.nix
@@ -25,7 +25,7 @@
outputs = [ "out" "dev" ] ++ lib.optionals usePython [ "py" ];
buildInputs = [
- gmp mpfr gnused
+ gmp mpfr gnused icu
] ++ lib.optionals useLibedit [
libedit
] ++ lib.optionals useReadline [
diff --git a/python/demo.py b/python/demo.py
index b3fe1d04..2102afa5 100755
--- a/python/demo.py
+++ b/python/demo.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
import sys
from datetime import datetime
diff --git a/src/query.cc b/src/query.cc
index 945bd34c..705ba151 100644
--- a/src/query.cc
+++ b/src/query.cc
@@ -126,6 +126,10 @@ query_t::lexer_t::next_token(query_t::lexer_t::token_t::kind_t tok_context)
case '#': ++arg_i; return token_t(token_t::TOK_CODE);
case '%': ++arg_i; return token_t(token_t::TOK_META);
case '=':
+ if (arg_i == (*begin).as_string().begin()) {
+ ++arg_i;
+ return token_t(token_t::TOK_NOTE);
+ }
++arg_i;
consume_next = true;
return token_t(token_t::TOK_EQ);
diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt
index 301959db..25d91a9e 100644
--- a/test/CMakeLists.txt
+++ b/test/CMakeLists.txt
@@ -19,19 +19,11 @@ macro(add_ledger_harness_tests _class)
file(GLOB ${_class}_TESTS *.test)
foreach(TestFile ${${_class}_TESTS})
get_filename_component(TestFile_Name ${TestFile} NAME_WE)
- string(FIND ${TestFile_Name} "_py" TestFile_IsPythonTest)
- if ((TestFile_IsPythonTest GREATER -1))
- get_filename_component(TestFile_FullName ${TestFile} NAME)
- string(FIND ${TestFile_FullName} "_py.test" TestFile_IsAnyPythonTest)
- string(FIND ${TestFile_FullName} "_py${Python_VERSION_MAJOR}.test" TestFile_IsThisPythonTest)
- if ((TestFile_IsAnyPythonTest EQUAL -1) AND (TestFile_IsThisPythonTest EQUAL -1))
- continue()
- endif()
- endif()
+ string(FIND ${TestFile_Name} "_py.test" TestFile_IsPythonTest)
if ((TestFile_IsPythonTest EQUAL -1) OR HAVE_BOOST_PYTHON)
add_test(NAME ${_class}Test_${TestFile_Name}
COMMAND ${Python_EXECUTABLE} ${PROJECT_SOURCE_DIR}/test/RegressTests.py
- $<TARGET_FILE:ledger> ${PROJECT_SOURCE_DIR}
+ --ledger $<TARGET_FILE:ledger> --sourcepath ${PROJECT_SOURCE_DIR}
${TestFile} ${TEST_PYTHON_FLAGS})
set_tests_properties(${_class}Test_${TestFile_Name}
PROPERTIES ENVIRONMENT "TZ=${Ledger_TEST_TIMEZONE}")
diff --git a/test/CheckBaselineTests.py b/test/CheckBaselineTests.py
index fa9fa2bc..1a983b00 100755
--- a/test/CheckBaselineTests.py
+++ b/test/CheckBaselineTests.py
@@ -1,10 +1,9 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
+#!/usr/bin/env python3
+import argparse
import sys
import re
import os
-import argparse
from os.path import *
from subprocess import Popen, PIPE
@@ -52,24 +51,9 @@ class CheckBaselineTests (CheckOptions):
return errors
if __name__ == "__main__":
- def getargs():
- parser = argparse.ArgumentParser(prog='CheckBaselineTests',
- description='Check that ledger options are tested')
- parser.add_argument('-l', '--ledger',
- dest='ledger',
- type=str,
- action='store',
- required=True,
- help='the path to the ledger executable to test with')
- parser.add_argument('-s', '--source',
- dest='source',
- type=str,
- action='store',
- required=True,
- help='the path to the top level ledger source directory')
- return parser.parse_args()
-
- args = getargs()
+ args = argparse.ArgumentParser(prog='CheckBaselineTests',
+ description='Check that ledger options are tested',
+ parents=[CheckOptions.parser()]).parse_args()
script = CheckBaselineTests(args)
status = script.main()
sys.exit(status)
diff --git a/test/CheckComments.py b/test/CheckComments.py
index 446137b0..9bcfcbb3 100644
--- a/test/CheckComments.py
+++ b/test/CheckComments.py
@@ -1,20 +1,19 @@
+#!/usr/bin/env python3
+
import sys
import re
import os
+import argparse
-ok = 100.0
-if sys.argv[1] == '-l':
- ok = float(sys.argv[2])
- sys.argv = [sys.argv[0]] + sys.argv[3:]
-
-debug = False
-if sys.argv[1] == '-v':
- debug = True
- sys.argv = [sys.argv[0]] + sys.argv[2:]
+parser = argparse.ArgumentParser( prog='CheckComments')
+parser.add_argument('-v', '--debug', dest='debug', action='store_true')
+parser.add_argument('-l', '--limit', dest='ok', type=float, default=100.0)
+parser.add_argument('path', nargs='+', help='Path to source file to check comments')
+args = parser.parse_args()
errors = 0
-for path in sys.argv[1:]:
+for path in args.path:
other_depth = 0
brace_depth = 0
code_count = 0
@@ -26,6 +25,7 @@ for path in sys.argv[1:]:
ctor_dtor = False
linenum = 0
+ print(f'checking {path}')
fd = open(path, 'r')
for line in fd.readlines():
linenum += 1
@@ -48,48 +48,48 @@ for path in sys.argv[1:]:
match = re.search('(namespace|enum|class|struct|union)', line)
if match:
kind = match.group(1)
- if debug: print "kind =", kind
+ if args.debug: print("kind =", kind)
elif kind == "function":
match = re.search('(\S+)\(', line)
if match:
function_name = match.group(1)
long_comments[function_name] = comment_count
comment_count = 0
- if debug: print "name found %s" % function_name
+ if args.debug: print("name found %s" % function_name)
if re.search('{', line) and not re.search('@{', line):
if kind == "function":
brace_depth += 1
- if debug: print "brace_depth =", brace_depth
+ if args.debug: print("brace_depth =", brace_depth)
else:
other_depth += 1
kind = "function"
- if debug: print "other_depth =", other_depth
+ if args.debug: print("other_depth =", other_depth)
if re.search('}', line) and not re.search('@}', line):
if brace_depth > 0:
brace_depth -= 1
- if debug: print "brace_depth =", brace_depth
+ if args.debug: print("brace_depth =", brace_depth)
if brace_depth == 0:
- if debug: print "function done"
+ if args.debug: print("function done")
if function_name in long_comments:
comment_count += long_comments[function_name]
if code_count == 0:
- percent = ok
- print "%7s %4d/%4d %s:%d: %s" % \
+ percent = args.ok
+ print("%7s %4d/%4d %s:%d: %s" % \
("empty", comment_count, code_count,
os.path.basename(path), linenum,
- function_name)
+ function_name))
errors += 1
else:
percent = 100.0 * (float(comment_count) /
float(code_count))
- if percent < ok and not ctor_dtor:
- print "%6.0f%% %4d/%4d %s:%d: %s" % \
+ if percent < args.ok and not ctor_dtor:
+ print("%6.0f%% %4d/%4d %s:%d: %s" % \
(percent, comment_count, code_count,
os.path.basename(path), linenum,
- function_name)
+ function_name))
errors += 1
code_count = 0
comment_count = 0
@@ -98,7 +98,7 @@ for path in sys.argv[1:]:
ctor_dtor = False
else:
other_depth -= 1
- if debug: print "other_depth =", other_depth
+ if args.debug: print("other_depth =", other_depth)
if brace_depth > 0:
if re.search("TRACE_[CD]TOR", line):
diff --git a/test/CheckManpage.py b/test/CheckManpage.py
index 3da9d872..1795b77e 100755
--- a/test/CheckManpage.py
+++ b/test/CheckManpage.py
@@ -1,5 +1,4 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
+#!/usr/bin/env python3
import sys
import re
@@ -20,24 +19,9 @@ class CheckManpage (CheckOptions):
self.source_type = 'manpage'
if __name__ == "__main__":
- def getargs():
- parser = argparse.ArgumentParser(prog='CheckManpage',
- description='Check that ledger options are documented in the manpage')
- parser.add_argument('-l', '--ledger',
- dest='ledger',
- type=str,
- action='store',
- required=True,
- help='the path to the ledger executable to test with')
- parser.add_argument('-s', '--source',
- dest='source',
- type=str,
- action='store',
- required=True,
- help='the path to the top level ledger source directory')
- return parser.parse_args()
-
- args = getargs()
+ args = argparse.ArgumentParser(prog='CheckManpage',
+ description='Check that ledger options are documented in the manpage'
+ parents=[CheckOptions.parser()]).parse_args()
script = CheckManpage(args)
status = script.main()
sys.exit(status)
diff --git a/test/CheckOptions.py b/test/CheckOptions.py
index 3f08fb0d..cdd9b244 100755
--- a/test/CheckOptions.py
+++ b/test/CheckOptions.py
@@ -1,10 +1,10 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
+#!/usr/bin/env python3
import re
import os
import sys
import shlex
+import pathlib
import argparse
import subprocess
@@ -12,6 +12,15 @@ from os.path import *
from subprocess import Popen, PIPE
class CheckOptions (object):
+ @staticmethod
+ def parser():
+ parser = argparse.ArgumentParser(add_help=False)
+ parser.add_argument('-l', '--ledger', type=pathlib.Path, required=True,
+ help='the path to the ledger executable to test with')
+ parser.add_argument('-s', '--source', type=pathlib.Path, required=True,
+ help='the path to the top level ledger source directory')
+ return parser
+
def __init__(self, args):
self.option_pattern = None
self.source_file = None
@@ -40,7 +49,7 @@ class CheckOptions (object):
for source_file in ['session', 'report']:
command.append(os.path.join(self.source, 'src', '%s.cc' % source_file))
try:
- output = subprocess.check_output(command).split('\n');
+ output = subprocess.check_output(command, universal_newlines=True).split('\n')
except subprocess.CalledProcessError:
output = ''
@@ -58,7 +67,7 @@ class CheckOptions (object):
def ledger_functions(self):
command = shlex.split('grep --no-filename fn_ %s' % (os.path.join(self.source, 'src', 'report.h')))
try:
- output = subprocess.check_output(command).split('\n');
+ output = subprocess.check_output(command, universal_newlines=True).split('\n');
except subprocess.CalledProcessError:
output = ''
@@ -74,7 +83,13 @@ class CheckOptions (object):
else:
options.remove(option)
known_alternates = self.find_alternates()
- self.unknown_options = {option for option in options if option not in known_alternates}
+ self.unknown_options = options - known_alternates
+ self.missing_options -= {
+ # The option --explicit is a no-op as of March 2020 and is
+ # therefore intentionally undocumented.
+ # For details see commit 43b07fbab3b4c144eca4a771524e59c531ffa074
+ 'explicit'
+ }
functions = self.find_functions(self.source_file)
for function in self.ledger_functions():
@@ -82,8 +97,8 @@ class CheckOptions (object):
self.missing_functions.add(function)
else:
functions.remove(function)
- known_functions = ['tag', 'has_tag', 'meta', 'has_meta']
- self.unknown_functions = {function for function in functions if function not in known_functions}
+ known_functions = {'tag', 'has_tag', 'meta', 'has_meta'}
+ self.unknown_functions = functions - known_functions
if len(self.missing_options):
print("Missing %s option entries for:%s%s\n" % (self.source_type, self.sep, self.sep.join(sorted(list(self.missing_options)))))
diff --git a/test/CheckTexinfo.py b/test/CheckTexinfo.py
index fa709e1b..7b13c50e 100755
--- a/test/CheckTexinfo.py
+++ b/test/CheckTexinfo.py
@@ -1,5 +1,4 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
+#!/usr/bin/env python3
import sys
import re
@@ -91,24 +90,9 @@ class CheckTexinfo (CheckOptions):
return options
if __name__ == "__main__":
- def getargs():
- parser = argparse.ArgumentParser(prog='CheckTexinfo',
- description='Check that ledger options are documented in the texinfo manual')
- parser.add_argument('-l', '--ledger',
- dest='ledger',
- type=str,
- action='store',
- required=True,
- help='the path to the ledger executable to test with')
- parser.add_argument('-s', '--source',
- dest='source',
- type=str,
- action='store',
- required=True,
- help='the path to the top level ledger source directory')
- return parser.parse_args()
-
- args = getargs()
+ args = argparse.ArgumentParser(prog='CheckTexinfo',
+ description='Check that ledger options are documented in the texinfo manual',
+ parents=[CheckOptions.parser()]).parse_args()
script = CheckTexinfo(args)
status = script.main()
sys.exit(status)
diff --git a/test/ConfirmTests.py b/test/ConfirmTests.py
index e82479ed..54187130 100755
--- a/test/ConfirmTests.py
+++ b/test/ConfirmTests.py
@@ -1,20 +1,24 @@
-#!/usr/bin/python
+#!/usr/bin/env python3
# This script confirms both that the register report "adds up", and that its
# final balance is the same as what the balance report shows.
+import argparse
+import pathlib
import sys
import os
import re
from LedgerHarness import LedgerHarness
-harness = LedgerHarness(sys.argv)
-tests = sys.argv[3]
+parser = argparse.ArgumentParser(prog='ConfirmTests', parents=[LedgerHarness.parser()])
+parser.add_argument('tests', type=pathlib.Path)
+args = parser.parse_args()
+harness = LedgerHarness(args.ledger, args.sourcepath, args.verify, args.gmalloc, args.python)
-if not os.path.isdir(tests) and not os.path.isfile(tests):
- sys.stderr.write("'%s' is not a directory or file (cwd %s)" %
- (tests, os.getcwd()))
+if not os.path.isdir(args.tests) and not os.path.isfile(args.tests):
+ print(f'{args.tests} is not a directory or file (cwd: {os.getcwd()})'
+ , file=sys.stderr)
sys.exit(1)
commands = [
@@ -56,8 +60,8 @@ def confirm_report(command):
if re.search(' -[VGB] ', command) and diff < 0.015:
diff = 0.0
if diff > 0.001:
- print("DISCREPANCY: %.3f (%.3f - %.3f) at line %d:" % \)
- (running_total - total, running_total, total, index)
+ print("DISCREPANCY: %.3f (%.3f - %.3f) at line %d:" % \
+ (running_total - total, running_total, total, index))
print(line,)
running_total = total
failure = True
@@ -78,15 +82,15 @@ def confirm_report(command):
diff = 0.0
if diff > 0.001:
print()
- print("DISCREPANCY: %.3f (%.3f - %.3f) between register and balance" % \)
- (balance_total - running_total, balance_total, running_total)
+ print("DISCREPANCY: %.3f (%.3f - %.3f) between register and balance" % \
+ (balance_total - running_total, balance_total, running_total))
print(last_line,)
failure = True
return not failure
for cmd in commands:
- if confirm_report('$ledger --rounding $cmd ' + re.sub('\$tests', tests, cmd)):
+ if confirm_report('$ledger --rounding $cmd ' + re.sub('\$tests', str(args.tests), cmd)):
harness.success()
else:
harness.failure()
diff --git a/test/DocTests.py b/test/DocTests.py
index daecdd90..e12a4ae4 100755
--- a/test/DocTests.py
+++ b/test/DocTests.py
@@ -1,5 +1,4 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
+#!/usr/bin/env python3
from io import open
diff --git a/test/GenerateTests.py b/test/GenerateTests.py
index 2b966e35..1301bcd0 100755
--- a/test/GenerateTests.py
+++ b/test/GenerateTests.py
@@ -1,9 +1,12 @@
-#!/usr/bin/python
+#!/usr/bin/env python3
# This script confirms both that the register report "adds up", and that its
# final balance is the same as what the balance report shows.
+import argparse
+import pathlib
import sys
+import os
import re
from difflib import ndiff
@@ -15,19 +18,21 @@ try:
except:
pass
-args = sys.argv
-jobs = 1
-match = re.match('-j([0-9]+)?', args[1])
-if match:
- args = [args[0]] + args[2:]
- if match.group(1):
- jobs = int(match.group(1))
-if jobs == 1:
- multiproc = False
-
from LedgerHarness import LedgerHarness
-harness = LedgerHarness(args)
+parser = argparse.ArgumentParser(prog='GenerateTests', parents=[LedgerHarness.parser()])
+parser.add_argument('-j', '--jobs', type=int, default=1)
+parser.add_argument('tests', type=pathlib.Path)
+parser.add_argument('beg_range', nargs='?', type=int, default=1)
+parser.add_argument('end_range', nargs='?', type=int, default=20)
+args = parser.parse_args()
+multiproc &= (args.jobs >= 1)
+harness = LedgerHarness(args.ledger, args.sourcepath, args.verify, args.gmalloc, args.python)
+
+if not os.path.isdir(args.tests) and not os.path.isfile(args.tests):
+ print(f'{args.tests} is not a directory or file (cwd: {os.getcwd()})'
+ , file=sys.stderr)
+ sys.exit(1)
#def normalize(line):
# match = re.match("((\s*)([A-Za-z]+)?(\s*)([-0-9.]+)(\s*)([A-Za-z]+)?)( (.+))?$", line)
@@ -123,12 +128,6 @@ def generation_test(seed):
return success
-beg_range = 1
-end_range = 20
-if len(args) > 4:
- beg_range = int(args[3])
- end_range = int(args[4])
-
def run_gen_test(i):
if generation_test(i):
harness.success()
@@ -137,14 +136,14 @@ def run_gen_test(i):
return harness.failed
if multiproc:
- pool = Pool(jobs*2)
+ pool = Pool(args.jobs*2)
else:
pool = None
if pool:
- pool.map(run_gen_test, range(beg_range, end_range))
+ pool.map(run_gen_test, range(args.beg_range, args.end_range))
else:
- for i in range(beg_range, end_range):
+ for i in range(args.beg_range, args.end_range):
run_gen_test(i)
if pool:
diff --git a/test/LedgerHarness.py b/test/LedgerHarness.py
index 5051fc8b..e2c96894 100755
--- a/test/LedgerHarness.py
+++ b/test/LedgerHarness.py
@@ -1,6 +1,8 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
+#!/usr/bin/env python3
+import argparse
+import pathlib
+import shlex
import sys
import os
import re
@@ -34,27 +36,36 @@ copyreg.pickle(types.MethodType, _pickle_method, _unpickle_method)
class LedgerHarness:
ledger = None
sourcepath = None
+ skipped = 0
succeeded = 0
failed = 0
verify = False
gmalloc = False
python = False
- def __init__(self, argv):
- if not os.path.isfile(argv[1]):
- print("Cannot find ledger at '%s'" % argv[1])
+ @staticmethod
+ def parser():
+ parser = argparse.ArgumentParser(add_help=False)
+ parser.add_argument('-l', '--ledger', type=pathlib.Path, required=True)
+ parser.add_argument('-s', '--sourcepath', type=pathlib.Path, required=True)
+ parser.add_argument('--verify', action='store_true')
+ parser.add_argument('--gmalloc', action='store_true')
+ parser.add_argument('--python', action='store_true')
+ return parser
+
+ def __init__(self, ledger, sourcepath, verify=False, gmalloc=False, python=False):
+ if not ledger.is_file():
+ print("Cannot find ledger at '{ledger}'", file=sys.stderr)
sys.exit(1)
- if not os.path.isdir(argv[2]):
- print("Cannot find source path at '%s'" % argv[2])
+ if not sourcepath.is_dir():
+ print("Cannot find source path at '{sourcepath}'", file=sys.stderr)
sys.exit(1)
- self.ledger = os.path.realpath(argv[1])
- self.sourcepath = os.path.realpath(argv[2])
- self.succeeded = 0
- self.failed = 0
- self.verify = '--verify' in argv
- self.gmalloc = '--gmalloc' in argv
- self.python = '--python' in argv
+ self.ledger = ledger.resolve()
+ self.sourcepath = sourcepath.resolve()
+ self.verify = verify
+ self.gmalloc = gmalloc
+ self.python = python
def run(self, command, verify=None, gmalloc=None, columns=True):
env = os.environ.copy()
@@ -71,34 +82,29 @@ class LedgerHarness:
env['MALLOC_FILL_SPACE'] = '1'
env['MALLOC_STRICT_SIZE'] = '1'
+ cmd = [str(self.ledger), '--args-only']
if (verify is not None and verify) or \
(verify is None and self.verify):
- insert = ' --verify'
- else:
- insert = ''
-
+ cmd.append('--verify')
if columns:
- insert += ' --columns=80'
-
- command = command.replace('$ledger', '"%s"%s %s' % \
- (self.ledger, insert, '--args-only'))
+ cmd.append('--columns=80')
+ command = command.replace('$ledger', shlex.join(cmd))
valgrind = '/usr/bin/valgrind'
if not os.path.isfile(valgrind):
valgrind = '/opt/local/bin/valgrind'
- if os.path.isfile(valgrind) and '--verify' in insert:
- command = valgrind + ' -q ' + command
+ if os.path.isfile(valgrind) and '--verify' in cmd:
+ command = shlex.join([valgrind, '-q', command])
- # If we are running under msys2, use bash to execute the test commands
- if 'MSYSTEM' in os.environ:
+ ismsys2 = 'MSYSTEM' in os.environ
+ if ismsys2:
+ # If we are running under msys2, use bash to execute the test commands
bash_path = os.environ['MINGW_PREFIX'] + '/../usr/bin/bash.exe'
- return Popen([bash_path, '-c', command], shell=False,
- close_fds=False, env=env, stdin=PIPE, stdout=PIPE,
- stderr=PIPE, cwd=self.sourcepath)
+ command = shlex.join([bash_path, '-c', command])
- return Popen(command, shell=True, close_fds=True, env=env,
- stdin=PIPE, stdout=PIPE, stderr=PIPE,
+ return Popen(command, shell=not ismsys2, close_fds=not ismsys2,
+ env=env, stdin=PIPE, stdout=PIPE, stderr=PIPE,
cwd=self.sourcepath)
def read(self, fd):
@@ -113,10 +119,7 @@ class LedgerHarness:
def readlines(self, fd):
lines = []
for line in fd.readlines():
- if sys.version_info.major == 2:
- line = unicode(line, 'utf-8')
- else:
- line = line.decode('utf-8')
+ line = line.decode('utf-8')
if not line.startswith('GuardMalloc'):
lines.append(line)
return lines
@@ -134,6 +137,11 @@ class LedgerHarness:
sys.stdout.flush()
self.succeeded += 1
+ def skip(self):
+ sys.stdout.write("S")
+ sys.stdout.flush()
+ self.skipped += 1
+
def failure(self, name=None):
sys.stdout.write("E")
if name:
@@ -144,16 +152,20 @@ class LedgerHarness:
def exit(self):
print()
if self.succeeded > 0:
- print("OK (%d) " % self.succeeded,)
+ print(f"OK ({self.succeeded})")
+ if self.skipped > 0:
+ print(f"SKIPPED ({self.skipped})")
if self.failed > 0:
- print("FAILED (%d)" % self.failed,)
+ print(f"FAILED ({self.failed})")
print()
sys.exit(self.failed)
if __name__ == '__main__':
- harness = LedgerHarness(sys.argv)
- proc = harness.run('$ledger -f doc/sample.dat reg')
+ parser = argparse.ArgumentParser(prog='LedgerHarness', parents=[LedgerHarness.parser()])
+ args = LedgerHarness.parser().parse_args()
+ harness = LedgerHarness(args.ledger, args.sourcepath, args.verify, args.gmalloc, args.python)
+ proc = harness.run('$ledger -f test/input/sample.dat reg')
print('STDOUT:')
print(proc.stdout.read())
print('STDERR:')
diff --git a/test/RegressTests.py b/test/RegressTests.py
index 849c4137..47abc3d0 100755
--- a/test/RegressTests.py
+++ b/test/RegressTests.py
@@ -1,8 +1,9 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
+#!/usr/bin/env python3
from io import open
+import argparse
+import pathlib
import sys
import os
import re
@@ -19,22 +20,16 @@ from difflib import unified_diff
from LedgerHarness import LedgerHarness
-args = sys.argv
-jobs = 1
-match = re.match('-j([0-9]+)?', args[1])
-if match:
- args = [args[0]] + args[2:]
- if match.group(1):
- jobs = int(match.group(1))
-if jobs == 1:
- multiproc = False
-
-harness = LedgerHarness(args)
-tests = args[3]
-
-if not os.path.isdir(tests) and not os.path.isfile(tests):
- sys.stderr.write("'%s' is not a directory or file (cwd %s)" %
- (tests, os.getcwd()))
+parser = argparse.ArgumentParser(prog='RegressTests', parents=[LedgerHarness.parser()])
+parser.add_argument('-j', '--jobs', type=int, default=1)
+parser.add_argument('tests', type=pathlib.Path)
+args = parser.parse_args()
+multiproc &= (args.jobs >= 1)
+harness = LedgerHarness(args.ledger, args.sourcepath, args.verify, args.gmalloc, args.python)
+
+if not args.tests.is_dir() and not args.tests.is_file():
+ print(f'{args.tests} is not a directory or file (cwd: {os.getcwd()})'
+ , file=sys.stderr)
sys.exit(1)
class RegressFile(object):
@@ -43,33 +38,31 @@ class RegressFile(object):
self.fd = open(self.filename, encoding='utf-8')
def transform_line(self, line):
- line = line.replace('$sourcepath', harness.sourcepath)
- line = line.replace('$FILE', os.path.realpath(self.filename))
- return line
+ return line\
+ .replace('$sourcepath', str(harness.sourcepath))\
+ .replace('$FILE', str(self.filename.resolve()))
def read_test(self):
- test = {
- 'command': None,
- 'output': None,
- 'error': None,
- 'exitcode': 0
- }
+ class Test:
+ command = None
+ output = None
+ error = None
+ exitcode = 0
in_output = False
in_error = False
line = self.fd.readline()
- if sys.version_info.major == 2 and type(line) is str:
- line = unicode(line, 'utf-8')
+ test = Test()
while line:
if line.startswith("test "):
command = line[5:]
match = re.match('(.*) -> ([0-9]+)', command)
if match:
- test['command'] = self.transform_line(match.group(1))
- test['exitcode'] = int(match.group(2))
+ test.command = self.transform_line(match.group(1))
+ test.exitcode = int(match.group(2))
else:
- test['command'] = command
+ test.command = command
in_output = True
elif in_output:
@@ -77,57 +70,50 @@ class RegressFile(object):
in_output = in_error = False
break
elif in_error:
- if test['error'] is None:
- test['error'] = []
- test['error'].append(self.transform_line(line))
+ if test.error is None:
+ test.error = []
+ test.error.append(self.transform_line(line))
else:
if line.startswith("__ERROR__"):
in_error = True
else:
- if test['output'] is None:
- test['output'] = []
- test['output'].append(self.transform_line(line))
-
+ if test.output is None:
+ test.output = []
+ test.output.append(self.transform_line(line))
line = self.fd.readline()
- if sys.version_info.major == 2 and type(line) is str:
- line = unicode(line, 'utf-8')
#print("line =", line)
- return test['command'] and test
+ return test.command and test
def notify_user(self, msg, test):
print(msg)
print("--")
- print(self.transform_line(test['command']),)
+ print(self.transform_line(test.command),)
print("--")
def run_test(self, test):
use_stdin = False
if sys.platform == 'win32':
- test['command'] = test['command'].replace('/dev/null', 'nul')
+ test.command = test.command.replace('/dev/null', 'nul')
# There is no equivalent to /dev/stdout, /dev/stderr, /dev/stdin
# on Windows, so skip tests that require them.
- if '/dev/std' in test['command']:
- harness.success()
+ if '/dev/std' in test.command:
+ harness.skipped()
return
- if test['command'].find("-f ") != -1:
- test['command'] = '$ledger ' + test['command']
- if re.search("-f (-|/dev/stdin)(\s|$)", test['command']):
+ if test.command.find('-f ') != -1:
+ test.command = '$ledger ' + test.command
+ if re.search('-f (-|/dev/stdin)(\s|$)', test.command):
use_stdin = True
else:
- test['command'] = (('$ledger -f "%s" ' %
- os.path.realpath(self.filename)) +
- test['command'])
+ test.command = f'$ledger -f "{str(self.filename.resolve())}" {test.command}'
- p = harness.run(test['command'],
- columns=(not re.search('--columns', test['command'])))
+ p = harness.run(test.command,
+ columns=(not re.search('--columns', test.command)))
if use_stdin:
fd = open(self.filename, encoding='utf-8')
try:
- stdin = fd.read()
- if sys.version_info.major > 2:
- stdin = stdin.encode('utf-8')
+ stdin = fd.read().encode('utf-8')
p.stdin.write(stdin)
finally:
fd.close()
@@ -136,9 +122,9 @@ class RegressFile(object):
success = True
printed = False
index = 0
- if test['output'] is not None:
+ if test.output is not None:
process_output = harness.readlines(p.stdout)
- expected_output = test['output']
+ expected_output = test.output
if sys.platform == 'win32':
process_output = [l.replace('\r\n', '\n').replace('\\', '/')
for l in process_output]
@@ -155,21 +141,19 @@ class RegressFile(object):
self.notify_user("FAILURE in output from %s:" % self.filename, test)
success = False
printed = True
- if sys.version_info.major == 2 and type(line) is str:
- line = unicode(line, 'utf-8')
print(' ', line,)
printed = False
index = 0
process_error = harness.readlines(p.stderr)
- if test['error'] is not None or process_error is not None:
- if test['error'] is None:
- test['error'] = []
+ if test.error is not None or process_error is not None:
+ if test.error is None:
+ test.error = []
if sys.platform == 'win32':
process_error = [l.replace('\r\n', '\n').replace('\\', '/')
for l in process_error]
- test['error'] = [l.replace('\\', '/') for l in test['error']]
- for line in unified_diff(test['error'], process_error):
+ test.error = [l.replace('\\', '/') for l in test.error]
+ for line in unified_diff(test.error, process_error):
index += 1
if index < 3:
continue
@@ -181,24 +165,24 @@ class RegressFile(object):
printed = True
print(" ", line,)
- if test['exitcode'] == p.wait():
+ if test.exitcode == p.wait():
if success:
harness.success()
else:
- harness.failure(os.path.basename(self.filename))
+ harness.failure(self.filename.name)
print("STDERR:")
print(p.stderr.read())
else:
if success: print
self.notify_user("FAILURE in exit code (%d != %d) from %s:"
- % (test['exitcode'], p.returncode, self.filename),
+ % (test.exitcode, p.returncode, self.filename),
test)
- harness.failure(os.path.basename(self.filename))
+ harness.failure(self.filename.name)
def run_tests(self):
if os.path.getsize(self.filename) == 0:
print("WARNING: Empty testfile detected: %s" % (self.filename), file=sys.stderr)
- harness.failure(os.path.basename(self.filename))
+ harness.failure(self.filename.name)
return False
test = self.read_test()
while test:
@@ -215,22 +199,21 @@ def do_test(path):
if __name__ == '__main__':
if multiproc:
- pool = Pool(jobs*2)
+ pool = Pool(args.jobs*2)
else:
pool = None
- if os.path.isdir(tests):
- tests = [os.path.join(tests, x)
- for x in os.listdir(tests)
- if (x.endswith('.test') and
- (not '_py.test' in x or (harness.python and
- not harness.verify)))]
+ if args.tests.is_dir():
+ tests = [p for p in args.tests.iterdir()
+ if (p.suffix == '.test' and
+ (not p.match('*_py.test') or (harness.python and
+ not harness.verify)))]
if pool:
pool.map(do_test, tests, 1)
else:
map(do_test, tests)
else:
- entry = RegressFile(tests)
+ entry = RegressFile(args.tests)
entry.run_tests()
entry.close()
diff --git a/test/baseline/feat-value_py3.test b/test/baseline/feat-value_py.test
index f82fbf2b..f82fbf2b 100644
--- a/test/baseline/feat-value_py3.test
+++ b/test/baseline/feat-value_py.test
diff --git a/test/baseline/feat-value_py2.test b/test/baseline/feat-value_py2.test
deleted file mode 100644
index 4378c91a..00000000
--- a/test/baseline/feat-value_py2.test
+++ /dev/null
@@ -1,23 +0,0 @@
-python
- def print_type(val):
- print(type(val), val)
-
-eval print_type(true)
-eval print_type([2010/08/10])
-eval print_type(10)
-eval print_type($10.00)
-eval print_type($10.00 + CAD 30)
-eval print_type("Hello!")
-eval print_type(/Hello!/)
-;eval print_type((1, 2, 3))
-
-test reg
-<type 'bool'> True
-<type 'datetime.date'> 2010-08-10
-<class 'ledger.Amount'> 10
-<class 'ledger.Amount'> $10.00
-<class 'ledger.Balance'> $10.00
-CAD 30
-<type 'unicode'> Hello!
-<class 'ledger.Value'> Hello!
-end test
diff --git a/test/convert.py b/test/convert.py
index 85e52701..a54edaac 100755
--- a/test/convert.py
+++ b/test/convert.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
# convert.py: This script converts a Boost.Test unit test into an
# equivalent Python unit test.
diff --git a/test/manual/transaction-notes-1.test b/test/manual/transaction-notes-1.test
index 05ab3412..4085a6e2 100644
--- a/test/manual/transaction-notes-1.test
+++ b/test/manual/transaction-notes-1.test
@@ -2,14 +2,14 @@
Expenses:Food $4.50
Assets:Checking
-2009/11/01 Panera Bread
+2009/11/02 Panera Bread
; Type: Coffee
; Let’s see, I ate a whole bunch of stuff, drank some coffee,
; pondered a bagel, then decided against the donut.
Expenses:Food $4.50
Assets:Checking
-2009/11/01 Panera Bread
+2009/11/03 Panera Bread
; Type: Dining
; :Eating:
; This is another long note, after the metadata.
@@ -18,5 +18,10 @@
test reg --columns=60 food and note eat
09-Nov-01 Panera Bread Expenses:Food $4.50 $4.50
-09-Nov-01 Panera Bread Expenses:Food $4.50 $9.00
+09-Nov-03 Panera Bread Expenses:Food $4.50 $9.00
+end test
+
+test reg --columns=60 food and =eat
+09-Nov-01 Panera Bread Expenses:Food $4.50 $4.50
+09-Nov-03 Panera Bread Expenses:Food $4.50 $9.00
end test
diff --git a/test/python/JournalTest.py b/test/python/JournalTest.py
index 84ad2f43..59442968 100644
--- a/test/python/JournalTest.py
+++ b/test/python/JournalTest.py
@@ -1,4 +1,4 @@
-# -*- coding: utf-8 -*-
+#!/usr/bin/env python3
import unittest
diff --git a/test/python/PostingTest.py b/test/python/PostingTest.py
index 0dd18112..f6b9bc04 100644
--- a/test/python/PostingTest.py
+++ b/test/python/PostingTest.py
@@ -1,4 +1,4 @@
-# -*- coding: utf-8 -*-
+#!/usr/bin/env python3
import unittest
import operator
diff --git a/test/python/TransactionTest.py b/test/python/TransactionTest.py
index c87f8c4b..12069389 100644
--- a/test/python/TransactionTest.py
+++ b/test/python/TransactionTest.py
@@ -1,4 +1,4 @@
-# -*- coding: utf-8 -*-
+#!/usr/bin/env python3
import unittest
import operator
diff --git a/test/regress/1182_2.test b/test/regress/1182_2.test
index d3c88dd8..6c018e94 100644
--- a/test/regress/1182_2.test
+++ b/test/regress/1182_2.test
@@ -13,5 +13,5 @@ __ERROR__
While parsing file "$FILE", line 5:
While parsing automated transaction:
> ============
-Error: Expected predicate after '='
+Error: note operator not followed by argument
end test
diff --git a/test/regress/B21BF389.py b/test/regress/B21BF389.py
index 11208e91..e130b7b3 100644
--- a/test/regress/B21BF389.py
+++ b/test/regress/B21BF389.py
@@ -1,5 +1,4 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
+#!/usr/bin/env python3
import sys
import ledger
diff --git a/test/unit/t_expr.cc b/test/unit/t_expr.cc
index c10ee029..ff30b3ed 100644
--- a/test/unit/t_expr.cc
+++ b/test/unit/t_expr.cc
@@ -142,7 +142,7 @@ BOOST_AUTO_TEST_CASE(testPredicateTokenizer6)
#ifndef NOT_FOR_PYTHON
query_t::lexer_t tokens(args.begin(), args.end());
- BOOST_CHECK_EQUAL(query_t::lexer_t::token_t::TOK_EQ, tokens.next_token().kind);
+ BOOST_CHECK_EQUAL(query_t::lexer_t::token_t::TOK_NOTE, tokens.next_token().kind);
BOOST_CHECK_EQUAL(query_t::lexer_t::token_t::TERM, tokens.next_token().kind);
BOOST_CHECK_EQUAL(query_t::lexer_t::token_t::TOK_AND, tokens.next_token().kind);
BOOST_CHECK_EQUAL(query_t::lexer_t::token_t::TERM, tokens.next_token().kind);
@@ -158,7 +158,7 @@ BOOST_AUTO_TEST_CASE(testPredicateTokenizer7)
#ifndef NOT_FOR_PYTHON
query_t::lexer_t tokens(args.begin(), args.end());
- BOOST_CHECK_EQUAL(query_t::lexer_t::token_t::TOK_EQ, tokens.next_token().kind);
+ BOOST_CHECK_EQUAL(query_t::lexer_t::token_t::TOK_NOTE, tokens.next_token().kind);
BOOST_CHECK_EQUAL(query_t::lexer_t::token_t::TERM, tokens.next_token().kind);
BOOST_CHECK_EQUAL(query_t::lexer_t::token_t::END_REACHED, tokens.next_token().kind);
#endif
diff --git a/tools/average b/tools/average
index 0e95c1c5..4c1e4b43 100755
--- a/tools/average
+++ b/tools/average
@@ -1,4 +1,4 @@
-#!/usr/bin/python
+#!/usr/bin/env python3
import getopt
import time
@@ -18,9 +18,9 @@ length = 0.0
i = 0
while i < count:
begin = time.time()
- cmd = '"' + string.join(args, '" "') + '"';
+ cmd = '"' + '" "'.join(args) + '"';
os.system(cmd)
length += time.time() - begin
i += 1
-print >> sys.stderr, length / count
+print(length / count, file=sys.stderr)
diff --git a/tools/genuuid b/tools/genuuid
index 53fb7a0a..0ad5bd92 100755
--- a/tools/genuuid
+++ b/tools/genuuid
@@ -1,24 +1,27 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
import re
import sys
def scan_path(path):
bug = uuid = None
- with open(path, 'r') as fd:
- for line in fd:
- match = re.match('\*', line)
- if match:
- bug = uuid = None
+ try:
+ with open(path, 'r') as fd:
+ for line in fd:
+ match = re.match('\*', line)
+ if match:
+ bug = uuid = None
- match = re.search('\[\[bug:([0-9]+)\]\[#[0-9]+\]\]', line)
- if match:
- bug = match.group(1)
- elif bug:
- match = re.search(':ID:\s+(.+?)\s*$', line)
+ match = re.search('\[\[bug:([0-9]+)\]\[#[0-9]+\]\]', line)
if match:
- uuid = match.group(1)
- print "UPDATE bugs SET cf_uuid='%s' WHERE bug_id=%s;" % (uuid, bug)
+ bug = match.group(1)
+ elif bug:
+ match = re.search(':ID:\s+(.+?)\s*$', line)
+ if match:
+ uuid = match.group(1)
+ print(f"UPDATE bugs SET cf_uuid='{uuid}' WHERE bug_id={bug};")
+ except FileNotFoundError:
+ print(f'{path}: No such file or directory')
scan_path('/Users/johnw/src/ledger/plan/TODO')
scan_path('/Users/johnw/src/ledger/plan/TODO-3.0')