diff --git a/CSVLibrary/__init__.py b/CSVLibrary/__init__.py index 297135c..97075a0 100644 --- a/CSVLibrary/__init__.py +++ b/CSVLibrary/__init__.py @@ -1,6 +1,12 @@ import csv from robot.api import logger from .version import VERSION +import sys + +if sys.version_info.major >= 3: + from io import StringIO as IO +else: + from io import BytesIO as IO __version__ = VERSION @@ -11,36 +17,47 @@ class CSVLibrary(object): ROBOT_LIBRARY_SCOPE = 'GLOBAL' @staticmethod - def _open_csv_file_for_read(filename, csv_reader=csv.reader, line_numbers=None, **kwargs): + def _reader(to_read, csv_reader=csv.reader, line_numbers=None, **kwargs): + reader = csv_reader(to_read, **kwargs) + try: + for line_number, row in enumerate(reader): + if line_numbers is None: + yield row + elif isinstance(line_numbers, list) and line_number in line_numbers: + yield row + line_numbers.remove(line_number) + if len(line_numbers) == 0: + break + except csv.Error as e: + logger.error('line %d: %s' % (reader.line_num, e)) + + def _read_csv(self, csv_handler, csv_reader=csv.reader, line_numbers=None, **kwargs): if line_numbers is not None and isinstance(line_numbers, list): - line_numbers = map(int, line_numbers) + line_numbers = list(map(int, line_numbers)) + + return [row for row in self._reader(csv_handler, csv_reader, line_numbers, **kwargs)] + + def _open_csv_file_for_read(self, filename, csv_reader=csv.reader, line_numbers=None, **kwargs): with open(filename, 'r') as csv_handler: - reader = csv_reader(csv_handler, **kwargs) - try: - for line_number, row in enumerate(reader): - if line_numbers is None: - yield row - elif isinstance(line_numbers, list): - if line_number in line_numbers: - yield row - line_numbers.remove(line_number) - if len(line_numbers) == 0: - break - except csv.Error as e: - logger.error('file %s, line %d: %s' % (filename, reader.line_num, e)) + return self._read_csv(csv_handler, csv_reader, line_numbers, **kwargs) @staticmethod - def _open_csv_file_for_write(filename, data, csv_writer=csv.writer, **kwargs): - with open(filename, 'ab') as csv_handler: - writer = csv_writer(csv_handler, **kwargs) - try: - if isinstance(writer, csv.DictWriter) and 'fieldnames' in kwargs.keys(): - csv_handler.truncate() - writer.writeheader() - - writer.writerows(data) - except csv.Error as e: - logger.error('file %s, line %d: %s' % (filename, writer.line_num, e)) + def _write_csv(csv_handler, data, csv_writer=csv.writer, **kwargs): + if 'fieldnames' not in kwargs.keys() and isinstance(data[0], dict): + kwargs['fieldnames'] = data[0].keys() + + writer = csv_writer(csv_handler, **kwargs) + try: + if isinstance(writer, csv.DictWriter) and csv_handler.tell() == 0: + writer.writeheader() + + writer.writerows(data) + except csv.Error as e: + logger.error('%s' % e) + + def _open_csv_file_for_write(self, filename, data, csv_writer=csv.writer, **kwargs): + with open(filename, 'a') as csv_handler: + self._write_csv(csv_handler, data, csv_writer, **kwargs) @staticmethod def empty_csv_file(filename): @@ -69,7 +86,31 @@ def read_csv_file_to_list(self, filename, delimiter=',', **kwargs): delimiter=str(delimiter), **kwargs ) - return [tuple(row) for row in csv_list] + return csv_list + + def read_csv_string_to_list(self, csv_string, delimiter=',', **kwargs): + """Read CSV string and return its content as a Python list of tuples. + + - ``csv_string``: name of csv file + - ``delimiter``: Default: `,` + - ``line_numbers``: List of linenumbers to read. Default None + - ``quoting`` (int): + _0_: QUOTE_MINIMAL + _1_: QUOTE_ALL + _2_: QUOTE_NONNUMERIC + _3_: QUOTE_NONE + """ + if sys.version_info.major < 3: + csv_string = csv_string.encode("utf-8") + + with IO(csv_string) as csv_handler: + csv_list = self._read_csv( + csv_handler, + csv_reader=csv.reader, + delimiter=str(delimiter), + **kwargs + ) + return csv_list def read_csv_file_to_associative(self, filename, delimiter=',', fieldnames=None, **kwargs): """Read CSV file and return its content as a Python list of dictionaries. @@ -84,14 +125,40 @@ def read_csv_file_to_associative(self, filename, delimiter=',', fieldnames=None, _2_: QUOTE_NONNUMERIC _3_: QUOTE_NONE """ + kwargs['fieldnames'] = fieldnames csv_dict = self._open_csv_file_for_read( filename, csv_reader=csv.DictReader, delimiter=str(delimiter), - fieldnames=fieldnames, **kwargs ) - return [item for item in csv_dict] + return csv_dict + + def read_csv_string_to_associative(self, csv_string, delimiter=',', fieldnames=None, **kwargs): + """Read CSV from string and return its content as a Python list of dictionaries. + + - ``csv_string``: csv formatted string + - ``delimiter``: Default: `,` + - ``fieldnames``: list of column names + - ``line_numbers``: List of linenumbers to read. Default None + - ``quoting`` (int): + _0_: QUOTE_MINIMAL + _1_: QUOTE_ALL + _2_: QUOTE_NONNUMERIC + _3_: QUOTE_NONE + """ + if sys.version_info.major < 3: + csv_string = csv_string.encode("utf-8") + + with IO(csv_string) as csv_handler: + csv_dict = self._read_csv( + csv_handler, + csv_reader=csv.DictReader, + delimiter=str(delimiter), + fieldnames=fieldnames, + **kwargs + ) + return csv_dict def append_to_csv_file(self, filename, data, **kwargs): """This keyword will append data to a new or existing CSV file. @@ -104,9 +171,56 @@ def append_to_csv_file(self, filename, data, **kwargs): _2_: QUOTE_NONNUMERIC _3_: QUOTE_NONE """ - if isinstance(data[0], dict): - data = map(lambda row: row.items(), data) - self._open_csv_file_for_write(filename, data=data, csv_writer=csv.writer, **kwargs) + + if isinstance(data, dict): + data = [data] + + fieldnames = self._open_csv_file_for_read( + filename, + csv_reader=csv.reader, + line_numbers=[0], + **kwargs + )[0] + + self._open_csv_file_for_write( + filename, + data=data, + csv_writer=csv.DictWriter, + fieldnames=fieldnames, + **kwargs + ) + + def append_to_csv_string(self, csv_string, data, **kwargs): + """This keyword will append data to a new or existing CSV string. + + - ``csv_string``: csv formatted string + - ``data``: iterable(e.g. list or tuple) data. + - ``quoting`` (int): + _0_: QUOTE_MINIMAL + _1_: QUOTE_ALL + _2_: QUOTE_NONNUMERIC + _3_: QUOTE_NONE + """ + if isinstance(data, dict): + data = [data] + + if 'lineterminator' not in kwargs.keys(): + kwargs['lineterminator'] = '\n' + + if sys.version_info.major < 3: + csv_string = csv_string.encode("utf-8") + + with IO(csv_string) as csv_handler: + fieldnames = self._read_csv( + csv_handler, + csv_reader=csv.reader, + **kwargs + )[0] + + kwargs['fieldnames'] = fieldnames + + self._write_csv(csv_handler, data, csv_writer=csv.DictWriter, **kwargs) + return csv_handler.getvalue() def csv_file_from_associative(self, filename, data, fieldnames=None, delimiter=',', **kwargs): """This keyword will create new file @@ -121,12 +235,36 @@ def csv_file_from_associative(self, filename, data, fieldnames=None, delimiter=' _2_: QUOTE_NONNUMERIC _3_: QUOTE_NONE """ - fieldnames = fieldnames or data[0].keys() + kwargs['fieldnames'] = fieldnames or data[0].keys() + self._open_csv_file_for_write( filename, data=data, csv_writer=csv.DictWriter, delimiter=str(delimiter), - fieldnames=fieldnames, **kwargs ) + + def csv_string_from_associative(self, data, fieldnames=None, delimiter=',', **kwargs): + """This keyword will create new file + + - ``data``: iterable(e.g. list or tuple) data. + - ``fieldnames``: list of column names + - ``delimiter``: Default: `,` + - ``quoting`` (int): + _0_: QUOTE_MINIMAL + _1_: QUOTE_ALL + _2_: QUOTE_NONNUMERIC + _3_: QUOTE_NONE + """ + if isinstance(data, dict): + data = [data] + + kwargs['fieldnames'] = fieldnames or data[0].keys() + if 'lineterminator' not in kwargs.keys(): + kwargs['lineterminator'] = '\n' + kwargs['delimiter'] = delimiter + + with IO() as csv_handler: + self._write_csv(csv_handler, data, csv_writer=csv.DictWriter, **kwargs) + return csv_handler.getvalue() diff --git a/doc/CSVLibrary.html b/doc/CSVLibrary.html index 8513f91..840e053 100644 --- a/doc/CSVLibrary.html +++ b/doc/CSVLibrary.html @@ -1,165 +1,583 @@ - + + - - + + + @@ -557,7 +1111,7 @@

Opening library documentation failed

@@ -565,26 +1119,83 @@

Opening library documentation failed

- + + + + + + + + + diff --git a/tests/ReadCSVTest.robot b/tests/ReadCSVTest.robot index 4442d7e..f3918bb 100644 --- a/tests/ReadCSVTest.robot +++ b/tests/ReadCSVTest.robot @@ -8,6 +8,10 @@ Library CSVLibrary @{template_list}= 1 Douglas Morris dmorris0@mozilla.org Male 205.4.212.229 &{template_dict}= id=1 first_name=Douglas last_name=Morris email=dmorris0@mozilla.org gender=Male ip_address=205.4.212.229 &{template_dict_quoting}= id=1 first_name=Douglas last_name=Morris email=dmorris0@mozilla.org gender="Male ip_address=205.4.212.229 +${csv_string}= SEPARATOR= +... id,first_name,last_name,email,gender,ip_address\n +... 1,Douglas,Morris,dmorris0@mozilla.org,Male,205.4.212.229\n +... 2,Stephanie,Oliver,soliver1@google.com.br,Female,18.101.154.106\n *** Test Cases *** Read csv file to a list example test @@ -21,3 +25,12 @@ Read csv file to a dict example test Read csv file without quoting to associative @{dict}= read csv file to associative ${CURDIR}${/}data_quoting.csv delimiter=, quoting=${3} dictionaries should be equal ${template_dict_quoting} ${dict[0]} + +Read csv string to a list example test + @{list}= read csv string to list ${csv_string} + lists should be equal ${template_list} ${list[1]} + +Read csv string to a dict example test + @{dict}= read csv string to associative ${csv_string} + dictionaries should be equal ${template_dict} ${dict[0]} + diff --git a/tests/WriteCSVTest.robot b/tests/WriteCSVTest.robot new file mode 100644 index 0000000..86a4068 --- /dev/null +++ b/tests/WriteCSVTest.robot @@ -0,0 +1,60 @@ +*** Settings *** +Documentation CSV examples for Robot Framework. +Library Collections +Library OperatingSystem +Library CSVLibrary + +*** Variables *** +# Example data generated with: https://www.mockaroo.com/ +@{template_list}= 1 Douglas Morris dmorris0@mozilla.org Male 205.4.212.229 +&{template_dict_row1}= id=1 first_name=Douglas last_name=Morris email=dmorris0@mozilla.org gender=Male ip_address=205.4.212.229 +&{template_dict_row2}= id=2 first_name=Stephanie last_name=Oliver email=soliver1@google.com.br gender=Female ip_address=18.101.154.106 +&{template_dict_row3}= id=3 first_name=Stephanie last_name=Oliver email=soliver1@google.com.br gender=Female ip_address=18.101.154.106 +@{template_dict_rows}= &{template_dict_row1} &{template_dict_row2} + +&{template_dict_append}= ip_address=255.36.191.0 id=11 first_name=John last_name=ParkerFisher email=jparker00@google.com gender=Male + +${csv_string}= SEPARATOR= +... id,first_name,last_name,email,gender,ip_address\n +... 1,Douglas,Morris,dmorris0@mozilla.org,Male,205.4.212.229\n +... 2,Stephanie,Oliver,soliver1@google.com.br,Female,18.101.154.106\n +${csv_string_ext}= SEPARATOR= +... id,first_name,last_name,email,gender,ip_address\n +... 1,Douglas,Morris,dmorris0@mozilla.org,Male,205.4.212.229\n +... 2,Stephanie,Oliver,soliver1@google.com.br,Female,18.101.154.106\n +... 3,Stephanie,Oliver,soliver1@google.com.br,Female,18.101.154.106\n +${data_empty_copy}= ${CURDIR}${/}data_copy.csv +${data_create_file}= ${CURDIR}${/}data_create.csv +${data_append_file}= ${CURDIR}${/}data_append.csv + +*** Test Cases *** +Empty csv file + [Setup] Copy File ${CURDIR}${/}data.csv ${data_empty_copy} + File Should Not Be Empty ${data_empty_copy} + Empty Csv File ${data_empty_copy} + File Should Be Empty ${data_empty_copy} + [Teardown] Remove File ${data_empty_copy} + +Csv file from associative + File Should Not Exist ${data_create_file} + Csv File From Associative ${data_create_file} ${template_dict_rows} + ${content}= Get File ${data_create_file} + Should Be Equal ${csv_string} ${content} + [Teardown] Remove File ${data_create_file} + +Append to csv file + [Setup] Copy File ${CURDIR}${/}data.csv ${data_append_file} + File Should Not Be Empty ${data_append_file} + ${aa}= Append To Csv File ${data_append_file} ${template_dict_append} + + @{appended_dict}= read csv file to associative ${data_append_file} + List Should Contain Value ${appended_dict} ${template_dict_append} + [Teardown] Remove File ${data_append_file} + +Append to csv string + ${content}= append to csv string ${csv_string} ${template_dict_row3} + Should Be Equal ${csv_string_ext} ${content} + +CSV string from associative + ${content}= csv string from associative ${template_dict_rows} + Should Be Equal ${csv_string} ${content} diff --git a/tests/data.csv b/tests/data.csv index 3066d92..4bce924 100644 --- a/tests/data.csv +++ b/tests/data.csv @@ -8,4 +8,4 @@ id,first_name,last_name,email,gender,ip_address 7,Rachel,Robinson,rrobinson6@blogger.com,Female,132.66.117.101 8,Phillip,Johnston,pjohnston7@disqus.com,Male,70.152.55.21 9,Craig,Burton,cburton8@toplist.cz,Male,73.117.157.82 -10,Patrick,Fisher,pfisher9@1und1.de,Male,2.36.191.97 \ No newline at end of file +10,Patrick,Fisher,pfisher9@1und1.de,Male,2.36.191.97 diff --git a/tests/data_quoting.csv b/tests/data_quoting.csv index 1faa8e7..36c7a58 100644 --- a/tests/data_quoting.csv +++ b/tests/data_quoting.csv @@ -8,4 +8,4 @@ id,first_name,last_name,email,gender,ip_address 7,Rachel,Robinson,rrobinson6@blogger.com,Female,132.66.117.101 8,Phillip,Johnston,pjohnston7@disqus.com,Male,70.152.55.21 9,Craig,Burton,cburton8@toplist.cz,Male,73.117.157.82 -10,Patrick,Fisher,pfisher9@1und1.de,Male,2.36.191.97 \ No newline at end of file +10,Patrick,Fisher,pfisher9@1und1.de,Male,2.36.191.97