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

Merge branch 'dev' into 'main'

Dev into main

See merge request !15
parents e9a6db50 ed0e2fea
No related branches found
No related tags found
1 merge request!15Dev into main
Pipeline #768793 passed
......@@ -57,6 +57,7 @@ test:
script:
# - python -m unittest discover -s ./tests/ -p "test*" # deprecated unittest command
- python tests/runner_tests.py
coverage: '/TOTAL.*\s+(\d+%)$/'
# - pip install tox flake8 # you can also use tox
# - tox -e py36,flake8
......
......@@ -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.
- "src_datapath" specifies the path to (raw) data that should be published. It can be a string or a list of strings that specifies all files and directories which will 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.
......@@ -47,12 +47,14 @@ FIGS_AS_LIST = [FIG1, FIG2]
# %% Save figure as tiff-file, but publish also exports the plot to a picture
# file in the destination folder.
for i, figure in enumerate(TAGGED_FIGS):
NAME = "Test"+str(i)+".tiff"
figure.savefig(NAME)
# for i, figure in enumerate(TAGGED_FIGS):
# NAME = "Test"+str(i)+".tiff"
# figure.savefig(NAME)
# %% Publish
# Arguments: Source directory, destination directory, figure, plot name,
# publish-mode).
publish('../../tests', '/home/chief/Dokumente/fst/plotid_python/data',
FIG1, 'Bild', 'individual')
publish(['../README.md', '../tests', '../LICENSE'],
'/home/chief/Dokumente/fst/plotid_python/data',
FIGS_AS_LIST, 'Bild', 'individual')
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Contains the PlotOption class."""
"""Contains the PlotOptions and PublishOptions class."""
class PlotOptions:
......@@ -18,7 +18,7 @@ class PlotOptions:
Figure that will be tagged.
prefix : str
Prefix that is placed before the ID.
id_method : int
id_method : str
Method that decides which method is used to generate the ID.
rotation : int
Rotation angle for the ID.
......@@ -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
......
......@@ -8,7 +8,7 @@ the plot is based on. Additionally, the script that produced the plot will be
copied to the destination directory.
Functions:
publish(path-like, path-like, figure, string, string) -> None
publish(str, str, figure, str, str) -> None
"""
import os
......@@ -18,92 +18,229 @@ import warnings
from plotid.save_plot import save_plot
def publish(src_datapath, dst_path, figure, plot_name,
data_storage='individual'):
class PublishOptions:
"""
Save plot, data and measuring script.
Container objects which include all publish options provided by plotid.
Parameters
Methods
-------
__init__
validate_input: Check if input is correct type.
Attributes
----------
src_datapath : string
Path to data that should be published.
dst_path : string
src_datapaths : str or list of str
Path(s) to data that should be published.
dst_path : str
Path to the destination directory.
figure : figure object
Figure that was tagged and now should be saved as picture.
plot_name: string
plot_name : str
Name for the exported plot.
data_storage: string
data_storage : str
Method how the data should be stored. Available options:
centralized: The raw data will copied only once. All other plots
centralized : The data files 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 data files will be copied to a
separate folder for every plot.
"""
def __init__(self, src_datapaths, dst_path, figure, plot_names,
data_storage):
self.src_datapaths = src_datapaths
self.dst_path = dst_path
self.figure = figure
self.plot_names = plot_names
self.data_storage = data_storage
self.dst_path_head, self.dst_dirname = os.path.split(self.dst_path)
def validate_input(self):
"""
Validate if input for PublishOptions is correct type.
Raises
------
FileNotFoundError
If the path to the source or the destination directory does not
exist.
TypeError
If input data is of wrong type.
Returns
-------
None.
"""
# Check if source directory exists
if not os.path.exists(src_datapath):
if isinstance(self.src_datapaths, str):
self.src_datapaths = [self.src_datapaths]
if isinstance(self.src_datapaths, list):
for path in self.src_datapaths:
if not isinstance(path, str):
raise TypeError(f'{path} is not a string.')
# Check if source directory and files exist
if not os.path.exists(path):
raise FileNotFoundError('The specified source directory'
'does not exist.')
f'/file {path} does not exist.')
else:
raise TypeError('The source directory/files are neither '
'a string nor a list.')
# Check if destination directory is allowed path
dst_path_head, dst_dirname = os.path.split(dst_path)
if not os.path.exists(dst_path_head):
if not os.path.exists(self.dst_path_head):
raise FileNotFoundError('The specified destination directory '
'does not exist.')
# If dst dir already exists ask user if it should be overwritten or not.
if os.path.isdir(dst_path):
warnings.warn(f'Folder "{dst_dirname}" already exists – '
# 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):
raise TypeError('The specified data_storage method is not a '
'string.')
def export(self):
"""
Export the plot and copy specified files to the destiantion folder.
Raises
------
RuntimeError
If user does not want to overwrite existing folder.
ValueError
If non-supported data_storage method is given.
Returns
-------
None.
"""
# Export plot figure to picture.
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):
warnings.warn(f'Folder "{self.dst_dirname}" already exists – '
'plot has already been published.')
overwrite_dir = input('Do you want to overwrite the existing folder?'
' (yes/no[default])\n')
overwrite_dir = input('Do you want to overwrite the existing'
' folder? (yes/no[default])\n')
if overwrite_dir in ('yes', 'y'):
shutil.rmtree(dst_path)
shutil.rmtree(self.dst_path)
else:
raise RuntimeError('PlotID has finished without an export.\n'
'Rerun TagPlot if you need a new ID or '
'consider overwriting.')
# Export plot figure to picture.
plot_path = save_plot(figure, plot_name)
# Create invisible folder
dst_path_invisible = os.path.join(dst_path_head, '.' + dst_dirname)
# If invisible Folder exists, delete it (publish was not successful before)
dst_path_invisible = os.path.join(self.dst_path_head,
'.' + self.dst_dirname)
# If invisible Folder exists, delete it
# (publish was not successful before)
if os.path.isdir(dst_path_invisible):
shutil.rmtree(dst_path_invisible)
match data_storage:
match self.data_storage:
case 'centralized':
self.centralized_data_storage()
case 'individual':
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(s) {plot_paths},\nyour'
f' data {self.src_datapaths}\nand your script {sys.argv[0]}\n'
f'were copied to {self.dst_path}\nin {self.data_storage} mode.')
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
case 'individual':
def individual_data_storage(self, destination, pic_paths):
"""
Store all the data in an individual directory.
Parameters
----------
destination : path-like
Directory where the data should be stored.
pic_path : path-like
Path to the picture file that will be stored in destination.
Returns
-------
None.
"""
# Copy all files to destination directory
print('Copying data has been started. Depending on the size of'
' your data this may take a while...')
# Copy data to invisible folder
shutil.copytree(src_datapath, dst_path_invisible)
os.makedirs(destination)
# Copy data to destination folder
for path in self.src_datapaths:
try:
shutil.copytree(path, destination, dirs_exist_ok=True)
except NotADirectoryError:
shutil.copy2(path, destination)
# Copy script that calls this function to folder
shutil.copy2(sys.argv[0], dst_path_invisible)
# Copy plot file to folder
if os.path.isfile(plot_path):
shutil.copy2(plot_path, dst_path_invisible)
os.remove(plot_path)
case _:
raise ValueError('The data storage method {data_storage} '
'is not available.')
shutil.copy2(sys.argv[0], destination)
# Copy plot files to folder
for path in pic_paths:
if os.path.isfile(path):
shutil.copy2(path, destination)
os.remove(path)
print('Copying data finished. Starting to clean up.')
# If export was successful, make the directory visible
os.rename(dst_path_invisible, dst_path)
print(f'Publish was successful.\nYour plot "{plot_path}",\n'
f'your data "{src_datapath}"\nand your script "{sys.argv[0]}"\n'
f'were copied to {dst_path}\nin {data_storage} mode.')
def publish(src_datapath, dst_path, figure, plot_name,
data_storage='individual'):
"""
Save plot, data and measuring script.
Parameters
----------
src_datapath : str
Path to data that should be published.
dst_path : str
Path to the destination directory.
figure : figure object
Figure that was tagged and now should be saved as picture.
plot_name : str
Name for the exported plot.
data_storage : str
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 [default]: The complete raw data will be copied to a
folder for every plot, respectively.
Returns
-------
None.
"""
publish_container = PublishOptions(src_datapath, dst_path, figure,
plot_name, data_storage)
publish_container.validate_input()
publish_container.export()
......@@ -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'
......
......@@ -23,11 +23,10 @@ result = testRunner.run(tests)
cov.stop()
cov.save()
cov.report(show_missing=True)
if result.wasSuccessful():
covered = cov.report()
assert covered > 90, "Not enough coverage."
covered = cov.report(show_missing=True, precision=2)
assert covered > 95, "Not enough coverage."
sys.exit(0)
else:
sys.exit(1)
......@@ -14,12 +14,16 @@ from plotid.publish import publish
SRC_DIR = 'test_src_folder'
SRC_FILES = ['test_file1.txt', 'test_file2.jpg', 'test_file3.exe']
PIC_NAME = 'test_picture'
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):
......@@ -28,9 +32,11 @@ class TestPublish(unittest.TestCase):
"""
def setUp(self):
""" Generate source and destination directory and test image. """
""" Generate source and destination directory and source files. """
os.makedirs(SRC_DIR, exist_ok=True)
os.makedirs(DST_PARENT_DIR, exist_ok=True)
for file in SRC_FILES:
open(file, 'w', encoding='utf-8').close()
# Skip test if tests are run from command line.
@unittest.skipIf(not os.path.isfile(sys.argv[0]), 'Publish is not called '
......@@ -38,20 +44,53 @@ 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, name + '.png'))
def test_publish_multiple_src_files(self):
"""
Test publish with multiple source files and check
if the exported data files exist.
"""
files_and_dir = list(SRC_FILES)
files_and_dir.append(SRC_DIR)
publish(files_and_dir, DST_PATH, FIGS_AS_LIST, PIC_NAME_LIST,
'individual')
assert os.path.isdir(DST_PATH)
for file in SRC_FILES:
assert os.path.isfile(os.path.join(DST_PATH, file))
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_src_directory_type(self):
""" Test if Error is raised when source directory is not a string."""
with self.assertRaises(TypeError):
publish([SRC_DIR, 4], DST_PATH, FIG, PIC_NAME, 'individual')
with self.assertRaises(TypeError):
publish(4, 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 +104,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 +119,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 +134,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,19 +146,34 @@ 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_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')
with self.assertRaises(TypeError):
publish(SRC_DIR, DST_PATH, FIG, (), 'individual')
with self.assertRaises(TypeError):
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)
with self.assertRaises(TypeError):
publish(SRC_DIR, DST_PATH, FIG, PIC_NAME, [])
def tearDown(self):
""" Delete all files created in setUp. """
shutil.rmtree(SRC_DIR)
shutil.rmtree(DST_PARENT_DIR)
for file in SRC_FILES:
os.remove(file)
if __name__ == '__main__':
......
......@@ -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