Skip to content

Commit 29fdc63

Browse files
author
James Draper
authored
Merge pull request draperjames#29 from draperjames/zbranch3
Added DataFrameModelManager and some tests. Added new main test class…
2 parents 02eefb2 + a355603 commit 29fdc63

File tree

6 files changed

+302
-3
lines changed

6 files changed

+302
-3
lines changed
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
# -*- coding: utf-8 -*-
2+
"""
3+
Created on Sun Nov 27 20:57:39 2016
4+
5+
@author: Zeke
6+
"""
7+
import os
8+
import pandas as pd
9+
from qtpandas.models.DataFrameModel import DataFrameModel, read_file
10+
from collections import defaultdict
11+
import datetime
12+
from compat import QtCore
13+
14+
15+
class DataFrameModelManager(QtCore.QObject):
16+
"""
17+
A central storage unit for managing
18+
DataFrameModels.
19+
"""
20+
signalNewModelRead = QtCore.Signal(str)
21+
signalModelDestroyed = QtCore.Signal(str)
22+
23+
def __init__(self):
24+
QtCore.QObject.__init__(self)
25+
self._models = {}
26+
self._updates = defaultdict(list)
27+
self._paths_read = []
28+
self._paths_updated = []
29+
30+
@property
31+
def file_paths(self):
32+
"""Returns a list of the currently stored file paths"""
33+
return list(self._models.keys())
34+
35+
@property
36+
def models(self):
37+
"""Returns a list of all currently stored DataFrameModels"""
38+
return list(self._models.values())
39+
40+
@property
41+
def last_path_read(self):
42+
"""Returns the last path read (via the DataFrameModelManager.read_file method)"""
43+
if self._paths_read:
44+
return self._paths_read[-1]
45+
else:
46+
return None
47+
48+
@property
49+
def last_path_updated(self):
50+
"""Returns the last path to register an update. (or None)"""
51+
if self._paths_updated:
52+
return self._paths_updated[-1]
53+
else:
54+
return None
55+
56+
def save_file(self, filepath, save_as=None, keep_orig=False, **kwargs):
57+
"""
58+
Saves a DataFrameModel to a file.
59+
60+
:param filepath: (str)
61+
The filepath of the DataFrameModel to save.
62+
:param save_as: (str, default None)
63+
The new filepath to save as.
64+
:param keep_orig: (bool, default False)
65+
True keeps the original filepath/DataFrameModel if save_as is specified.
66+
:param kwargs:
67+
pandas.DataFrame.to_excel(**kwargs) if .xlsx
68+
pandas.DataFrame.to_csv(**kwargs) otherwise.
69+
:return: None
70+
"""
71+
df = self._models[filepath].dataFrame()
72+
kwargs['index'] = kwargs.get('index', False)
73+
74+
if save_as is not None:
75+
to_path = save_as
76+
else:
77+
to_path = filepath
78+
79+
ext = os.path.splitext(to_path)[1].lower()
80+
81+
if ext == ".xlsx":
82+
kwargs.pop('sep', None)
83+
df.to_excel(to_path, **kwargs)
84+
85+
elif ext in ['.csv','.txt']:
86+
df.to_csv(to_path, **kwargs)
87+
88+
else:
89+
raise NotImplementedError("Cannot save file of type {}".format(ext))
90+
91+
if save_as is not None:
92+
if keep_orig is False:
93+
# Re-purpose the original model
94+
# Todo - capture the DataFrameModelManager._updates too
95+
model = self._models.pop(filepath)
96+
model._filePath = to_path
97+
else:
98+
# Create a new model.
99+
model = DataFrameModel()
100+
model.setDataFrame(df, copyDataFrame=True, filePath=to_path)
101+
102+
self._models[to_path] = model
103+
104+
def set_model(self, df_model: DataFrameModel, file_path):
105+
"""
106+
Sets a DataFrameModel and registers it to the given file_path.
107+
:param df_model: (DataFrameModel)
108+
The DataFrameModel to register.
109+
:param file_path:
110+
The file path to associate with the DataFrameModel.
111+
*Overrides the current filePath on the DataFrameModel (if any)
112+
:return: None
113+
"""
114+
assert isinstance(df_model, DataFrameModel), "df_model argument must be a DataFrameModel!"
115+
df_model._filePath = file_path
116+
117+
try:
118+
self._models[file_path]
119+
except KeyError:
120+
self.signalNewModelRead.emit(file_path)
121+
122+
self._models[file_path] = df_model
123+
124+
def get_model(self, filepath):
125+
"""
126+
Returns the DataFrameModel registered to filepath
127+
"""
128+
return self._models[filepath]
129+
130+
def get_frame(self, filepath):
131+
"""Returns the DataFrameModel.dataFrame() registered to filepath """
132+
return self._models[filepath].dataFrame()
133+
134+
def update_file(self, filepath, df, notes=None):
135+
"""
136+
Sets a new DataFrame for the DataFrameModel registered to filepath.
137+
:param filepath (str)
138+
The filepath to the DataFrameModel to be updated
139+
:param df (pandas.DataFrame)
140+
The new DataFrame to register to the model.
141+
142+
:param notes (str, default None)
143+
Optional notes to register along with the update.
144+
145+
"""
146+
assert isinstance(df, pd.DataFrame), "Cannot update file with type '{}'".format(type(df))
147+
148+
self._models[filepath].setDataFrame(df, copyDataFrame=False)
149+
150+
if notes:
151+
update = dict(date=pd.Timestamp(datetime.datetime.now()),
152+
notes=notes)
153+
154+
self._updates[filepath].append(update)
155+
self._paths_updated.append(filepath)
156+
157+
def remove_file(self, filepath):
158+
"""
159+
Removes the DataFrameModel from being registered.
160+
:param filepath: (str)
161+
The filepath to delete from the DataFrameModelManager.
162+
:return: None
163+
"""
164+
self._models.pop(filepath)
165+
self._updates.pop(filepath, default=None)
166+
self.signalModelDestroyed.emit(filepath)
167+
168+
def read_file(self, filepath, **kwargs) -> DataFrameModel:
169+
"""
170+
Reads a filepath into a DataFrameModel and registers
171+
it.
172+
Example use:
173+
dfmm = DataFrameModelManger()
174+
dfmm.read_file(path_to_file)
175+
dfm = dfmm.get_model(path_to_file)
176+
df = dfm.get_frame(path_to_file)
177+
178+
:param filepath: (str)
179+
The filepath to read
180+
:param kwargs:
181+
.xlsx files: pandas.read_excel(**kwargs)
182+
.csv files: pandas.read_csv(**kwargs)
183+
:return: DataFrameModel
184+
"""
185+
try:
186+
model = self._models[filepath]
187+
except KeyError:
188+
model = read_file(filepath, **kwargs)
189+
self._models[filepath] = model
190+
self.signalNewModelRead.emit(filepath)
191+
finally:
192+
self._paths_read.append(filepath)
193+
194+
return self._models[filepath]
195+

tests/main.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import os
2+
import pytest
3+
import pandas as pd
4+
5+
6+
class MainTestClass(object):
7+
8+
@pytest.fixture
9+
def df(self) -> pd.DataFrame:
10+
sample_cols = ['id', 'name', 'address', 'updated']
11+
sample_recs = [[1000, 'zeke', '123 street'],
12+
[1001, 'larry', '688 road'],
13+
[1002, 'fred', '585 lane']]
14+
for rec in sample_recs:
15+
rec.append(pd.NaT)
16+
return pd.DataFrame(sample_recs, columns=sample_cols)
17+
18+
@pytest.fixture
19+
def output_dir(self) -> str:
20+
fp = os.path.join(os.path.dirname(__file__), "output")
21+
if not os.path.exists(fp):
22+
os.mkdir(fp)
23+
return fp
24+
25+
@pytest.fixture
26+
def fixtures_dir(self) -> str:
27+
fp = os.path.join(os.path.dirname(__file__), "fixtures")
28+
if not os.path.exists(fp):
29+
os.mkdir(fp)
30+
return fp
31+
32+
@pytest.fixture
33+
def project_root_dir(self, fixtures_dir):
34+
return os.path.join(fixtures_dir, "fixed_root_dir/fixed_project")
35+
36+
@pytest.fixture
37+
def project_log_dir(self, project_root_dir):
38+
return os.path.join(project_root_dir, "logs")
39+
40+
@pytest.fixture
41+
def project_settings_path(self, project_root_dir):
42+
return os.path.join(project_root_dir, "sample_project_config.ini")
43+
44+
@pytest.fixture
45+
def example_file_path(self, project_root_dir):
46+
return os.path.join(project_root_dir, "example.csv")
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
,id,name,address,updated
2+
0,1000,zeke,123 street,
3+
1,1001,larry,688 road,
4+
2,1002,fred,585 lane,

tests/test_BigIntSpinbox.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55

66
import pytest
7-
87
from qtpandas.views.BigIntSpinbox import BigIntSpinbox
98

109
class TestClass(object):
@@ -13,6 +12,7 @@ class TestClass(object):
1312
def spinbox(self, qtbot):
1413
widget = BigIntSpinbox()
1514
qtbot.addWidget(widget)
15+
1616
return widget
1717

1818
def test_init(self, spinbox):

tests/test_DataFrameModel.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -615,8 +615,6 @@ def bad_func(df):
615615

616616
assert expected
617617

618-
619-
620618
def test_edit_data(self, model):
621619
index = model.index(0, 0)
622620
currentData = index.data()
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import os
2+
import pytest
3+
from qtpandas.models.DataFrameModelManager import DataFrameModelManager
4+
from tests.main import MainTestClass
5+
6+
7+
class TestClass(MainTestClass):
8+
9+
@pytest.fixture
10+
def sample_file(self, df, output_dir) -> str:
11+
file_path = os.path.join(output_dir, "test_dfm_manager_file.csv")
12+
if not os.path.exists(file_path):
13+
df.to_csv(file_path)
14+
return file_path
15+
16+
@pytest.fixture
17+
def manager(self) -> DataFrameModelManager:
18+
return DataFrameModelManager()
19+
20+
def test_read_file_basics(self, sample_file, manager):
21+
manager.read_file(sample_file)
22+
model = manager.get_model(sample_file)
23+
df = manager.get_frame(sample_file)
24+
assert sample_file in manager.file_paths
25+
assert model.dataFrame().index.size == df.index.size
26+
assert sample_file in manager._paths_read
27+
28+
def test_save_file(self, sample_file, manager):
29+
30+
manager.read_file(sample_file)
31+
check_path = os.path.splitext(sample_file)[0] + "_check.csv"
32+
33+
manager.save_file(sample_file, save_as=check_path, keep_orig=True)
34+
35+
assert check_path in manager.file_paths
36+
assert sample_file in manager.file_paths
37+
assert os.path.exists(check_path)
38+
os.remove(check_path)
39+
40+
manager.save_file(sample_file, save_as=check_path, keep_orig=False)
41+
42+
assert check_path in manager.file_paths
43+
assert sample_file not in manager.file_paths
44+
assert os.path.exists(check_path)
45+
os.remove(check_path)
46+
47+
48+
49+
50+
51+
52+
53+
54+
55+
56+

0 commit comments

Comments
 (0)