1import copy
2import weakref
3import operator
4
5from . import enums
6from .base import BTBaseRow, BTBaseColumn
7from .utils import pre_process, termwidth, textwrap, ensure_type
8from .compat import basestring, Iterable, to_unicode, zip_longest
9from .meta import AlignmentMetaData, NonNegativeIntegerMetaData
10
11
12class BTRowHeader(BTBaseColumn):
13    def __init__(self, table, value):
14        for i in value:
15            self._validate_item(i)
16        super(BTRowHeader, self).__init__(table, value)
17
18    def __setitem__(self, key, value):
19        self._validate_item(value)
20        super(BTRowHeader, self).__setitem__(key, value)
21
22    def _validate_item(self, value):
23        if not (isinstance(value, basestring) or value is None):
24            raise TypeError(
25                ("header must be of type 'str', " "got {}").format(
26                    type(value).__name__
27                )
28            )
29
30
31class BTColumnHeader(BTBaseRow):
32    def __init__(self, table, value):
33        for i in value:
34            self._validate_item(i)
35        super(BTColumnHeader, self).__init__(table, value)
36        self.alignment = None
37
38    @property
39    def alignment(self):
40        """get/set alignment of the column header of the table.
41
42        It can be any iterable containing only the following:
43
44        * beautifultable.ALIGN_LEFT
45        * beautifultable.ALIGN_CENTER
46        * beautifultable.ALIGN_RIGHT
47        """
48        return self._alignment
49
50    @alignment.setter
51    def alignment(self, value):
52        if value is None:
53            self._alignment = None
54            return
55        if isinstance(value, enums.Alignment):
56            value = [value] * len(self)
57        self._alignment = AlignmentMetaData(self._table, value)
58
59    @property
60    def separator(self):
61        """Character used to draw the line seperating header from the table."""
62        return self._table._header_separator
63
64    @separator.setter
65    def separator(self, value):
66        self._table._header_separator = ensure_type(value, basestring)
67
68    @property
69    def junction(self):
70        """Character used to draw junctions in the header separator."""
71        return self._table._header_junction
72
73    @junction.setter
74    def junction(self, value):
75        self._table._header_junction = ensure_type(value, basestring)
76
77    def __setitem__(self, key, value):
78        self._validate_item(value)
79        super(BTColumnHeader, self).__setitem__(key, value)
80
81    def _validate_item(self, value):
82        if not (isinstance(value, basestring) or value is None):
83            raise TypeError(
84                ("header must be of type 'str', " "got {}").format(
85                    type(value).__name__
86                )
87            )
88
89
90class BTRowData(BTBaseRow):
91    def _get_padding(self):
92        return (
93            self._table.columns.padding_left,
94            self._table.columns.padding_right,
95        )
96
97    def _clamp_row(self, row):
98        """Process a row so that it is clamped by column_width.
99
100        Parameters
101        ----------
102        row : array_like
103             A single row.
104
105        Returns
106        -------
107        list of list:
108            List representation of the `row` after it has been processed
109            according to width exceed policy.
110        """
111        table = self._table
112        lpw, rpw = self._get_padding()
113        wep = table.columns.width_exceed_policy
114
115        result = []
116
117        if (
118            wep is enums.WidthExceedPolicy.WEP_STRIP
119            or wep is enums.WidthExceedPolicy.WEP_ELLIPSIS
120        ):
121
122            # Let's strip the row
123            delimiter = (
124                "" if wep is enums.WidthExceedPolicy.WEP_STRIP else "..."
125            )
126            row_item_list = []
127            for index, row_item in enumerate(row):
128                left_pad = table.columns._pad_character * lpw[index]
129                right_pad = table.columns._pad_character * rpw[index]
130                clmp_str = (
131                    left_pad
132                    + self._clamp_string(row_item, index, delimiter)
133                    + right_pad
134                )
135                row_item_list.append(clmp_str)
136            result.append(row_item_list)
137        elif wep is enums.WidthExceedPolicy.WEP_WRAP:
138
139            # Let's wrap the row
140            string_partition = []
141
142            for index, row_item in enumerate(row):
143                width = table.columns.width[index] - lpw[index] - rpw[index]
144                string_partition.append(textwrap(row_item, width))
145
146            for row_items in zip_longest(*string_partition, fillvalue=""):
147                row_item_list = []
148                for index, row_item in enumerate(row_items):
149                    left_pad = table.columns._pad_character * lpw[index]
150                    right_pad = table.columns._pad_character * rpw[index]
151                    row_item_list.append(left_pad + row_item + right_pad)
152                result.append(row_item_list)
153
154        return [[""] * len(table.columns)] if len(result) == 0 else result
155
156    def _clamp_string(self, row_item, index, delimiter=""):
157        """Clamp `row_item` to fit in column referred by index.
158
159        This method considers padding and appends the delimiter if `row_item`
160        needs to be truncated.
161
162        Parameters
163        ----------
164        row_item: str
165            String which should be clamped.
166
167        index: int
168            Index of the column `row_item` belongs to.
169
170        delimiter: str
171            String which is to be appended to the clamped string.
172
173        Returns
174        -------
175        str
176            The modified string which fits in it's column.
177        """
178        lpw, rpw = self._get_padding()
179        width = self._table.columns.width[index] - lpw[index] - rpw[index]
180
181        if termwidth(row_item) <= width:
182            return row_item
183        else:
184            if width - len(delimiter) >= 0:
185                clamped_string = (
186                    textwrap(row_item, width - len(delimiter))[0] + delimiter
187                )
188            else:
189                clamped_string = delimiter[:width]
190            return clamped_string
191
192    def _get_string(
193        self,
194        align=None,
195        mask=None,
196        draw_left_border=True,
197        draw_right_border=True,
198    ):
199        """Return a string representation of a row."""
200
201        rows = []
202
203        table = self._table
204        width = table.columns.width
205        sign = table.sign
206
207        if align is None:
208            align = table.columns.alignment
209
210        if mask is None:
211            mask = [True] * len(table.columns)
212
213        lpw, rpw = self._get_padding()
214
215        string = []
216        for i, item in enumerate(self._value):
217            if isinstance(item, type(table)):
218                # temporarily change the max width of the table
219                curr_maxwidth = item.maxwidth
220                item.maxwidth = width[i] - lpw[i] - rpw[i]
221                rows.append(
222                    pre_process(
223                        item,
224                        table.detect_numerics,
225                        table.precision,
226                        sign.value,
227                    ).split("\n")
228                )
229                item.maxwidth = curr_maxwidth
230            else:
231                rows.append(
232                    pre_process(
233                        item,
234                        table.detect_numerics,
235                        table.precision,
236                        sign.value,
237                    ).split("\n")
238                )
239        for row in map(list, zip_longest(*rows, fillvalue="")):
240            for i in range(len(row)):
241                row[i] = pre_process(
242                    row[i],
243                    table.detect_numerics,
244                    table.precision,
245                    sign.value,
246                )
247            for row_ in self._clamp_row(row):
248                for i in range(len(table.columns)):
249                    # str.format method doesn't work for multibyte strings
250                    # hence, we need to manually align the texts instead
251                    # of using the align property of the str.format method
252                    pad_len = width[i] - termwidth(row_[i])
253                    if align[i].value == "<":
254                        right_pad = " " * pad_len
255                        row_[i] = to_unicode(row_[i]) + right_pad
256                    elif align[i].value == ">":
257                        left_pad = " " * pad_len
258                        row_[i] = left_pad + to_unicode(row_[i])
259                    else:
260                        left_pad = " " * (pad_len // 2)
261                        right_pad = " " * (pad_len - pad_len // 2)
262                        row_[i] = left_pad + to_unicode(row_[i]) + right_pad
263                content = []
264                for j, item in enumerate(row_):
265                    if j > 0:
266                        content.append(
267                            table.columns.separator
268                            if (mask[j - 1] or mask[j])
269                            else " " * termwidth(table.columns.separator)
270                        )
271                    content.append(item)
272                content = "".join(content)
273                content = (
274                    table.border.left
275                    if mask[0]
276                    else " " * termwidth(table.border.left)
277                ) + content
278                content += (
279                    table.border.right
280                    if mask[-1]
281                    else " " * termwidth(table.border.right)
282                )
283                string.append(content)
284        return "\n".join(string)
285
286    def __str__(self):
287        return self._get_string()
288
289
290class BTColumnData(BTBaseColumn):
291    pass
292
293
294class BTRowCollection(object):
295    def __init__(self, table):
296        self._table = table
297        self._reset_state(0)
298
299    @property
300    def _table(self):
301        return self._table_ref()
302
303    @_table.setter
304    def _table(self, value):
305        self._table_ref = weakref.ref(value)
306
307    def _reset_state(self, nrow):
308        self._table._data = type(self._table._data)(
309            self._table,
310            [
311                BTRowData(self._table, [None] * self._table._ncol)
312                for i in range(nrow)
313            ],
314        )
315        self.header = BTRowHeader(self._table, [None] * nrow)
316
317    @property
318    def header(self):
319        return self._header
320
321    @header.setter
322    def header(self, value):
323        self._header = BTRowHeader(self._table, value)
324
325    @property
326    def separator(self):
327        """Character used to draw the line seperating two rows."""
328        return self._table._row_separator
329
330    @separator.setter
331    def separator(self, value):
332        self._table._row_separator = ensure_type(value, basestring)
333
334    def _canonical_key(self, key):
335        if isinstance(key, (int, slice)):
336            return key
337        elif isinstance(key, basestring):
338            return self.header.index(key)
339        raise TypeError(
340            ("row indices must be int, str or slices, not {}").format(
341                type(key).__name__
342            )
343        )
344
345    def __len__(self):
346        return len(self._table._data)
347
348    def __getitem__(self, key):
349        """Get a particular row, or a new table by slicing.
350
351        Parameters
352        ----------
353        key : int, slice, str
354            If key is an `int`, returns a row at index `key`.
355            If key is an `str`, returns the first row with heading `key`.
356            If key is a slice object, returns a new sliced table.
357
358        Raises
359        ------
360        TypeError
361            If key is not of type int, slice or str.
362        IndexError
363            If `int` index is out of range.
364        KeyError
365            If `str` key is not found in header.
366        """
367        if isinstance(key, slice):
368            new_table = copy.deepcopy(self._table)
369            new_table.rows.clear()
370            new_table.rows.header = self._table.rows.header[key]
371            for i, r in enumerate(self._table._data[key]):
372                new_table.rows[i] = r.value
373            return new_table
374        if isinstance(key, (int, basestring)):
375            return self._table._data[key]
376        raise TypeError(
377            ("row indices must be int, str or a slice object, not {}").format(
378                type(key).__name__
379            )
380        )
381
382    def __delitem__(self, key):
383        """Delete a row, or multiple rows by slicing.
384
385        Parameters
386        ----------
387        key : int, slice, str
388            If key is an `int`, deletes a row at index `key`.
389            If key is an `str`, deletes the first row with heading `key`.
390            If key is a slice object, deletes multiple rows.
391
392        Raises
393        ------
394        TypeError
395            If key is not of type int, slice or str.
396        IndexError
397            If `int` key is out of range.
398        KeyError
399            If `str` key is not in header.
400        """
401        if isinstance(key, (int, basestring, slice)):
402            del self._table._data[key]
403            del self.header[key]
404        else:
405            raise TypeError(
406                (
407                    "row indices must be int, str or " "a slice object, not {}"
408                ).format(type(key).__name__)
409            )
410
411    def __setitem__(self, key, value):
412        """Update a row, or multiple rows by slicing.
413
414        Parameters
415        ----------
416        key : int, slice, str
417            If key is an `int`, updates a row.
418            If key is an `str`, updates the first row with heading `key`.
419            If key is a slice object, updates multiple rows.
420
421        Raises
422        ------
423        TypeError
424            If key is not of type int, slice or str.
425        IndexError
426            If `int` key is out of range.
427        KeyError
428            If `str` key is not in header.
429        """
430        if isinstance(key, (int, basestring)):
431            self._table._data[key] = BTRowData(self._table, value)
432        elif isinstance(key, slice):
433            value = [list(row) for row in value]
434            if len(self._table.columns) == 0:
435                self._table.columns._initialize(len(value[0]))
436            self._table._data[key] = [
437                BTRowData(self._table, row) for row in value
438            ]
439        else:
440            raise TypeError("key must be int, str or a slice object")
441
442    def __contains__(self, key):
443        if isinstance(key, basestring):
444            return key in self.header
445        elif isinstance(key, Iterable):
446            return key in self._table._data
447        else:
448            raise TypeError(
449                ("'key' must be str or Iterable, " "not {}").format(
450                    type(key).__name__
451                )
452            )
453
454    def __iter__(self):
455        return BTCollectionIterator(self)
456
457    def __repr__(self):
458        return repr(self._table._data)
459
460    def __str__(self):
461        return str(self._table._data)
462
463    def reverse(self):
464        """Reverse the table row-wise *IN PLACE*."""
465        self._table._data._reverse()
466
467    def pop(self, index=-1):
468        """Remove and return row at index (default last).
469
470        Parameters
471        ----------
472        index : int, str
473            index or heading of the row. Normal list rules apply.
474        """
475        if not isinstance(index, (int, basestring)):
476            raise TypeError(
477                ("row index must be int or str, " "not {}").format(
478                    type(index).__name__
479                )
480            )
481        if len(self._table._data) == 0:
482            raise IndexError("pop from empty table")
483        else:
484            res = self._table._data._pop(index)
485            self.header._pop(index)
486            return res
487
488    def insert(self, index, row, header=None):
489        """Insert a row before index in the table.
490
491        Parameters
492        ----------
493        index : int
494            List index rules apply
495
496        row : iterable
497            Any iterable of appropriate length.
498
499        header : str, optional
500            Heading of the row
501
502        Raises
503        ------
504        TypeError:
505            If `row` is not an iterable.
506
507        ValueError:
508            If size of `row` is inconsistent with the current number
509            of columns.
510        """
511        if self._table._ncol == 0:
512            row = list(row)
513            self._table.columns._reset_state(len(row))
514        self.header._insert(index, header)
515        self._table._data._insert(index, BTRowData(self._table, row))
516
517    def append(self, row, header=None):
518        """Append a row to end of the table.
519
520        Parameters
521        ----------
522        row : iterable
523            Any iterable of appropriate length.
524
525        header : str, optional
526            Heading of the row
527
528        """
529        self.insert(len(self), row, header)
530
531    def update(self, key, value):
532        """Update row(s) identified with `key` in the table.
533
534        `key` can be a index or a slice object.
535
536        Parameters
537        ----------
538        key : int or slice
539            index of the row, or a slice object.
540
541        value : iterable
542            If an index is specified, `value` should be an iterable
543            of appropriate length. Instead if a slice object is
544            passed as key, value should be an iterable of rows.
545
546        Raises
547        ------
548        IndexError:
549            If index specified is out of range.
550
551        TypeError:
552            If `value` is of incorrect type.
553
554        ValueError:
555            If length of row does not matches number of columns.
556        """
557        self[key] = value
558
559    def clear(self):
560        self._reset_state(0)
561
562    def sort(self, key, reverse=False):
563        """Stable sort of the table *IN-PLACE* with respect to a column.
564
565        Parameters
566        ----------
567        key: int, str
568            index or header of the column. Normal list rules apply.
569        reverse : bool
570            If `True` then table is sorted as if each comparison was reversed.
571        """
572        if isinstance(key, (int, basestring)):
573            key = operator.itemgetter(key)
574        elif callable(key):
575            pass
576        else:
577            raise TypeError(
578                "'key' must either be 'int' or 'str' or a 'callable'"
579            )
580
581        indices = sorted(
582            range(len(self)),
583            key=lambda x: key(self._table._data[x]),
584            reverse=reverse,
585        )
586        self._table._data._sort(key=key, reverse=reverse)
587        self.header = [self.header[i] for i in indices]
588
589    def filter(self, key):
590        """Return a copy of the table with only those rows which satisfy a
591        certain condition.
592
593        Returns
594        -------
595        BeautifulTable:
596            Filtered copy of the BeautifulTable instance.
597        """
598        new_table = self._table.rows[:]
599        new_table.rows.clear()
600        for row in filter(key, self):
601            new_table.rows.append(row)
602        return new_table
603
604
605class BTCollectionIterator(object):
606    def __init__(self, collection):
607        self._collection = collection
608        self._index = -1
609
610    def __iter__(self):
611        return self
612
613    def __next__(self):
614        self._index += 1
615        if self._index == len(self._collection):
616            raise StopIteration
617        return self._collection[self._index]
618
619
620class BTColumnCollection(object):
621    def __init__(self, table, default_alignment, default_padding):
622        self._table = table
623        self._width_exceed_policy = enums.WEP_WRAP
624        self._pad_character = " "
625        self.default_alignment = default_alignment
626        self.default_padding = default_padding
627
628        self._reset_state(0)
629
630    @property
631    def _table(self):
632        return self._table_ref()
633
634    @_table.setter
635    def _table(self, value):
636        self._table_ref = weakref.ref(value)
637
638    @property
639    def padding(self):
640        """Set width for left and rigth padding of the columns of the table."""
641        raise AttributeError(
642            "cannot read attribute 'padding'. use 'padding_{left|right}'"
643        )
644
645    @padding.setter
646    def padding(self, value):
647        self.padding_left = value
648        self.padding_right = value
649
650    def _reset_state(self, ncol):
651        self._table._ncol = ncol
652        self._header = BTColumnHeader(self._table, [None] * ncol)
653        self._auto_width = True
654        self._alignment = AlignmentMetaData(
655            self._table, [self.default_alignment] * ncol
656        )
657        self._width = NonNegativeIntegerMetaData(self._table, [0] * ncol)
658        self._padding_left = NonNegativeIntegerMetaData(
659            self._table, [self.default_padding] * ncol
660        )
661        self._padding_right = NonNegativeIntegerMetaData(
662            self._table, [self.default_padding] * ncol
663        )
664        self._table._data = type(self._table._data)(
665            self._table,
666            [
667                BTRowData(self._table, [None] * ncol)
668                for i in range(len(self._table._data))
669            ],
670        )
671
672    def _canonical_key(self, key):
673        if isinstance(key, (int, slice)):
674            return key
675        elif isinstance(key, basestring):
676            return self.header.index(key)
677        raise TypeError(
678            ("column indices must be int, str or slices, not {}").format(
679                type(key).__name__
680            )
681        )
682
683    @property
684    def header(self):
685        """get/set headings for the columns of the table.
686
687        It can be any iterable with all members an instance of `str` or None.
688        """
689        return self._header
690
691    @header.setter
692    def header(self, value):
693        self._header = BTColumnHeader(self._table, value)
694
695    @property
696    def alignment(self):
697        """get/set alignment of the columns of the table.
698
699        It can be any iterable containing only the following:
700
701        * beautifultable.ALIGN_LEFT
702        * beautifultable.ALIGN_CENTER
703        * beautifultable.ALIGN_RIGHT
704        """
705        return self._alignment
706
707    @alignment.setter
708    def alignment(self, value):
709        if isinstance(value, enums.Alignment):
710            value = [value] * len(self)
711        self._alignment = AlignmentMetaData(self._table, value)
712
713    @property
714    def width(self):
715        """get/set width for the columns of the table.
716
717        Width of the column specifies the max number of characters
718        a column can contain. Larger characters are handled according to
719        `width_exceed_policy`. This can be one of `'auto'`, a non-negative
720        integer or an iterable of the same length as the number of columns.
721        If set to anything other than 'auto', the user is responsible for
722        updating it if new columns are added or existing ones are updated.
723        """
724        return self._width
725
726    @width.setter
727    def width(self, value):
728        if isinstance(value, str):
729            if value == "auto":
730                self._auto_width = True
731                return
732            raise ValueError("Invalid value '{}'".format(value))
733        if isinstance(value, int):
734            value = [value] * len(self)
735        self._width = NonNegativeIntegerMetaData(self._table, value)
736        self._auto_width = False
737
738    @property
739    def padding_left(self):
740        """get/set width for left padding of the columns of the table.
741
742        Left Width of the padding specifies the number of characters
743        on the left of a column reserved for padding. By Default It is 1.
744        """
745        return self._padding_left
746
747    @padding_left.setter
748    def padding_left(self, value):
749        if isinstance(value, int):
750            value = [value] * len(self)
751        self._padding_left = NonNegativeIntegerMetaData(self._table, value)
752
753    @property
754    def padding_right(self):
755        """get/set width for right padding of the columns of the table.
756
757        Right Width of the padding specifies the number of characters
758        on the rigth of a column reserved for padding. By default It is 1.
759        """
760        return self._padding_right
761
762    @padding_right.setter
763    def padding_right(self, value):
764        if isinstance(value, int):
765            value = [value] * len(self)
766        self._padding_right = NonNegativeIntegerMetaData(self._table, value)
767
768    @property
769    def width_exceed_policy(self):
770        """Attribute to control how exceeding column width should be handled.
771
772        It can be one of the following:
773
774        ============================  =========================================
775         Option                        Meaning
776        ============================  =========================================
777         beautifulbable.WEP_WRAP       An item is wrapped so every line fits
778                                       within it's column width.
779
780         beautifultable.WEP_STRIP      An item is stripped to fit in it's
781                                       column.
782
783         beautifultable.WEP_ELLIPSIS   An item is stripped to fit in it's
784                                       column and appended with ...(Ellipsis).
785        ============================  =========================================
786        """
787        return self._width_exceed_policy
788
789    @width_exceed_policy.setter
790    def width_exceed_policy(self, value):
791        if not isinstance(value, enums.WidthExceedPolicy):
792            allowed = (
793                "{}.{}".format(type(self).__name__, i.name)
794                for i in enums.WidthExceedPolicy
795            )
796            error_msg = (
797                "allowed values for width_exceed_policy are: "
798                + ", ".join(allowed)
799            )
800            raise ValueError(error_msg)
801        self._width_exceed_policy = value
802
803    @property
804    def default_alignment(self):
805        """Attribute to control the alignment of newly created columns.
806
807        It can be one of the following:
808
809        ============================  =========================================
810         Option                        Meaning
811        ============================  =========================================
812         beautifultable.ALIGN_LEFT     New columns are left aligned.
813
814         beautifultable.ALIGN_CENTER   New columns are center aligned.
815
816         beautifultable.ALIGN_RIGHT    New columns are right aligned.
817        ============================  =========================================
818        """
819        return self._default_alignment
820
821    @default_alignment.setter
822    def default_alignment(self, value):
823        if not isinstance(value, enums.Alignment):
824            allowed = (
825                "{}.{}".format(type(self).__name__, i.name)
826                for i in enums.Alignment
827            )
828            error_msg = (
829                "allowed values for default_alignment are: "
830                + ", ".join(allowed)
831            )
832            raise ValueError(error_msg)
833        self._default_alignment = value
834
835    @property
836    def default_padding(self):
837        """Initial value for Left and Right padding widths for new columns."""
838        return self._default_padding
839
840    @default_padding.setter
841    def default_padding(self, value):
842        if not isinstance(value, int):
843            raise TypeError("default_padding must be an integer")
844        elif value < 0:
845            raise ValueError("default_padding must be a non-negative integer")
846        else:
847            self._default_padding = value
848
849    @property
850    def separator(self):
851        """Character used to draw the line seperating two columns."""
852        return self._table._column_separator
853
854    @separator.setter
855    def separator(self, value):
856        self._table._column_separator = ensure_type(value, basestring)
857
858    def __len__(self):
859        return self._table._ncol
860
861    def __getitem__(self, key):
862        """Get a column, or a new table by slicing.
863
864        Parameters
865        ----------
866
867        key : int, slice, str
868            If key is an `int`, returns column at index `key`.
869            If key is an `str`, returns first column with heading `key`.
870            If key is a slice object, returns a new sliced table.
871
872        Raises
873        ------
874
875        TypeError
876            If key is not of type int, slice or str.
877        IndexError
878            If `int` key is out of range.
879        KeyError
880            If `str` key is not in header.
881        """
882        if isinstance(key, int):
883            pass
884        elif isinstance(key, slice):
885            new_table = copy.deepcopy(self._table)
886
887            new_table.columns.clear()
888            new_table.columns.header = self.header[key]
889            new_table.columns.alignment = self.alignment[key]
890            new_table.columns.padding_left = self.padding_left[key]
891            new_table.columns.padding_right = self.padding_right[key]
892            new_table.columns.width = self.width[key]
893            new_table.columns._auto_width = self._auto_width
894            for i, r in enumerate(self._table._data):
895                new_table.rows[i] = r.value[key]
896            return new_table
897        elif isinstance(key, basestring):
898            key = self.header.index(key)
899        else:
900            raise TypeError(
901                (
902                    "column indices must be integers, strings or "
903                    "slices, not {}"
904                ).format(type(key).__name__)
905            )
906        return BTColumnData(
907            self._table, [row[key] for row in self._table._data]
908        )
909
910    def __delitem__(self, key):
911        """Delete a column, or multiple columns by slicing.
912
913        Parameters
914        ----------
915
916        key : int, slice, str
917            If key is an `int`, deletes column at index `key`.
918            If key is a slice object, deletes multiple columns.
919            If key is an `str`, deletes the first column with heading `key`
920
921        Raises
922        ------
923
924        TypeError
925            If key is not of type int, slice or str.
926        IndexError
927            If `int` key is out of range.
928        KeyError
929            If `str` key is not in header.
930        """
931        if isinstance(key, (int, basestring, slice)):
932            key = self._canonical_key(key)
933
934            del self.alignment[key]
935            del self.width[key]
936            del self.padding_left[key]
937            del self.padding_right[key]
938            for row in self._table.rows:
939                del row[key]
940            del self.header[key]
941            if self.header.alignment is not None:
942                del self.header.alignment[key]
943            self._table._ncol = len(self.header)
944            if self._table._ncol == 0:
945                del self._table.rows[:]
946        else:
947            raise TypeError(
948                ("table indices must be int, str or " "slices, not {}").format(
949                    type(key).__name__
950                )
951            )
952
953    def __setitem__(self, key, value):
954        """Update a column, or multiple columns by slicing.
955
956        Parameters
957        ----------
958
959        key : int, slice, str
960            If key is an `int`, updates column at index `key`.
961            If key is an `str`, updates first column with heading `key`.
962            If key is a slice object, updates multiple columns.
963
964        Raises
965        ------
966
967        TypeError
968            If key is not of type int, slice or str.
969        IndexError
970            If `int` key is out of range.
971        KeyError
972            If `str` key is not in header
973        """
974        if not isinstance(key, (int, basestring, slice)):
975            raise TypeError(
976                "column indices must be of type int, str or a slice object"
977            )
978        for row, new_item in zip(self._table.rows, value):
979            row[key] = new_item
980
981    def __contains__(self, key):
982        if isinstance(key, basestring):
983            return key in self.header
984        elif isinstance(key, Iterable):
985            key = list(key)
986            return any(key == column for column in self)
987        else:
988            raise TypeError(
989                ("'key' must be str or Iterable, " "not {}").format(
990                    type(key).__name__
991                )
992            )
993
994    def __iter__(self):
995        return BTCollectionIterator(self)
996
997    def __repr__(self):
998        return repr(self._table)
999
1000    def __str__(self):
1001        return str(self._table._data)
1002
1003    def clear(self):
1004        self._reset_state(0)
1005
1006    def pop(self, index=-1):
1007        """Remove and return column at index (default last).
1008
1009        Parameters
1010        ----------
1011        index : int, str
1012            index of the column, or the header of the column.
1013            If index is specified, then normal list rules apply.
1014
1015        Raises
1016        ------
1017        TypeError:
1018            If index is not an instance of `int`, or `str`.
1019
1020        IndexError:
1021            If Table is empty.
1022        """
1023        if not isinstance(index, (int, basestring)):
1024            raise TypeError(
1025                ("column index must be int or str, " "not {}").format(
1026                    type(index).__name__
1027                )
1028            )
1029        if self._table._ncol == 0:
1030            raise IndexError("pop from empty table")
1031        else:
1032            res = []
1033            index = self._canonical_key(index)
1034            for row in self._table.rows:
1035                res.append(row._pop(index))
1036            res = BTColumnData(self._table, res)
1037            self.alignment._pop(index)
1038            self.width._pop(index)
1039            self.padding_left._pop(index)
1040            self.padding_right._pop(index)
1041            self.header._pop(index)
1042
1043            self._table._ncol = len(self.header)
1044            if self._table._ncol == 0:
1045                del self._table.rows[:]
1046            return res
1047
1048    def update(self, key, value):
1049        """Update a column named `header` in the table.
1050
1051        If length of column is smaller than number of rows, lets say
1052        `k`, only the first `k` values in the column is updated.
1053
1054        Parameters
1055        ----------
1056        key : int, str
1057            If `key` is int, column at index `key` is updated.
1058            If `key` is str, the first column with heading `key` is updated.
1059
1060        value : iterable
1061            Any iterable of appropriate length.
1062
1063        Raises
1064        ------
1065        TypeError:
1066            If length of `column` is shorter than number of rows.
1067
1068        ValueError:
1069            If no column exists with heading `header`.
1070        """
1071        self[key] = value
1072
1073    def insert(
1074        self,
1075        index,
1076        column,
1077        header=None,
1078        padding_left=None,
1079        padding_right=None,
1080        alignment=None,
1081    ):
1082        """Insert a column before `index` in the table.
1083
1084        If length of column is bigger than number of rows, lets say
1085        `k`, only the first `k` values of `column` is considered.
1086        If column is shorter than 'k', ValueError is raised.
1087
1088        Note that Table remains in consistent state even if column
1089        is too short. Any changes made by this method is rolled back
1090        before raising the exception.
1091
1092        Parameters
1093        ----------
1094        index : int
1095            List index rules apply.
1096
1097        column : iterable
1098            Any iterable of appropriate length.
1099
1100        header : str, optional
1101            Heading of the column.
1102
1103        padding_left : int, optional
1104            Left padding of the column.
1105
1106        padding_right : int, optional
1107            Right padding of the column.
1108
1109        alignment : Alignment, optional
1110            alignment of the column.
1111
1112        Raises
1113        ------
1114        TypeError:
1115            If `header` is not of type `str`.
1116
1117        ValueError:
1118            If length of `column` is shorter than number of rows.
1119        """
1120        padding_left = (
1121            self.default_padding if padding_left is None else padding_left
1122        )
1123        padding_right = (
1124            self.default_padding if padding_right is None else padding_right
1125        )
1126        alignment = self.default_alignment if alignment is None else alignment
1127        if not isinstance(padding_left, int):
1128            raise TypeError(
1129                "'padding_left' should be of type 'int' not '{}'".format(
1130                    type(padding_left).__name__
1131                )
1132            )
1133        if not isinstance(padding_right, int):
1134            raise TypeError(
1135                "'padding_right' should be of type 'int' not '{}'".format(
1136                    type(padding_right).__name__
1137                )
1138            )
1139        if not isinstance(alignment, enums.Alignment):
1140            raise TypeError(
1141                "alignment should be of type '{}' not '{}'".format(
1142                    enums.Alignment.__name__, type(alignment).__name__
1143                )
1144            )
1145
1146        if self._table._ncol == 0:
1147            self.header = [header]
1148            self.padding_left = [padding_left]
1149            self.padding_right = [padding_right]
1150            self.alignment = [alignment]
1151            self._table._data = type(self._table._data)(
1152                self._table, [BTRowData(self._table, [i]) for i in column]
1153            )
1154        else:
1155            if (not isinstance(header, basestring)) and (header is not None):
1156                raise TypeError(
1157                    "header must be of type 'str' not '{}'".format(
1158                        type(header).__name__
1159                    )
1160                )
1161            column_length = 0
1162            for row, new_item in zip(self._table.rows, column):
1163                row._insert(index, new_item)
1164                column_length += 1
1165            if column_length == len(self._table.rows):
1166                self._table._ncol += 1
1167                self.header._insert(index, header)
1168                self.width._insert(index, 0)
1169                self.alignment._insert(index, alignment)
1170                self.padding_left._insert(index, padding_left)
1171                self.padding_right._insert(index, padding_right)
1172                if self.header.alignment is not None:
1173                    self.header.alignment._insert(index, alignment)
1174            else:
1175                # Roll back changes so that table remains in consistent state
1176                for j in range(column_length, -1, -1):
1177                    self._table.rows[j]._pop(index)
1178                raise ValueError(
1179                    (
1180                        "length of 'column' should be atleast {}, " "got {}"
1181                    ).format(len(self._table.rows), column_length)
1182                )
1183
1184    def append(
1185        self,
1186        column,
1187        header=None,
1188        padding_left=None,
1189        padding_right=None,
1190        alignment=None,
1191    ):
1192        """Append a column to end of the table.
1193
1194        Parameters
1195        ----------
1196        column : iterable
1197            Any iterable of appropriate length.
1198
1199        header : str, optional
1200            Heading of the column
1201
1202        padding_left : int, optional
1203            Left padding of the column
1204
1205        padding_right : int,  optional
1206            Right padding of the column
1207
1208        alignment : Alignment, optional
1209            alignment of the column
1210        """
1211        self.insert(
1212            self._table._ncol,
1213            column,
1214            header,
1215            padding_left,
1216            padding_right,
1217            alignment,
1218        )
1219