Skip to content

Commit 9a9ab57

Browse files
authored
Version 4.0.0 (#100)
- Removed Python 3.4 support - Removed Python 3.5 support - Added Python 3.9 support - Allowing StyleFrame.ExcelWriter to accept any argument (except for engine) that pandas.ExcelWriter accepts - Allowing customizing formats of date, time and datetime objects when creating Styler instances - Fixed rows mis-alignment issue when calling to_excel with header=False (Closes #88) - read_excel does not accept sheetname argument anymore (was deprecated since version 1.6). Use sheet_name instead. - Version bumped to 4.0.0
1 parent c12971c commit 9a9ab57

File tree

12 files changed

+106
-103
lines changed

12 files changed

+106
-103
lines changed

.github/workflows/test.yml

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,9 @@ jobs:
1212
strategy:
1313
fail-fast: false
1414
matrix:
15-
python-version: [3.5, 3.6, 3.7, 3.8]
15+
python-version: [3.6, 3.7, 3.8, 3.9]
1616
experimental: [false]
1717
include:
18-
- python-version: 3.4
19-
experimental: true
20-
- python-version: 3.9-dev
21-
experimental: true
2218
- python-version: 3.10-dev
2319
experimental: true
2420

.travis.yml

Lines changed: 0 additions & 36 deletions
This file was deleted.

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1+
#### 4.0
2+
* **Removed Python 3.4 support**
3+
* **Removed Python 3.5 support**
4+
* **Added Python 3.9 support**
5+
* Allowing `StyleFrame.ExcelWriter` to accept any argument (except for `engine`) that `pandas.ExcelWriter` accepts
6+
* Allowing customizing formats of `date`, `time` and `datetime` objects when creating `Styler` instances
7+
* Fixed rows mis-alignment issue when calling `to_excel` with `header=False` ([GitHub issue #88](https://github.com/DeepSpace2/StyleFrame/issues/88))
8+
* `read_excel` does not accept `sheetname` argument anymore (was deprecated since version 1.6). Use `sheet_name` instead.
9+
110
#### 3.0.6
211
* Fixes [GitHub issue #94](https://github.com/DeepSpace2/StyleFrame/issues/94) - Passing `border_type=utils.borders.default_grid` to `Styler`
312

codecov.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
fixes:
2+
- "/home/runner/work/StyleFrame/::"

setup.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,11 +64,10 @@ def find_version(*file_paths):
6464

6565
# Specify the Python versions you support here. In particular, ensure
6666
# that you indicate whether you support Python 2, Python 3 or both.
67-
'Programming Language :: Python :: 3.4',
68-
'Programming Language :: Python :: 3.5',
6967
'Programming Language :: Python :: 3.6',
7068
'Programming Language :: Python :: 3.7',
71-
'Programming Language :: Python :: 3.8'
69+
'Programming Language :: Python :: 3.8',
70+
'Programming Language :: Python :: 3.9'
7271
],
7372

7473
# What does your project relate to?
@@ -83,5 +82,5 @@ def find_version(*file_paths):
8382
# your project is installed. For an analysis of "install_requires" vs pip's
8483
# requirements files see:
8584
# https://packaging.python.org/en/latest/requirements.html
86-
install_requires=['openpyxl>=2.5,<3.0.6', 'colour>=0.1.5,<0.2', 'jsonschema', 'xlrd>=1.0.0,<1.3.0', 'pandas<1.2.0']
85+
install_requires=['openpyxl>=2.5,<4', 'colour>=0.1.5,<0.2', 'jsonschema', 'xlrd>=1.0.0,<1.3.0', 'pandas<2']
8786
)

styleframe/command_line/commandline.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
styler_kwargs = set(inspect.signature(Styler).parameters.keys())
1414

1515

16-
class CommandLineInterface(object):
16+
class CommandLineInterface:
1717
def __init__(self, input_path=None, output_path=None, input_json=None):
1818
self.input_path = input_path
1919
self.input_json = input_json
Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,42 @@
1+
import argparse
12
import unittest
23
import os
34

5+
from contextlib import suppress
6+
from unittest.mock import patch
7+
48
from styleframe import CommandLineInterface, Styler, utils
9+
from styleframe.command_line.commandline import get_cli_args
510
from styleframe.command_line.tests import TEST_JSON_FILE, TEST_JSON_STRING_FILE
611
from styleframe.tests import TEST_FILENAME
712

813

914
class CommandlineInterfaceTest(unittest.TestCase):
1015
@classmethod
1116
def setUpClass(cls):
12-
cls.cli = CommandLineInterface(TEST_JSON_FILE, TEST_FILENAME)
1317
cls.sheet_1_col_a_style = Styler(bg_color=utils.colors.blue, font_color=utils.colors.yellow).to_openpyxl_style()
1418
cls.sheet_1_col_a_cell_2_style = Styler(bold=True, font=utils.fonts.arial, font_size=30,
1519
font_color=utils.colors.green,
1620
border_type=utils.borders.double).to_openpyxl_style()
1721
cls.sheet_1_col_b_cell_4_style = Styler(bold=True, font=utils.fonts.arial, font_size=16).to_openpyxl_style()
1822

1923
def tearDown(self):
20-
try:
24+
with suppress(OSError):
2125
os.remove(TEST_FILENAME)
22-
except OSError as ex:
23-
print(ex)
2426

2527
# noinspection PyUnresolvedReferences
2628
def test_parse_as_json(self):
27-
self.cli.parse_as_json()
28-
loc_col_a = self.cli.Sheet1_sf.columns.get_loc('col_a')
29-
loc_col_b = self.cli.Sheet1_sf.columns.get_loc('col_b')
30-
self.assertEqual(self.cli.Sheet1_sf.iloc[0, loc_col_a].style.to_openpyxl_style(), self.sheet_1_col_a_style)
31-
self.assertEqual(self.cli.Sheet1_sf.iloc[1, loc_col_a].style.to_openpyxl_style(), self.sheet_1_col_a_cell_2_style)
32-
self.assertEqual(self.cli.Sheet1_sf.iloc[1, loc_col_b].style.to_openpyxl_style(), self.sheet_1_col_b_cell_4_style)
29+
cli = CommandLineInterface(TEST_JSON_FILE, TEST_FILENAME)
30+
cli.parse_as_json()
31+
loc_col_a = cli.Sheet1_sf.columns.get_loc('col_a')
32+
loc_col_b = cli.Sheet1_sf.columns.get_loc('col_b')
33+
self.assertEqual(cli.Sheet1_sf.iloc[0, loc_col_a].style.to_openpyxl_style(), self.sheet_1_col_a_style)
34+
self.assertEqual(cli.Sheet1_sf.iloc[1, loc_col_a].style.to_openpyxl_style(), self.sheet_1_col_a_cell_2_style)
35+
self.assertEqual(cli.Sheet1_sf.iloc[1, loc_col_b].style.to_openpyxl_style(), self.sheet_1_col_b_cell_4_style)
36+
37+
def test_load_from_json_invalid_args(self):
38+
with self.assertRaises((TypeError, ValueError)):
39+
CommandLineInterface()._load_from_json()
3340

3441
# noinspection PyUnresolvedReferences
3542
def test_init_with_json_string(self):
@@ -42,3 +49,11 @@ def test_init_with_json_string(self):
4249
self.assertEqual(cli.Sheet1_sf.iloc[0, loc_col_a].style.to_openpyxl_style(), self.sheet_1_col_a_style)
4350
self.assertEqual(cli.Sheet1_sf.iloc[1, loc_col_a].style.to_openpyxl_style(), self.sheet_1_col_a_cell_2_style)
4451
self.assertEqual(cli.Sheet1_sf.iloc[1, loc_col_b].style.to_openpyxl_style(), self.sheet_1_col_b_cell_4_style)
52+
53+
@patch('sys.stderr.write')
54+
@patch('argparse.ArgumentParser.parse_args',
55+
return_value=argparse.Namespace(version=False, show_schema=False, test=False,
56+
json_path=None, json=None))
57+
def test_get_cli_args_invalid_args(self, args_mock, stderr_mock):
58+
with self.assertRaises(SystemExit):
59+
get_cli_args()

styleframe/container.py

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
pd_timestamp = pd.tslib.Timestamp
1212

1313

14-
class Container(object):
14+
class Container:
1515
"""
1616
A container class used to store value and style pairs.
1717
Value can be any datatype, and style is a Styler object
@@ -84,16 +84,6 @@ def __rsub__(self, other):
8484
return Container(other.value - self.value)
8585
return Container(other - self.value)
8686

87-
def __div__(self, other):
88-
if isinstance(other, self.__class__):
89-
return Container(self.value / other.value)
90-
return Container(self.value / other)
91-
92-
def __rdiv__(self, other):
93-
if isinstance(other, self.__class__):
94-
return Container(other.value / self.value)
95-
return Container(other / self.value)
96-
9787
def __truediv__(self, other):
9888
if isinstance(other, self.__class__):
9989
return Container(self.value / other.value)
@@ -146,6 +136,3 @@ def __bool__(self):
146136

147137
def __len__(self):
148138
return len(self.value)
149-
150-
# for Python 2
151-
__nonzero__ = __bool__

styleframe/style_frame.py

Lines changed: 33 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
pd_timestamp = pd.tslib.Timestamp
2626

2727

28-
class StyleFrame(object):
28+
class StyleFrame:
2929
"""
3030
A wrapper class that wraps a :class:`pandas.DataFrame` object and represent a stylized dataframe.
3131
Stores container objects that have values and styles that will be applied to excel
@@ -142,7 +142,6 @@ def _get_column_as_letter(self, sheet, column_to_convert, startcol=0):
142142
return column_as_letter
143143

144144
@classmethod
145-
@deprecated_kwargs(('sheetname',))
146145
def read_excel(cls, path, sheet_name=0, read_style=False, use_openpyxl_styles=False,
147146
read_comments=False, **kwargs):
148147
"""
@@ -154,6 +153,8 @@ def read_excel(cls, path, sheet_name=0, read_style=False, use_openpyxl_styles=Fa
154153
:param sheetname:
155154
.. deprecated:: 1.6
156155
Use ``sheet_name`` instead.
156+
.. versionchanged:: 4.0
157+
Removed
157158
:param sheet_name: The sheet name to read. If an integer is provided then it be used as a zero-based
158159
sheet index. Default is 0.
159160
:type sheet_name: str or int
@@ -229,7 +230,6 @@ def _read_style():
229230

230231
sf._columns_width[col_name] = sheet.column_dimensions[sf._get_column_as_letter(sheet, col_name)].width
231232

232-
sheet_name = kwargs.pop('sheetname', sheet_name)
233233
header_arg = kwargs.get('header', 0)
234234
if read_style and isinstance(header_arg, Iterable):
235235
raise ValueError('Not supporting multiple index columns with read style.')
@@ -308,8 +308,14 @@ def read_excel_as_template(cls, path, df, use_df_boundaries=False, **kwargs):
308308

309309
# noinspection PyPep8Naming
310310
@classmethod
311-
def ExcelWriter(cls, path):
312-
return pd.ExcelWriter(path, engine='openpyxl')
311+
def ExcelWriter(cls, path, **kwargs):
312+
"""
313+
A shortcut for :class:`pandas.ExcelWriter`, and accepts any argument it accepts except for ``engine``
314+
"""
315+
316+
if 'engine' in kwargs:
317+
raise ValueError('`engine` argument for StyleFrame.ExcelWriter can not be set')
318+
return pd.ExcelWriter(path, engine='openpyxl', **kwargs)
313319

314320
@property
315321
def row_indexes(self):
@@ -439,6 +445,12 @@ def get_range_of_cells(row_index=None, columns=None):
439445
index_name_cell.style = self._index_header_style.to_openpyxl_style()
440446
for row_index, index in enumerate(self.data_df.index):
441447
try:
448+
date_time_types_to_formats = {pd_timestamp: index.style.date_time_format,
449+
dt.datetime: index.style.date_time_format,
450+
dt.date: index.style.date_format,
451+
dt.time: index.style.time_format}
452+
index.style.number_format = date_time_types_to_formats.get(type(index.value),
453+
index.style.number_format)
442454
style_to_apply = index.style.to_openpyxl_style()
443455
except AttributeError:
444456
style_to_apply = index.style
@@ -460,6 +472,13 @@ def get_range_of_cells(row_index=None, columns=None):
460472
# openpyxl's rows and cols start from 1,1 while the dataframe is 0,0
461473
for col_index, column in enumerate(self.data_df.columns):
462474
try:
475+
date_time_types_to_formats = {pd_timestamp: column.style.date_time_format,
476+
dt.datetime: column.style.date_time_format,
477+
dt.date: column.style.date_format,
478+
dt.time: column.style.time_format}
479+
480+
column.style.number_format = date_time_types_to_formats.get(type(column.value),
481+
column.style.number_format)
463482
style_to_apply = column.style.to_openpyxl_style()
464483
except AttributeError:
465484
style_to_apply = Styler.from_openpyxl_style(column.style, [],
@@ -472,7 +491,7 @@ def get_range_of_cells(row_index=None, columns=None):
472491
if hasattr(column.style, 'comment') and column.style.comment is not None:
473492
column_header_cell.comment = column.style.comment
474493
for row_index, index in enumerate(self.data_df.index):
475-
current_cell = sheet.cell(row=row_index + startrow + 2, column=col_index + startcol + 1)
494+
current_cell = sheet.cell(row=row_index + startrow + (2 if header else 1), column=col_index + startcol + 1)
476495
data_df_style = self.data_df.at[index, column].style
477496
try:
478497
if '=HYPERLINK' in str(current_cell.value):
@@ -483,6 +502,13 @@ def get_range_of_cells(row_index=None, columns=None):
483502
data_df_style.wrap_text = False
484503
data_df_style.shrink_to_fit = False
485504
try:
505+
date_time_types_to_formats = {pd_timestamp: data_df_style.date_time_format,
506+
dt.datetime: data_df_style.date_time_format,
507+
dt.date: data_df_style.date_format,
508+
dt.time: data_df_style.time_format}
509+
510+
data_df_style.number_format = date_time_types_to_formats.get(type(self.data_df.at[index,column].value),
511+
data_df_style.number_format)
486512
style_to_apply = data_df_style.to_openpyxl_style()
487513
except AttributeError:
488514
style_to_apply = Styler.from_openpyxl_style(data_df_style, [],
@@ -596,12 +622,6 @@ def apply_style_by_indexes(self, indexes_to_style, styler_obj, cols_to_style=Non
596622
elif isinstance(indexes_to_style, Container):
597623
indexes_to_style = pd.Index([indexes_to_style])
598624

599-
default_number_formats = {pd_timestamp: utils.number_formats.default_date_time_format,
600-
dt.date: utils.number_formats.default_date_format,
601-
dt.time: utils.number_formats.default_time_format}
602-
603-
orig_number_format = styler_obj.number_format
604-
605625
if cols_to_style is not None and not isinstance(cols_to_style, (list, tuple, set)):
606626
cols_to_style = [cols_to_style]
607627
elif cols_to_style is None:
@@ -613,18 +633,9 @@ def apply_style_by_indexes(self, indexes_to_style, styler_obj, cols_to_style=Non
613633
style_to_apply = Styler.combine(self._default_style, styler_obj)
614634

615635
for index in indexes_to_style:
616-
if orig_number_format == utils.number_formats.general:
617-
style_to_apply.number_format = default_number_formats.get(type(index.value),
618-
utils.number_formats.general)
619636
index.style = style_to_apply
620-
621637
for col in cols_to_style:
622-
cell = self.iloc[self.index.get_loc(index), self.columns.get_loc(col)]
623-
if orig_number_format == utils.number_formats.general:
624-
style_to_apply.number_format = default_number_formats.get(type(cell.value),
625-
utils.number_formats.general)
626-
627-
cell.style = style_to_apply
638+
self.iloc[self.index.get_loc(index), self.columns.get_loc(col)].style = style_to_apply
628639

629640
if height:
630641
# Add offset 2 since rows do not include the headers and they starts from 1 (not 0).

styleframe/styler.py

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from pprint import pformat
77

88

9-
class Styler(object):
9+
class Styler:
1010
"""
1111
Used to represent a style
1212
@@ -43,6 +43,18 @@ class Styler(object):
4343
:param str comment_author:
4444
:param str comment_text:
4545
:param int text_rotation: Integer in the range 0 - 180
46+
47+
.. versionadded:: 4.0
48+
49+
:param date_format:
50+
:type date_format: str: one of :class:`.utils.number_formats` or any other format Excel supports
51+
:param time_format:
52+
:type time_format: str: one of :class:`.utils.number_formats` or any other format Excel supports
53+
:param date_time_format:
54+
:type date_time_format: str: one of :class:`.utils.number_formats` or any other format Excel supports
55+
56+
.. note:: For any of ``date_format``, ``time_format`` and ``date_time_format`` to take effect, the value being
57+
styled must be an actual ``date``/``time``/``datetime`` object.
4658
"""
4759

4860
cache = {}
@@ -52,7 +64,8 @@ def __init__(self, bg_color=None, bold=False, font=utils.fonts.arial, font_size=
5264
border_type=utils.borders.thin, horizontal_alignment=utils.horizontal_alignments.center,
5365
vertical_alignment=utils.vertical_alignments.center, wrap_text=True, shrink_to_fit=True,
5466
fill_pattern_type=utils.fill_pattern_types.solid, indent=0.0, comment_author=None, comment_text=None,
55-
text_rotation=0):
67+
text_rotation=0, date_format=utils.number_formats.date,
68+
time_format=utils.number_formats.time_24_hours, date_time_format=utils.number_formats.date_time):
5669

5770
def get_color_from_string(color_str, default_color=None):
5871
if color_str and color_str.startswith('#'):
@@ -86,11 +99,12 @@ def get_color_from_string(color_str, default_color=None):
8699
self.comment_author = comment_author
87100
self.comment_text = comment_text
88101
self.text_rotation = text_rotation
102+
self.date_format = date_format
103+
self.time_format = time_format
104+
self.date_time_format = date_time_format
89105

90106
def __eq__(self, other):
91-
if not isinstance(other, self.__class__):
92-
return False
93-
return self.__dict__ == other.__dict__
107+
return isinstance(other, self.__class__) and self.__dict__ == other.__dict__
94108

95109
def __hash__(self):
96110
return hash(tuple((k, v) for k, v in sorted(self.__dict__.items())))
@@ -232,7 +246,7 @@ def combine(cls, *styles):
232246
create_style = to_openpyxl_style
233247

234248

235-
class ColorScaleConditionalFormatRule(object):
249+
class ColorScaleConditionalFormatRule:
236250
"""Creates a color scale conditional format rule. Wraps openpyxl's ColorScaleRule.
237251
Mostly should not be used directly, but through StyleFrame.add_color_scale_conditional_formatting
238252
"""

0 commit comments

Comments
 (0)