Skip to content
Snippets Groups Projects
Commit 420870d0 authored by Mayr, Hannes's avatar Mayr, Hannes
Browse files

Support publishing multiple figures at once.

parent 0865f565
No related branches found
No related tags found
2 merge requests!15Dev into main,!14Publish oop
......@@ -43,7 +43,7 @@ plotID has two main functionalities:
2. Export the resulting file to a specified directory along the corresponding research data, the plot is based on. Additionally, the script that created the plot will also be copied to the directory.
### tagplot()
Tag your figure/plot with an ID.
Tag your figure/plot with an ID. It is possible to tag multiple figures at once.
`tagplot(figures, plot_engine)`
The variable "figures" can be a single figure or a list of multiple figures.
The argument "plot_engine" defines which plot engine was used to create the figures. It also determines which plot engine plotID uses to place the ID on the plot. Currently supported plot engines are:
......@@ -69,13 +69,13 @@ FIGS_AS_LIST = [FIG1, FIG2]
### publish()
Save plot, data and measuring script.
Save plot, data and measuring script. It is possible to export multiple figures at once.
`publish(src_datapath, dst_path, figure, plot_name)`
- "src_datapath" specifies the path to (raw) data that should be published.
- "dst_path" is the path to the destination directory, where all the data should be copied/exported to.
- "figure" expects the figure that was tagged and now should be saved as picture.
- "plot_name" will be the file name for the exported plot.
- "figure" expects the figure or a list of figures that were tagged and now should be saved as pictures.
- "plot_names" will be the file names for the exported plots. If you give only one plot name but several figures, plotID will name the exported plots with an appended number, e.g. example_fig1.png, example_fig2.png, ...
Optional parameters can be used to customize the publish process.
- data_storage: str, optional
......@@ -96,6 +96,7 @@ If you want to build plotID yourself, follow these steps:
4. Build the package
`python3 -m build`
## Documentation
If you have more questions about plotID, please have a look at the [documentation](link-to-docs).
Also have a look at the example.py that is shipped with plotID.
......@@ -54,5 +54,5 @@ for i, figure in enumerate(TAGGED_FIGS):
# %% Publish
# Arguments: Source directory, destination directory, figure, plot name,
# publish-mode).
publish('../../tests', '/home/chief/Dokumente/fst/plotid_python/data',
FIG1, 'Bild', 'individual')
publish('../tests', '/home/chief/Dokumente/fst/plotid_python/data',
FIGS_AS_LIST, 'Bild', 'individual')
......@@ -49,7 +49,6 @@ class PlotOptions:
0, if all checks succeed.
"""
# %% Validate inputs
# Input validation for figs is done in submodules tagplot_$engine.py
if isinstance(self.prefix, str):
pass
......
......@@ -41,17 +41,17 @@ class PublishOptions:
Method how the data should be stored. Available options:
centralized : The raw data will copied only once. All other plots
will reference this data via sym link.
individual: The complete raw data will be copied to a folder for
every plot, respectively. This is the default value.
individual [default]: The complete raw data will be copied to a
folder for every plot, respectively.
"""
def __init__(self, src_datapath, dst_path, figure, plot_name,
def __init__(self, src_datapath, dst_path, figure, plot_names,
data_storage):
self.src_datapath = src_datapath
self.dst_path = dst_path
self.figure = figure
self.plot_name = plot_name
self.plot_names = plot_names
self.data_storage = data_storage
self.dst_path_head, self.dst_dirname = os.path.split(self.dst_path)
......@@ -82,9 +82,17 @@ class PublishOptions:
raise FileNotFoundError('The specified destination directory '
'does not exist.')
# Check if plot_name is a string
if not isinstance(self.plot_name, str):
raise TypeError('The specified plot_name is not a string.')
# Check if plot_name is a string or a list of strings
if isinstance(self.plot_names, str):
self.plot_names = [self.plot_names]
if isinstance(self.plot_names, list):
for name in self.plot_names:
if not isinstance(name, str):
raise TypeError('The list of plot_names contains an object'
' which is not a string.')
else:
raise TypeError('The specified plot_names is neither a string nor'
' a list of strings.')
# Check if data_storage is a string
if not isinstance(self.data_storage, str):
......@@ -108,7 +116,7 @@ class PublishOptions:
"""
# Export plot figure to picture.
plot_path = save_plot(self.figure, self.plot_name)
plot_paths = save_plot(self.figure, self.plot_names)
# If dst dir already exists ask user if it should be overwritten.
if os.path.isdir(self.dst_path):
......@@ -135,21 +143,32 @@ class PublishOptions:
match self.data_storage:
case 'centralized':
# Does nothing, not implemented yet
pass
self.centralized_data_storage()
case 'individual':
self.individual_data_storage(dst_path_invisible, plot_path)
self.individual_data_storage(dst_path_invisible, plot_paths)
case _:
raise ValueError('The data storage method {data_storage} '
'is not available.')
# If export was successful, make the directory visible
os.rename(dst_path_invisible, self.dst_path)
print(f'Publish was successful.\nYour plot "{plot_path}",\nyour'
f' data "{self.src_datapath}"\nand your script "{sys.argv[0]}"\n'
print(f'Publish was successful.\nYour plot(s) {plot_paths},\nyour'
f' data {self.src_datapath}\nand your script {sys.argv[0]}\n'
f'were copied to {self.dst_path}\nin {self.data_storage} mode.')
def individual_data_storage(self, destination, pic_path):
def centralized_data_storage(self):
"""
Store the data only in one directory and link all others to it.
Returns
-------
None.
"""
# Does nothing, not implemented yet
pass
def individual_data_storage(self, destination, pic_paths):
"""
Store all the data in an individual directory.
......@@ -174,9 +193,10 @@ class PublishOptions:
# Copy script that calls this function to folder
shutil.copy2(sys.argv[0], destination)
# Copy plot file to folder
if os.path.isfile(pic_path):
shutil.copy2(pic_path, destination)
os.remove(pic_path)
for path in pic_paths:
if os.path.isfile(path):
shutil.copy2(path, destination)
os.remove(path)
def publish(src_datapath, dst_path, figure, plot_name,
......@@ -198,8 +218,8 @@ def publish(src_datapath, dst_path, figure, plot_name,
Method how the data should be stored. Available options:
centralized : The raw data will copied only once. All other plots
will reference this data via sym link.
individual: The complete raw data will be copied to a folder for
every plot, respectively. This is the default value.
individual [default]: The complete raw data will be copied to a
folder for every plot, respectively.
Returns
-------
......
......@@ -7,33 +7,59 @@ Functions:
save_plot(figure, string) -> path-like
"""
import warnings
import matplotlib
import matplotlib.pyplot as plt
def save_plot(figure, plot_name, extension='png'):
def save_plot(figures, plot_names, extension='png'):
"""
Export plot.
Export plot(s).
Parameters
----------
figure : figure object
figure : list of/single figure object
Figure that was tagged and now should be saved as picture.
plot_name: Name of the file where the plot will be saved to.
extension: string
plot_name : list of strings
Names of the files where the plots will be saved to.
extension : str
File extension for the plot export.
Returns
-------
plot_path: Name of the created picture.
plot_path : list
Names of the created pictures.
"""
match type(figure):
case matplotlib.figure.Figure:
plt.figure(figure)
plot_path = plot_name + '.' + extension
plt.savefig(plot_path)
case _:
raise TypeError('The given figure is not a valid figure object.')
# Check if figs is a valid figure or a list of valid figures
if isinstance(figures, matplotlib.figure.Figure):
figures = [figures]
if isinstance(figures, list):
for fig in figures:
if isinstance(fig, matplotlib.figure.Figure):
pass
else:
raise TypeError('Figures is neither a valid matplotlib-figure nor'
' a list of matplotlib-figures.')
if len(plot_names) < len(figures):
warnings.warn('There are more figures than plot names. The first name'
' will be taken for all plots with an appended number.')
first_name = plot_names[0]
plot_names = [None] * len(figures)
for i, _ in enumerate(plot_names):
plot_names[i] = first_name + f'{i+1}'
elif len(plot_names) > len(figures):
raise IndexError('There are more plot names than figures.')
plot_path = []
# match type(figures):
# case matplotlib.figure.Figure:
for i, fig in enumerate(figures):
plt.figure(fig)
plot_path.append(plot_names[i] + '.' + extension)
plt.savefig(plot_path[i])
# case _:
# raise TypeError('The given figure is not a valid figure object.')
return plot_path
......@@ -27,12 +27,13 @@ def tagplot_matplotlib(plotid_object):
# Check if figs is a valid figure or a list of valid figures
if isinstance(plotid_object.figs, matplotlib.figure.Figure):
plotid_object.figs = [plotid_object.figs]
elif isinstance(plotid_object.figs, list):
if isinstance(plotid_object.figs, list):
for figure in plotid_object.figs:
if isinstance(figure, matplotlib.figure.Figure):
pass
else:
raise TypeError('Figure is not a valid matplotlib-figure.')
raise TypeError('Figures is neither a valid matplotlib-figure nor'
' a list of matplotlib-figures.')
fontsize = 'small'
color = 'grey'
......
......@@ -19,7 +19,10 @@ DST_DIR = 'test_dst_folder'
DST_PARENT_DIR = 'test_parent'
DST_PATH = os.path.join(DST_PARENT_DIR, DST_DIR)
INVISIBLE_PATH = os.path.join(DST_PARENT_DIR, '.' + DST_DIR)
fig = plt.figure()
FIG = plt.figure()
FIG2 = plt.figure()
FIGS_AS_LIST = [FIG, FIG2]
PIC_NAME_LIST = [PIC_NAME, 'second_picture']
class TestPublish(unittest.TestCase):
......@@ -38,20 +41,33 @@ class TestPublish(unittest.TestCase):
'copied.')
def test_publish(self):
""" Test publish and check if an exported picture file exists. """
publish(SRC_DIR, DST_PATH, fig, PIC_NAME, 'individual')
publish(SRC_DIR, DST_PATH, FIG, PIC_NAME, 'individual')
assert os.path.isfile(os.path.join(DST_PATH, PIC_NAME + '.png'))
# Skip test if tests are run from command line.
@unittest.skipIf(not os.path.isfile(sys.argv[0]), 'Publish is not called '
'from a Python script. Therefore, the script cannot be '
'copied.')
def test_publish_multiple_figs(self):
"""
Test publish with multiple figures and check if all exported picture
files exist.
"""
publish(SRC_DIR, DST_PATH, FIGS_AS_LIST, PIC_NAME_LIST, 'individual')
for name in PIC_NAME_LIST:
assert os.path.isfile(os.path.join(DST_PATH, PIC_NAME + '.png'))
def test_src_directory(self):
""" Test if Error is raised when source directory does not exist."""
with self.assertRaises(FileNotFoundError):
publish('not_existing_folder', DST_PATH, fig,
publish('not_existing_folder', DST_PATH, FIG,
PIC_NAME, 'individual')
def test_dst_directory(self):
""" Test if Error is raised when destination dir does not exist."""
with self.assertRaises(FileNotFoundError):
publish(SRC_DIR, 'not_existing_folder',
fig, PIC_NAME, 'individual')
FIG, PIC_NAME, 'individual')
# Skip test if tests are run from command line.
@unittest.skipIf(not os.path.isfile(sys.argv[0]), 'Publish is not called '
......@@ -65,7 +81,7 @@ class TestPublish(unittest.TestCase):
os.mkdir(DST_PATH)
# Mock user input as 'yes'
with patch('builtins.input', return_value='yes'):
publish(SRC_DIR, DST_PATH, fig, PIC_NAME, 'individual')
publish(SRC_DIR, DST_PATH, FIG, PIC_NAME, 'individual')
# Skip test if tests are run from command line.
@unittest.skipIf(not os.path.isfile(sys.argv[0]), 'Publish is not called '
......@@ -80,7 +96,7 @@ class TestPublish(unittest.TestCase):
# Mock user input as 'no'
with patch('builtins.input', return_value='no'):
with self.assertRaises(RuntimeError):
publish(SRC_DIR, DST_PATH, fig, PIC_NAME, 'individual')
publish(SRC_DIR, DST_PATH, FIG, PIC_NAME, 'individual')
# Skip test if tests are run from command line.
@unittest.skipIf(not os.path.isfile(sys.argv[0]), 'Publish is not called '
......@@ -95,7 +111,7 @@ class TestPublish(unittest.TestCase):
# Mock user input as empty (no should be default).
with patch('builtins.input', return_value=''):
with self.assertRaises(RuntimeError):
publish(SRC_DIR, DST_PATH, fig, PIC_NAME, 'individual')
publish(SRC_DIR, DST_PATH, FIG, PIC_NAME, 'individual')
# Skip test if tests are run from command line.
@unittest.skipIf(not os.path.isfile(sys.argv[0]), 'Publish is not called '
......@@ -107,25 +123,27 @@ class TestPublish(unittest.TestCase):
directory from a previous run (delete the folder and proceed).
"""
os.mkdir(INVISIBLE_PATH)
publish(SRC_DIR, DST_PATH, fig, PIC_NAME, 'individual')
publish(SRC_DIR, DST_PATH, FIG, PIC_NAME, 'individual')
def test_plot_name(self):
def test_plot_names(self):
""" Test if Error is raised if plot_name is not a string. """
with self.assertRaises(TypeError):
publish(SRC_DIR, DST_PATH, fig, 7.6, 'individual')
publish(SRC_DIR, DST_PATH, FIG, 7.6, 'individual')
with self.assertRaises(TypeError):
publish(SRC_DIR, DST_PATH, FIG, (), 'individual')
with self.assertRaises(TypeError):
publish(SRC_DIR, DST_PATH, fig, (), 'individual')
publish(SRC_DIR, DST_PATH, FIG, ['test', 3], 'individual')
def test_data_storage(self):
"""
Test if Error is raised when unsupported storage method was chosen.
"""
with self.assertRaises(ValueError):
publish(SRC_DIR, DST_PATH, fig, PIC_NAME, 'none_existing_method')
publish(SRC_DIR, DST_PATH, FIG, PIC_NAME, 'none_existing_method')
with self.assertRaises(TypeError):
publish(SRC_DIR, DST_PATH, fig, PIC_NAME, 3)
publish(SRC_DIR, DST_PATH, FIG, PIC_NAME, 3)
with self.assertRaises(TypeError):
publish(SRC_DIR, DST_PATH, fig, PIC_NAME, [])
publish(SRC_DIR, DST_PATH, FIG, PIC_NAME, [])
def tearDown(self):
""" Delete all files created in setUp. """
......
......@@ -20,9 +20,26 @@ class TestSavePlot(unittest.TestCase):
def test_save_plot(self):
""" Test if save_plot succeeds with valid arguments. """
save_plot(FIGURE, PLOT_NAME, extension='jpg')
plot_paths = save_plot(FIGURE, [PLOT_NAME], extension='jpg')
self.assertIsInstance(plot_paths, list)
os.remove(PLOT_NAME + '.jpg')
def test_more_figs_than_names(self):
"""
Test if a warning is raised if more figures than plot names are given.
Additionally, check if files are named correctly.
"""
with self.assertWarns(Warning):
save_plot([FIGURE, FIGURE, FIGURE], [PLOT_NAME])
for i in (1, 2, 3):
assert os.path.isfile(PLOT_NAME + f'{i}.png')
os.remove(PLOT_NAME + f'{i}.png')
def test_more_names_than_figs(self):
""" Test if Error is raised if more names than figures are given. """
with self.assertRaises(IndexError):
save_plot([FIGURE, FIGURE], [PLOT_NAME, PLOT_NAME, PLOT_NAME])
def test_wrong_fig_type(self):
""" Test if Error is raised when not a figure object is given. """
with self.assertRaises(TypeError):
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment