Skip to content

Commit

Permalink
Improved compatibility with Gnumeric and testing for Gnumeric via ssc…
Browse files Browse the repository at this point in the history
…onvert
  • Loading branch information
mmulqueen committed Jul 28, 2015
1 parent af6639d commit b52b2a8
Show file tree
Hide file tree
Showing 7 changed files with 199 additions and 36 deletions.
5 changes: 4 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,11 @@ python:
- pypy3
before_install:
- sudo apt-get update -qq
- "export DISPLAY=:99.0"
- "sh -e /etc/init.d/xvfb start"

install:
- sudo apt-get install -qq libreoffice-calc
- sudo apt-get install -qq libreoffice-calc gnumeric
- pip install nose2
script: nose2
deploy:
Expand Down
25 changes: 1 addition & 24 deletions odswriter/__init__.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,3 @@
# The MIT License (MIT)
#
# Copyright (c) 2014 Michael Mulqueen (http://michael.mulqueen.me.uk/)
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

from __future__ import unicode_literals
from zipfile import ZipFile
import decimal
Expand Down Expand Up @@ -103,7 +81,6 @@ def new_sheet(self, name=None):
"""
return Sheet(self.dom, name)


class Sheet(object):
def __init__(self, dom, name="Sheet 1"):
self.dom = dom
Expand Down Expand Up @@ -175,7 +152,7 @@ def writerows(self, rows):
self.writerow(row)


def writer(odsfile):
def writer(odsfile, *args, **kwargs):
"""
Returns an ODSWriter object.
Expand Down
11 changes: 10 additions & 1 deletion odswriter/formula.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

import re

class Formula(object):
"""
Expand All @@ -36,4 +37,12 @@ def __init__(self, s):
self.formula_string = s

def __str__(self):
return "of:={}".format(self.formula_string)
s = self.formula_string
# Remove = sign if present
if s.startswith("="):
s = s[1:]
# Wrap cell refs in square brackets.
s = re.sub(r"([A-Z]+[0-9]+(:[A-Z]+[0-9]+)?)", r"[\1]", s)
# Place a . before cell references, so for example . A2 becomes .A2
s = re.sub(r"([A-Z]+[0-9]+)(?!\()", r".\1", s)
return "of:={}".format(s)
25 changes: 17 additions & 8 deletions odswriter/ods_components.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,14 @@
<style:style style:name="ta1" style:family="table">
<style:table-properties table:display="true" style:writing-mode="lr-tb"/>
</style:style>
<style:style style:name="odswriterTime" style:family="table-cell" style:data-style-name="odswriterStyleXMLTime"/>
<number:date-style style:name="DateISO" number:automatic-order="true">
<number:year />
<number:text>-</number:text>
<number:month number:style="long" />
<number:text>-</number:text>
<number:day number:style="long" />
</number:date-style>
<number:date-style style:name="Time" number:automatic-order="true">
<number:hour number:style="long" />
<number:text>:</number:text>
<number:minute number:style="long" />
<number:text>:</number:text>
<number:second number:style="long" />
</number:date-style>
<number:boolean-style style:name="Bool">
<number:boolean />
</number:boolean-style>
Expand Down Expand Up @@ -62,4 +56,19 @@
"""

styles_xml = """<?xml version="1.0" encoding="UTF-8"?>
<office:document-styles xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" office:version="1.2" />"""
<office:document-styles
xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0"
xmlns:number="urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0"
office:version="1.2">
<office:styles>
<number:time-style style:name="odswriterStyleXMLTime"
number:format-source="fixed">
<number:hours number:style="long"/>
<number:text>:</number:text>
<number:minutes number:style="long"/>
<number:text>:</number:text>
<number:seconds number:style="long"/>
</number:time-style>
</office:styles>
</office:document-styles>
"""
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from distutils.core import setup

setup(name="odswriter",
version="0.2.0",
version="0.3.0",
description="A pure-Python module for writing OpenDocument spreadsheets (similar to csv.writer).",
author="Michael Mulqueen",
author_email="[email protected]",
Expand Down
165 changes: 165 additions & 0 deletions tests/test_via_gnumeric.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@

# The MIT License (MIT)
#
# Copyright (c) 2014 Michael Mulqueen (http://michael.mulqueen.me.uk/)
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

from unittest import TestCase

import tempfile
import os
import subprocess
import csv
import shutil

import decimal
import datetime

import odswriter as ods


class TempDir(object):
"""
A simple context manager for temporary directories.
"""

def __init__(self):
self.path = tempfile.mkdtemp()

def __enter__(self):
return self

def __exit__(self, *args, **kwargs):
shutil.rmtree(self.path)


def command_is_executable(args):
try:
subprocess.call(args)
return True
except OSError:
return False


def launder_through_gnumeric(rows):
"""
Saves rows into an ods, uses ssconvert (based on gnumeric) to convert to a CSV and loads
the rows from that CSV.
"""
with TempDir() as d:
# Make an ODS
temp_ods = os.path.join(d.path, "test.ods")
temp_csv = os.path.join(d.path, "test.csv")
with ods.writer(open(temp_ods, "wb")) as odsfile:
odsfile.writerows(rows)

# Convert it to a CSV
p = subprocess.Popen(["ssconvert", temp_ods, temp_csv])

p.wait()

# Read the CSV
csvfile = csv.reader(open(temp_csv))
return list(csvfile)


if command_is_executable(["ssconvert", "--version"]):
class TestViaGnumeric(TestCase):
def test_string(self):
lrows = launder_through_gnumeric([["String", "ABCDEF123456", "123456"]])
self.assertEqual(lrows,[["String", "ABCDEF123456", "123456"]])

def test_numeric(self):
lrows = launder_through_gnumeric([["Float",
1,
123,
123.123,
decimal.Decimal("10.321")]])
self.assertEqual(lrows,
[["Float",
"1",
"123",
"123.123",
"10.321"]])
def test_datetime(self):
lrows = launder_through_gnumeric([["Date/DateTime",
datetime.date(1989,11,9)]])

# Locales may effect how LibreOffice outputs the dates, so I'll
# settle for checking for the presence of substrings.

self.assertEqual(lrows[0][0],"Date/DateTime")
self.assertIn("1989", lrows[0][1])
self.assertIn("11", lrows[0][1])
self.assertIn("09", lrows[0][1])

def test_time(self):
lrows = launder_through_gnumeric([["Time",
datetime.time(13,37),
datetime.time(16,17,18)]])

# Again locales may be important.

self.assertEqual(lrows[0][0],"Time")
self.assertTrue("13" in lrows[0][1] or "1:" in lrows[0][1])
self.assertIn("37",lrows[0][1])
self.assertNotIn("AM", lrows[0][1])
self.assertTrue("16" in lrows[0][2] or "4:" in lrows[0][2])
self.assertNotIn("AM", lrows[0][2])
self.assertIn("17",lrows[0][2])
self.assertIn("18",lrows[0][2])

def test_bool(self):
lrows = launder_through_gnumeric([["Bool",True,False,True]])

self.assertEqual(lrows,[["Bool", "TRUE", "FALSE", "TRUE"]])

def test_formula(self):
lrows = launder_through_gnumeric([["Formula", # A1
1, # B1
2, # C1
3, # D1
ods.Formula("IF(C1=2;B1;C1)"), # E1
ods.Formula("SUM(B1:D1)")]]) # F1
self.assertEqual(lrows, [["Formula","1","2","3","1","6"]])

def test_nested_formula(self):
lrows = launder_through_gnumeric([["Formula", # A1
1, # B1
2, # C1
3, # D1
ods.Formula("IF(C1=2;MIN(B1:D1);MAX(B1:D1))"), # E1
ods.Formula("IF(C1=3;MIN(B1:D1);MAX(B1:D1))")]]) # F1
self.assertEqual(lrows, [["Formula","1","2","3","1","3"]])

def test_escape(self):
"""
Make sure that special characters are actually being escaped.
"""
lrows = launder_through_gnumeric([["<table:table-cell>",
"</table:table-cell>",
"<br />",
"&",
"&amp;"]])
self.assertEqual(lrows, [["<table:table-cell>",
"</table:table-cell>",
"<br />",
"&",
"&amp;"]])
2 changes: 1 addition & 1 deletion tests/test_via_libreoffice.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@

# The MIT License (MIT)
#
# Copyright (c) 2014 Michael Mulqueen (http://michael.mulqueen.me.uk/)
# Copyright (c) 2015 Michael Mulqueen (http://michael.mulqueen.me.uk/)
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
Expand Down

0 comments on commit b52b2a8

Please sign in to comment.