diff --git a/.travis.yml b/.travis.yml index 75674b6..e2a9073 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,7 +16,7 @@ script: # pep8 - pep8 . # Examples - - (cd "Examples/Replicate Workbook" && python replicateWorkbook.py) - - (cd "Examples/List TDS Info" && python listTDSInfo.py) - - (cd "Examples/GetFields" && python show_fields.py) + - (cd "samples/replicate-workbook" && python replicate_workbook.py) + - (cd "samples/list-tds-info" && python list_tds_info.py) + - (cd "samples/show-fields" && python show_fields.py) diff --git a/CHANGELOG.md b/CHANGELOG.md index 33b164c..7ba40cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## 0.3 (31 August 2016) + +* Added basic connection class retargeting (#65) +* Added ability to create a new connection (#69) +* Added description to the field object (#73) +* Improved Test Coverage (#62, #67) + ## 0.2 (22 July 2016) * Added support for loading twbx and tdsx files (#43, #44) diff --git a/Examples/GetFields/World.tds b/Examples/GetFields/World.tds deleted file mode 120000 index 397f696..0000000 --- a/Examples/GetFields/World.tds +++ /dev/null @@ -1 +0,0 @@ -../List TDS Info/World.tds \ No newline at end of file diff --git a/README.md b/README.md index a6534d2..588fb14 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ Features include: - Database Name - Getting Field information from data sources and workbooks - Get all fields in a data source - - Get all feilds in use by certain sheets in a workbook + - Get all fields in use by certain sheets in a workbook We don't yet support creating files from scratch, adding extracts into workbooks or data sources, or updating field information @@ -45,7 +45,7 @@ Download the `.zip` file that contains the SDK. Unzip the file and then run the pip install -e ``` -#### Installing the Development Version From Git +#### Installing the Development Version from Git *Only do this if you know you want the development version, no guarantee that we won't break APIs during development* @@ -74,7 +74,7 @@ sourceWB.datasources[0].connections[0].username = "benl" sourceWB.save() ``` -With Data Integration in Tableau 10, a data source can have multiple connections. To access the connections simply index them like you would datasources +With Data Integration in Tableau 10, a data source can have multiple connections. To access the connections simply index them like you would datasources. ```python from tableaudocumentapi import Workbook @@ -104,6 +104,6 @@ sourceWB.save() -###Examples +###[Examples](Examples) -The downloadable package contains several example scripts that show more detailed usage of the Document API +The downloadable package contains several example scripts that show more detailed usage of the Document API. diff --git a/Examples/List TDS Info/listTDSInfo.py b/samples/list-tds-info/list_tds_info.py similarity index 94% rename from Examples/List TDS Info/listTDSInfo.py rename to samples/list-tds-info/list_tds_info.py index 0c73d3a..129f85c 100644 --- a/Examples/List TDS Info/listTDSInfo.py +++ b/samples/list-tds-info/list_tds_info.py @@ -6,7 +6,7 @@ ############################################################ # Step 2) Open the .tds we want to replicate ############################################################ -sourceTDS = Datasource.from_file('World.tds') +sourceTDS = Datasource.from_file('world.tds') ############################################################ # Step 3) List out info from the TDS diff --git a/Examples/List TDS Info/World.tds b/samples/list-tds-info/world.tds similarity index 100% rename from Examples/List TDS Info/World.tds rename to samples/list-tds-info/world.tds diff --git a/Examples/Replicate Workbook/databases.csv b/samples/replicate-workbook/databases.csv similarity index 100% rename from Examples/Replicate Workbook/databases.csv rename to samples/replicate-workbook/databases.csv diff --git a/Examples/Replicate Workbook/replicateWorkbook.py b/samples/replicate-workbook/replicate_workbook.py similarity index 96% rename from Examples/Replicate Workbook/replicateWorkbook.py rename to samples/replicate-workbook/replicate_workbook.py index 4b555e6..65281cb 100644 --- a/Examples/Replicate Workbook/replicateWorkbook.py +++ b/samples/replicate-workbook/replicate_workbook.py @@ -8,7 +8,7 @@ ############################################################ # Step 2) Open the .twb we want to replicate ############################################################ -sourceWB = Workbook('Sample - Superstore.twb') +sourceWB = Workbook('sample-superstore.twb') ############################################################ # Step 3) Use a database list (in CSV), loop thru and diff --git a/Examples/Replicate Workbook/Sample - Superstore.twb b/samples/replicate-workbook/sample-superstore.twb similarity index 100% rename from Examples/Replicate Workbook/Sample - Superstore.twb rename to samples/replicate-workbook/sample-superstore.twb diff --git a/Examples/GetFields/show_fields.py b/samples/show-fields/show_fields.py similarity index 90% rename from Examples/GetFields/show_fields.py rename to samples/show-fields/show_fields.py index ee45f87..84cdd44 100644 --- a/Examples/GetFields/show_fields.py +++ b/samples/show-fields/show_fields.py @@ -6,7 +6,7 @@ ############################################################ # Step 2) Open the .tds we want to inspect ############################################################ -sourceTDS = Datasource.from_file('World.tds') +sourceTDS = Datasource.from_file('world.tds') ############################################################ # Step 3) Print out all of the fields and what type they are @@ -23,6 +23,8 @@ if field.default_aggregation: print(' the default aggregation is {}'.format(field.default_aggregation)) blank_line = True + if field.description: + print(' the description is {}'.format(field.description)) if blank_line: print('') diff --git a/samples/show-fields/world.tds b/samples/show-fields/world.tds new file mode 120000 index 0000000..5e73bb3 --- /dev/null +++ b/samples/show-fields/world.tds @@ -0,0 +1 @@ +../list-tds-info/world.tds \ No newline at end of file diff --git a/setup.py b/setup.py index e047b34..27d6a73 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( name='tableaudocumentapi', - version='0.2', + version='0.3', author='Tableau Software', author_email='github@tableau.com', url='https://github.com/tableau/document-api-python', diff --git a/tableaudocumentapi/connection.py b/tableaudocumentapi/connection.py index 3e64c1b..8e9eb58 100644 --- a/tableaudocumentapi/connection.py +++ b/tableaudocumentapi/connection.py @@ -3,6 +3,8 @@ # Connection - A class for writing connections to Tableau files # ############################################################################### +import xml.etree.ElementTree as ET +from tableaudocumentapi.dbclass import is_valid_dbclass class Connection(object): @@ -32,6 +34,17 @@ def __init__(self, connxml): def __repr__(self): return "''".format(self._server, self._dbname, hex(id(self))) + @classmethod + def from_attributes(cls, server, dbname, username, dbclass, authentication=''): + root = ET.Element('connection', authentication=authentication) + xml = cls(root) + xml.server = server + xml.dbname = dbname + xml.username = username + xml.dbclass = dbclass + + return xml + ########### # dbname ########### @@ -111,3 +124,12 @@ def authentication(self): @property def dbclass(self): return self._class + + @dbclass.setter + def dbclass(self, value): + + if not is_valid_dbclass(value): + raise AttributeError("'{}' is not a valid database type".format(value)) + + self._class = value + self._connectionXML.set('class', value) diff --git a/tableaudocumentapi/datasource.py b/tableaudocumentapi/datasource.py index 3f64145..dd3d5c5 100644 --- a/tableaudocumentapi/datasource.py +++ b/tableaudocumentapi/datasource.py @@ -5,8 +5,10 @@ ############################################################################### import collections import itertools +import random import xml.etree.ElementTree as ET import xml.sax.saxutils as sax +from uuid import uuid4 from tableaudocumentapi import Connection, xfile from tableaudocumentapi import Field @@ -19,7 +21,7 @@ # dropped, remove this and change the basestring references below to str try: basestring -except NameError: +except NameError: # pragma: no cover basestring = str ######## @@ -38,6 +40,7 @@ def _is_used_by_worksheet(names, field): class FieldDictionary(MultiLookupDict): + def used_by_sheet(self, name): # If we pass in a string, no need to get complicated, just check to see if name is in # the field's list of worksheets @@ -63,7 +66,36 @@ def _column_object_from_metadata_xml(metadata_xml): return _ColumnObjectReturnTuple(field_object.id, field_object) +def base36encode(number): + """Converts an integer into a base36 string.""" + + ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyz" + + base36 = '' + sign = '' + + if number < 0: + sign = '-' + number = -number + + if 0 <= number < len(ALPHABET): + return sign + ALPHABET[number] + + while number != 0: + number, i = divmod(number, len(ALPHABET)) + base36 = ALPHABET[i] + base36 + + return sign + base36 + + +def make_unique_name(dbclass): + rand_part = base36encode(uuid4().int) + name = dbclass + '.' + rand_part + return name + + class ConnectionParser(object): + def __init__(self, datasource_xml, version): self._dsxml = datasource_xml self._dsversion = version @@ -113,9 +145,23 @@ def __init__(self, dsxml, filename=None): def from_file(cls, filename): """Initialize datasource from file (.tds)""" - dsxml = xml_open(filename).getroot() + dsxml = xml_open(filename, cls.__name__.lower()).getroot() return cls(dsxml, filename) + @classmethod + def from_connections(cls, caption, connections): + root = ET.Element('datasource', caption=caption, version='10.0', inline='true') + outer_connection = ET.SubElement(root, 'connection') + outer_connection.set('class', 'federated') + named_conns = ET.SubElement(outer_connection, 'named-connections') + for conn in connections: + nc = ET.SubElement(named_conns, + 'named-connection', + name=make_unique_name(conn.dbclass), + caption=conn.server) + nc.append(conn._connectionXML) + return cls(root) + def save(self): """ Call finalization code and save file. @@ -143,6 +189,7 @@ def save_as(self, new_filename): Nothing. """ + xfile._save_file(self._filename, self._datasourceTree, new_filename) ########### diff --git a/tableaudocumentapi/dbclass.py b/tableaudocumentapi/dbclass.py new file mode 100644 index 0000000..b466452 --- /dev/null +++ b/tableaudocumentapi/dbclass.py @@ -0,0 +1,60 @@ + + +KNOWN_DB_CLASSES = ('msaccess', + 'msolap', + 'bigquery', + 'asterncluster', + 'bigsql', + 'aurora', + 'awshadoophive', + 'dataengine', + 'DataStax', + 'db2', + 'essbase', + 'exasolution', + 'excel', + 'excel-direct', + 'excel-reader', + 'firebird', + 'powerpivot', + 'genericodbc', + 'google-analytics', + 'googlecloudsql', + 'google-sheets', + 'greenplum', + 'saphana', + 'hadoophive', + 'hortonworkshadoophive', + 'maprhadoophive', + 'marklogic', + 'memsql', + 'mysql', + 'netezza', + 'oracle', + 'paraccel', + 'postgres', + 'progressopenedge', + 'redshift', + 'snowflake', + 'spark', + 'splunk', + 'kognitio', + 'sqlserver', + 'salesforce', + 'sapbw', + 'sybasease', + 'sybaseiq', + 'tbio', + 'teradata', + 'vectorwise', + 'vertica', + 'denormalized-cube', + 'csv', + 'textscan', + 'webdata', + 'webdata-direct', + 'cubeextract') + + +def is_valid_dbclass(dbclass): + return dbclass in KNOWN_DB_CLASSES diff --git a/tableaudocumentapi/field.py b/tableaudocumentapi/field.py index 4af648f..63cc72c 100644 --- a/tableaudocumentapi/field.py +++ b/tableaudocumentapi/field.py @@ -1,4 +1,6 @@ import functools +import xml.etree.ElementTree as ET + _ATTRIBUTES = [ 'id', # Name of the field as specified in the file, usually surrounded by [ ] @@ -8,6 +10,7 @@ 'type', # three possible values: quantitative, ordinal, or nominal 'alias', # Name of the field as displayed in Tableau if the default name isn't wanted 'calculation', # If this field is a calculated field, this will be the formula + 'description', # If this field has a description, this will be the description (including formatting tags) ] _METADATA_ATTRIBUTES = [ @@ -42,8 +45,10 @@ def __init__(self, column_xml=None, metadata_xml=None): if column_xml is not None: self._initialize_from_column_xml(column_xml) - if metadata_xml is not None: - self.apply_metadata(metadata_xml) + # This isn't currently never called because of the way we get the data from the xml, + # but during the refactor, we might need it. This is commented out as a reminder + # if metadata_xml is not None: + # self.apply_metadata(metadata_xml) elif metadata_xml is not None: self._initialize_from_metadata_xml(metadata_xml) @@ -162,6 +167,11 @@ def default_aggregation(self): """ The default type of aggregation on the field (e.g Sum, Avg)""" return self._aggregation + @property + def description(self): + """ The contents of the tag on a field """ + return self._description + @property def worksheets(self): return list(self._worksheets) @@ -182,3 +192,11 @@ def _read_calculation(xmldata): return None return calc.attrib.get('formula', None) + + @staticmethod + def _read_description(xmldata): + description = xmldata.find('.//desc') + if description is None: + return None + + return u'{}'.format(ET.tostring(description, encoding='utf-8')) # This is necessary for py3 support diff --git a/tableaudocumentapi/multilookup_dict.py b/tableaudocumentapi/multilookup_dict.py index 64b742a..732b7da 100644 --- a/tableaudocumentapi/multilookup_dict.py +++ b/tableaudocumentapi/multilookup_dict.py @@ -13,6 +13,7 @@ def _resolve_value(key, value): if retval is None: retval = getattr(value, key, None) except AttributeError: + # We should never hit this. retval = None return retval @@ -39,15 +40,18 @@ def _populate_indexes(self): self._indexes['alias'] = _build_index('alias', self) self._indexes['caption'] = _build_index('caption', self) + def _get_real_key(self, key): + if key in self._indexes['alias']: + return self._indexes['alias'][key] + if key in self._indexes['caption']: + return self._indexes['caption'][key] + + return key + def __setitem__(self, key, value): - alias = _resolve_value('alias', value) - caption = _resolve_value('caption', value) - if alias is not None: - self._indexes['alias'][alias] = key - if caption is not None: - self._indexes['caption'][caption] = key + real_key = self._get_real_key(key) - dict.__setitem__(self, key, value) + dict.__setitem__(self, real_key, value) def get(self, key, default_value=_no_default_value): try: @@ -58,9 +62,5 @@ def get(self, key, default_value=_no_default_value): raise def __getitem__(self, key): - if key in self._indexes['alias']: - key = self._indexes['alias'][key] - elif key in self._indexes['caption']: - key = self._indexes['caption'][key] - - return dict.__getitem__(self, key) + real_key = self._get_real_key(key) + return dict.__getitem__(self, real_key) diff --git a/tableaudocumentapi/workbook.py b/tableaudocumentapi/workbook.py index 28ddd03..1359356 100644 --- a/tableaudocumentapi/workbook.py +++ b/tableaudocumentapi/workbook.py @@ -32,7 +32,7 @@ def __init__(self, filename): self._filename = filename - self._workbookTree = xml_open(self._filename) + self._workbookTree = xml_open(self._filename, self.__class__.__name__.lower()) self._workbookRoot = self._workbookTree.getroot() # prepare our datasource objects diff --git a/tableaudocumentapi/xfile.py b/tableaudocumentapi/xfile.py index a0cd62e..66e5aac 100644 --- a/tableaudocumentapi/xfile.py +++ b/tableaudocumentapi/xfile.py @@ -17,15 +17,28 @@ class TableauVersionNotSupportedException(Exception): pass -def xml_open(filename): - # Determine if this is a twb or twbx and get the xml root +class TableauInvalidFileException(Exception): + pass + + +def xml_open(filename, expected_root=None): + if zipfile.is_zipfile(filename): tree = get_xml_from_archive(filename) else: tree = ET.parse(filename) - file_version = Version(tree.getroot().attrib.get('version', '0.0')) + + tree_root = tree.getroot() + + file_version = Version(tree_root.attrib.get('version', '0.0')) + if file_version < MIN_SUPPORTED_VERSION: raise TableauVersionNotSupportedException(file_version) + + if expected_root and (expected_root != tree_root.tag): + raise TableauInvalidFileException( + "'{}'' is not a valid '{}' file".format(filename, expected_root)) + return tree @@ -40,14 +53,13 @@ def temporary_directory(*args, **kwargs): def find_file_in_zip(zip_file): for filename in zip_file.namelist(): - try: - with zip_file.open(filename) as xml_candidate: - ET.parse(xml_candidate).getroot().tag in ( - 'workbook', 'datasource') + with zip_file.open(filename) as xml_candidate: + try: + ET.parse(xml_candidate) return filename - except ET.ParseError: - # That's not an XML file by gosh - pass + except ET.ParseError: + # That's not an XML file by gosh + pass def get_xml_from_archive(filename): @@ -92,6 +104,10 @@ def save_into_archive(xml_tree, filename, new_filename=None): def _save_file(container_file, xml_tree, new_filename=None): + + if container_file is None: + container_file = new_filename + if zipfile.is_zipfile(container_file): save_into_archive(xml_tree, container_file, new_filename) else: diff --git a/test/assets/BadZip.zip b/test/assets/BadZip.zip new file mode 100644 index 0000000..9eca08f Binary files /dev/null and b/test/assets/BadZip.zip differ diff --git a/test/assets/TABLEAU_82_TWB.twb b/test/assets/TABLEAU_82_TWB.twb new file mode 100644 index 0000000..2ab236d --- /dev/null +++ b/test/assets/TABLEAU_82_TWB.twb @@ -0,0 +1 @@ + diff --git a/test/assets/datasource_test.tds b/test/assets/datasource_test.tds index a1e78a8..bfab77b 100644 --- a/test/assets/datasource_test.tds +++ b/test/assets/datasource_test.tds @@ -75,7 +75,14 @@ - + + + + A thing + Something will go here too, in a muted gray + + + diff --git a/test/bvt.py b/test/bvt.py index 6a7cdf8..4ea3d11 100644 --- a/test/bvt.py +++ b/test/bvt.py @@ -4,9 +4,12 @@ import xml.etree.ElementTree as ET from tableaudocumentapi import Workbook, Datasource, Connection, ConnectionParser +from tableaudocumentapi.xfile import TableauInvalidFileException, TableauVersionNotSupportedException TEST_DIR = os.path.dirname(__file__) +TABLEAU_82_TWB = os.path.join(TEST_DIR, 'assets', 'TABLEAU_82_TWB.twb') + TABLEAU_93_TWB = os.path.join(TEST_DIR, 'assets', 'TABLEAU_93_TWB.twb') TABLEAU_93_TDS = os.path.join(TEST_DIR, 'assets', 'TABLEAU_93_TDS.tds') @@ -64,6 +67,32 @@ def test_can_write_attributes_to_connection(self): self.assertEqual(conn.username, 'bob') self.assertEqual(conn.server, 'mssql2014.test.tsi.lan') + def test_bad_dbclass_rasies_attribute_error(self): + conn = Connection(self.connection) + conn.dbclass = 'sqlserver' + self.assertEqual(conn.dbclass, 'sqlserver') + with self.assertRaises(AttributeError): + conn.dbclass = 'NotReal' + + def test_can_create_connection_from_scratch(self): + conn = Connection.from_attributes( + server='a', dbname='b', username='c', dbclass='mysql', authentication='d') + self.assertEqual(conn.server, 'a') + self.assertEqual(conn.dbname, 'b') + self.assertEqual(conn.username, 'c') + self.assertEqual(conn.dbclass, 'mysql') + self.assertEqual(conn.authentication, 'd') + + def test_can_create_datasource_from_connections(self): + conn1 = Connection.from_attributes( + server='a', dbname='b', username='c', dbclass='mysql', authentication='d') + conn2 = Connection.from_attributes( + server='1', dbname='2', username='3', dbclass='mysql', authentication='7') + ds = Datasource.from_connections('test', connections=[conn1, conn2]) + + self.assertEqual(ds.connections[0].server, 'a') + self.assertEqual(ds.connections[1].server, '1') + class DatasourceModelTests(unittest.TestCase): @@ -191,6 +220,14 @@ def test_can_extract_datasource(self): self.assertEqual(wb.datasources[0].name, 'sqlserver.17u3bqc16tjtxn14e2hxh19tyvpo') + def test_can_get_worksheets(self): + wb = Workbook(self.workbook_file.name) + self.assertIsNotNone(wb.worksheets) + + def test_has_filename(self): + wb = Workbook(self.workbook_file.name) + self.assertEqual(wb.filename, self.workbook_file.name) + def test_can_update_datasource_connection_and_save(self): original_wb = Workbook(self.workbook_file.name) original_wb.datasources[0].connections[0].dbname = 'newdb.test.tsi.lan' @@ -282,10 +319,28 @@ def test_can_open_twbx_and_save_as_changes(self): class EmptyWorkbookWillLoad(unittest.TestCase): + def test_no_exceptions_thrown(self): wb = Workbook(EMPTY_WORKBOOK) self.assertIsNotNone(wb) +class LoadOnlyValidFileTypes(unittest.TestCase): + + def test_exception_when_workbook_given_tdsx(self): + with self.assertRaises(TableauInvalidFileException): + wb = Workbook(TABLEAU_10_TDSX) + + def test_exception_when_datasource_given_twbx(self): + with self.assertRaises(TableauInvalidFileException): + ds = Datasource.from_file(TABLEAU_10_TWBX) + + +class SupportedWorkbookVersions(unittest.TestCase): + + def test_82_workbook_throws_exception(self): + with self.assertRaises(TableauVersionNotSupportedException): + wb = Workbook(TABLEAU_82_TWB) + if __name__ == '__main__': unittest.main() diff --git a/test/test_datasource.py b/test/test_datasource.py index bf51746..66b3f79 100644 --- a/test/test_datasource.py +++ b/test/test_datasource.py @@ -19,6 +19,7 @@ class DataSourceFieldsTDS(unittest.TestCase): + def setUp(self): self.ds = Datasource.from_file(TEST_TDS_FILE) @@ -51,11 +52,24 @@ def test_datasource_field_is_quantitative(self): def test_datasource_field_is_ordinal(self): self.assertTrue(self.ds.fields['[x]'].is_ordinal) + def test_datasource_field_datatype(self): + self.assertEqual(self.ds.fields['[x]'].datatype, 'integer') + + def test_datasource_field_role(self): + self.assertEqual(self.ds.fields['[x]'].role, 'measure') + + def test_datasource_field_description(self): + actual = self.ds.fields['[a]'].description + self.assertIsNotNone(actual) + self.assertTrue(u'muted gray' in actual) + class DataSourceFieldsTWB(unittest.TestCase): + def setUp(self): self.wb = Workbook(TEST_TWB_FILE) - self.ds = self.wb.datasources[0] # Assume the first datasource in the file + # Assume the first datasource in the file + self.ds = self.wb.datasources[0] def test_datasource_fields_loaded_in_workbook(self): self.assertIsNotNone(self.ds.fields) @@ -63,9 +77,11 @@ def test_datasource_fields_loaded_in_workbook(self): class DataSourceFieldsFoundIn(unittest.TestCase): + def setUp(self): self.wb = Workbook(TEST_TWB_FILE) - self.ds = self.wb.datasources[0] # Assume the first datasource in the file + # Assume the first datasource in the file + self.ds = self.wb.datasources[0] def test_datasource_fields_found_in_returns_fields(self): actual_values = self.ds.fields.used_by_sheet('Sheet 1') diff --git a/test/test_field.py b/test/test_field.py new file mode 100644 index 0000000..7cbe885 --- /dev/null +++ b/test/test_field.py @@ -0,0 +1,29 @@ +import unittest +import os.path + +from tableaudocumentapi import Datasource, Field +from tableaudocumentapi.field import _find_metadata_record + +TEST_ASSET_DIR = os.path.join( + os.path.dirname(__file__), + 'assets' +) +TEST_TDS_FILE = os.path.join( + TEST_ASSET_DIR, + 'datasource_test.tds' +) + + +class FieldsUnitTest(unittest.TestCase): + def test_field_throws_if_no_data_passed_in(self): + with self.assertRaises(AttributeError): + Field() + + +class FindMetaDataRecordEdgeTest(unittest.TestCase): + class MockXmlWithNoFind(object): + def find(self, *args, **kwargs): + return None + + def test_find_metadata_record_returns_none(self): + self.assertIsNone(_find_metadata_record(self.MockXmlWithNoFind(), 'foo')) diff --git a/test/test_multidict.py b/test/test_multidict.py index 0a78e9d..21ecdc6 100644 --- a/test/test_multidict.py +++ b/test/test_multidict.py @@ -22,6 +22,10 @@ def setUp(self): } }) + def test_multilookupdict_can_be_empty(self): + mld = MultiLookupDict() + self.assertIsNotNone(mld) + def test_multilookupdict_name_only(self): actual = self.mld['[baz]'] self.assertEqual(3, actual['value']) @@ -61,3 +65,16 @@ def test_multilookupdict_get_returns_default_value(self): def test_multilookupdict_get_returns_value(self): actual = self.mld.get('baz') self.assertEqual(1, actual['value']) + + def test_multilookupdict_can_set_item(self): + before = self.mld['baz'] + self.mld['baz'] = 4 + self.assertEqual(4, self.mld['baz']) + + def test_multilookupdict_can_set_new_item(self): + self.mld['wakka'] = 1 + self.assertEqual(1, self.mld['wakka']) + + def test_multilookupdict_can_set_with_alias(self): + self.mld['bar'] = 2 + self.assertEqual(2, self.mld['[foo]']) diff --git a/test/test_xfile.py b/test/test_xfile.py new file mode 100644 index 0000000..6cbe67f --- /dev/null +++ b/test/test_xfile.py @@ -0,0 +1,19 @@ +import os.path +import unittest +import zipfile +from tableaudocumentapi.xfile import find_file_in_zip + +TEST_ASSET_DIR = os.path.join( + os.path.dirname(__file__), + 'assets' +) +BAD_ZIP_FILE = os.path.join( + TEST_ASSET_DIR, + 'BadZip.zip' +) + + +class XFileEdgeTests(unittest.TestCase): + def test_find_file_in_zip_no_xml_file(self): + badzip = zipfile.ZipFile(BAD_ZIP_FILE) + self.assertIsNone(find_file_in_zip(badzip)) pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy