From b432fd7b6a22667c54a690fdc4e487c23e500f45 Mon Sep 17 00:00:00 2001 From: "Mayr, Hannes" Date: Tue, 23 Aug 2022 15:24:20 +0200 Subject: [PATCH 01/18] Allow publishing data to a directory with trailing slash. --- plotid/example.py | 2 +- plotid/publish.py | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/plotid/example.py b/plotid/example.py index 7e41ff8..344f310 100644 --- a/plotid/example.py +++ b/plotid/example.py @@ -56,5 +56,5 @@ IMGS_AS_LIST = [IMG1, IMG2] # plots or images. publish(['../README.md', '../docs', '../LICENSE'], - '/home/chief/Dokumente/fst/plotid_python/data', + '/home/chief/Dokumente/fst/plotid_python/data/', TAGGED_FIGS, 'Bild') diff --git a/plotid/publish.py b/plotid/publish.py index 79a926b..2b7f2d1 100644 --- a/plotid/publish.py +++ b/plotid/publish.py @@ -55,6 +55,13 @@ class PublishOptions: self.data_storage = data_storage self.dst_path_head, self.dst_dirname = os.path.split(self.dst_path) + # If the second string after os.path.split is empty, + # a trailing slash was given. + # To get the dir name correctly, split the first string again. + if not self.dst_dirname: + self.dst_path_head, self.dst_dirname = os.path.split( + self.dst_path_head) + def validate_input(self): """ Validate if input for PublishOptions is correct type. -- GitLab From a9b0723dd770a9f124ac2a073b9bec9e8ab6cf0d Mon Sep 17 00:00:00 2001 From: "Mayr, Hannes" Date: Tue, 23 Aug 2022 15:34:56 +0200 Subject: [PATCH 02/18] Adjust unittest for covering dst_dir with trailing slash. --- tests/test_publish.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_publish.py b/tests/test_publish.py index 159f2be..4956734 100644 --- a/tests/test_publish.py +++ b/tests/test_publish.py @@ -46,7 +46,7 @@ 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. -- GitLab From 3825f198e85e45b7981fc9cc1f78e31d390ae959 Mon Sep 17 00:00:00 2001 From: "Mayr, Hannes" Date: Tue, 23 Aug 2022 15:37:15 +0200 Subject: [PATCH 03/18] Fix cleanup of test_save_plot.py. --- tests/test_save_plot.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_save_plot.py b/tests/test_save_plot.py index 0541724..fbb9f22 100644 --- a/tests/test_save_plot.py +++ b/tests/test_save_plot.py @@ -84,6 +84,7 @@ class TestSavePlot(unittest.TestCase): """ with self.assertRaises(TypeError): save_plot([FIGURE, 'figure', FIGURE], 'PLOT_NAME', extension='jpg') + os.remove('PLOT_NAME1.jpg') def tearDown(self): os.remove(IMG1) -- GitLab From 03fb017fb506660f7952373cafa457696e473f34 Mon Sep 17 00:00:00 2001 From: "Hock, Martin" Date: Mon, 29 Aug 2022 13:09:40 +0200 Subject: [PATCH 04/18] Change publication of docs on each merge on master, because we forget manual, and tags have to be pushed (and we apply release tags after commit) --- .gitlab-ci.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 94dffe1..3972355 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -63,7 +63,6 @@ test: pages: stage: docs - when: manual script: - pip install -U sphinx sphinx-autoapi sphinx_rtd_theme # sphinx_panels - cd docs @@ -72,7 +71,9 @@ pages: artifacts: paths: - public - + rules: + - if: $CI_COMMIT_REF_NAME == $CI_DEFAULT_BRANCH + - when: manual # Commenting out all other stages and jobs #run: -- GitLab From 779cd772c021ecc0ee40229a5b9059a00964371d Mon Sep 17 00:00:00 2001 From: "Mayr, Hannes" Date: Tue, 30 Aug 2022 12:24:39 +0200 Subject: [PATCH 05/18] Restructure functions and methods to use **kwargs and improve docstrings. --- plotid/create_id.py | 13 ++++++--- plotid/example.py | 6 ++-- plotid/plotoptions.py | 21 +++++++++----- plotid/publish.py | 47 ++++++++++++-------------------- plotid/save_plot.py | 7 ++--- plotid/tagplot.py | 22 +++++++++------ plotid/tagplot_image.py | 9 +++++- plotid/tagplot_matplotlib.py | 9 +++++- tests/test_publish.py | 40 ++++++++++++++------------- tests/test_tagplot.py | 22 +++++++-------- tests/test_tagplot_image.py | 14 ++++------ tests/test_tagplot_matplotlib.py | 10 +++---- 12 files changed, 118 insertions(+), 102 deletions(-) diff --git a/plotid/create_id.py b/plotid/create_id.py index 450e2da..0388411 100644 --- a/plotid/create_id.py +++ b/plotid/create_id.py @@ -3,7 +3,7 @@ Create an identifier to print it on a plot. Functions: - create_id(str) -> string + create_id(str) -> str """ import time import uuid @@ -13,12 +13,17 @@ def create_id(id_method): """ Create an Identifier (str). - Creates an (sometimes unique) identifier based on the selected method - if no method is selected method 1 will be the default method + Creates an (sometimes unique) identifier based on the selected method. + + Parameters + ---------- + id_method : str + id_method for creating the ID. Create an ID by Unix time is referenced + as 'time', create a random ID with id_method='random'. Returns ------- - figure_id + figure_id : str """ match id_method: case 'time': diff --git a/plotid/example.py b/plotid/example.py index 344f310..1ef907b 100644 --- a/plotid/example.py +++ b/plotid/example.py @@ -14,7 +14,7 @@ from plotid.tagplot import tagplot from plotid.publish import publish # %% Set Project ID -PROJECT_ID = "MR04_" +PROJECT_ID = "MR05_" # %% Create sample data x = np.linspace(0, 10, 100) @@ -44,8 +44,8 @@ FIGS_AS_LIST = [FIG1, FIG2] IMGS_AS_LIST = [IMG1, IMG2] # Example for how to use tagplot with matplotlib figures -[TAGGED_FIGS, ID] = tagplot(FIGS_AS_LIST, 'matplotlib', prefix=PROJECT_ID, - id_method='time', location='west') +[TAGGED_FIGS, ID] = tagplot(FIGS_AS_LIST, 'matplotlib', location='west', + id_method='random', prefix=PROJECT_ID) # Example for how to use tagplot with image files # [TAGGED_FIGS, ID] = tagplot(IMGS_AS_LIST, 'image', prefix=PROJECT_ID, diff --git a/plotid/plotoptions.py b/plotid/plotoptions.py index 163a1c1..5a10b04 100644 --- a/plotid/plotoptions.py +++ b/plotid/plotoptions.py @@ -16,23 +16,30 @@ class PlotOptions: ---------- figs : figure object Figure that will be tagged. - prefix : str - Prefix that is placed before the ID. - id_method : str - Method that decides which method is used to generate the ID. rotation : int Rotation angle for the ID. position : tuple Relative position of the ID on the plot (x,y). + **kwargs : dict, optional + Extra arguments for additional plot options. + + Other Parameters + ---------------- + prefix : str, optional + Will be added as prefix to the ID. + id_method : str, optional + id_method for creating the ID. Create an ID by Unix time is referenced + as 'time', create a random ID with id_method='random'. + The default is 'time'. """ - def __init__(self, figs, prefix, id_method, rotation, position): + def __init__(self, figs, rotation, position, **kwargs): self.figs = figs - self.prefix = prefix - self.id_method = id_method self.rotation = rotation self.position = position + self.prefix = kwargs.get('prefix', '') + self.id_method = kwargs.get('id_method', 'time') def validate_input(self): """ diff --git a/plotid/publish.py b/plotid/publish.py index 2b7f2d1..517d4e8 100644 --- a/plotid/publish.py +++ b/plotid/publish.py @@ -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(str, str, figure, str, str) -> None + publish(str, str, figure, str) -> None """ import os @@ -25,34 +25,19 @@ class PublishOptions: Methods ------- __init__ - validate_input: Check if input is correct type. - - Attributes - ---------- - 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 : str - Name for the exported plot. - data_storage : str - Method how the data should be stored. Available options: - centralized : The data files will copied only once. All other plots - will reference this data via sym link. - individual [default]: The complete data files will be copied to a - separate folder for every plot. + validate_input + Check if input is correct type. + export + Export the plot and copy specified files to the destiantion folder. """ - def __init__(self, src_datapaths, dst_path, figure, plot_names, - data_storage='individual'): + def __init__(self, src_datapaths, dst_path, figure, plot_names, **kwargs): self.src_datapaths = src_datapaths self.dst_path = dst_path self.figure = figure self.plot_names = plot_names - self.data_storage = data_storage + self.data_storage = kwargs.get('data_storage', 'individual') self.dst_path_head, self.dst_dirname = os.path.split(self.dst_path) # If the second string after os.path.split is empty, @@ -202,7 +187,7 @@ class PublishOptions: Parameters ---------- - destination : string + destination : str Directory where the data should be stored. pic_paths : list Paths to the picture file that will be stored in destination. @@ -232,22 +217,26 @@ class PublishOptions: os.remove(path) -def publish(src_datapath, dst_path, figure, plot_name, - data_storage='individual'): +def publish(src_datapath, dst_path, figure, plot_name, **kwargs): """ Save plot, data and measuring script. Parameters ---------- - src_datapath : str + src_datapath : str or list of 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 + plot_name : str or list of str Name for the exported plot. - data_storage : str + **kwargs : dict, optional + Extra arguments for additional publish options. + + Other Parameters + ---------------- + data_storage : str, optional 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. @@ -260,6 +249,6 @@ def publish(src_datapath, dst_path, figure, plot_name, """ publish_container = PublishOptions(src_datapath, dst_path, figure, - plot_name, data_storage) + plot_name, **kwargs) publish_container.validate_input() publish_container.export() diff --git a/plotid/save_plot.py b/plotid/save_plot.py index 8642c4a..2c04585 100644 --- a/plotid/save_plot.py +++ b/plotid/save_plot.py @@ -4,7 +4,7 @@ Export a plot figure to a picture file. Functions: - save_plot(figure, string) -> path-like + save_plot(figure, str) -> path-like """ import warnings @@ -20,16 +20,15 @@ def save_plot(figures, plot_names, extension='png'): ---------- figure : list of/single figure object Figure that was tagged and now should be saved as picture. - plot_name : list of strings + plot_name : str or list of str Names of the files where the plots will be saved to. extension : str File extension for the plot export. Returns ------- - plot_path : list + plot_path : str or list of str Names of the created pictures. - """ # Check if figs is a valid figure or a list of valid figures if isinstance(figures, matplotlib.figure.Figure): diff --git a/plotid/tagplot.py b/plotid/tagplot.py index 596679a..bb16c1a 100644 --- a/plotid/tagplot.py +++ b/plotid/tagplot.py @@ -16,27 +16,32 @@ from plotid.tagplot_matplotlib import tagplot_matplotlib from plotid.tagplot_image import tagplot_image -def tagplot(figs, engine, prefix='', id_method='time', location='east'): +def tagplot(figs, engine, location='east', **kwargs): """ Tag your figure/plot with an ID. - After determining the plot engine, TagPlot calls the corresponding + After determining the plot engine, tagplot calls the corresponding function which tags the plot. Parameters ---------- figs : list Figures that should be tagged. - engine : string + engine : str Plot engine which should be used to tag the plot. - prefix : string + location : str, optional + Location for ID to be displayed on the plot. Default is 'east'. + **kwargs : dict, optional + Extra arguments for additional plot options. + + Other Parameters + ---------------- + prefix : str, optional Will be added as prefix to the ID. - id_method : string, optional + id_method : str, optional id_method for creating the ID. Create an ID by Unix time is referenced as 'time', create a random ID with id_method='random'. The default is 'time'. - location : string, optional - Location for ID to be displayed on the plot. Default is 'east'. Raises ------ @@ -84,8 +89,7 @@ def tagplot(figs, engine, prefix='', id_method='time', location='east'): rotation = 90 position = (0.975, 0.35) - option_container = PlotOptions(figs, prefix, id_method, - rotation, position) + option_container = PlotOptions(figs, rotation, position, **kwargs) option_container.validate_input() match engine: diff --git a/plotid/tagplot_image.py b/plotid/tagplot_image.py index 104352c..548c04a 100644 --- a/plotid/tagplot_image.py +++ b/plotid/tagplot_image.py @@ -17,7 +17,14 @@ def tagplot_image(plotid_object): The ID is placed visual on the figure window and returned as string in a list together with the figures. - TagPlot can tag multiple figures at once. + + Parameters + ---------- + plotid_object : instance of PlotOptions + + Returns + ------- + list with figures and IDs """ # Check if plotid_object is a valid instance of PlotOptions if not isinstance(plotid_object, PlotOptions): diff --git a/plotid/tagplot_matplotlib.py b/plotid/tagplot_matplotlib.py index 34f06cf..8bfd791 100644 --- a/plotid/tagplot_matplotlib.py +++ b/plotid/tagplot_matplotlib.py @@ -18,7 +18,14 @@ def tagplot_matplotlib(plotid_object): The ID is placed visual on the figure window and returned as string in a list together with the figures. - TagPlot can tag multiple figures at once. + + Parameters + ---------- + plotid_object : instance of PlotOptions + + Returns + ------- + list with figures and IDs """ # Check if plotid_object is a valid instance of PlotOptions if not isinstance(plotid_object, PlotOptions): diff --git a/tests/test_publish.py b/tests/test_publish.py index 4956734..aa6fd92 100644 --- a/tests/test_publish.py +++ b/tests/test_publish.py @@ -46,7 +46,8 @@ 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, + data_storage='individual') assert os.path.isfile(os.path.join(DST_PATH, PIC_NAME + '.png')) # Skip test if tests are run from command line. @@ -58,7 +59,7 @@ class TestPublish(unittest.TestCase): 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') + publish(SRC_DIR, DST_PATH, FIGS_AS_LIST, PIC_NAME_LIST) for name in PIC_NAME_LIST: assert os.path.isfile(os.path.join(DST_PATH, name + '.png')) @@ -70,7 +71,7 @@ class TestPublish(unittest.TestCase): 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') + data_storage='individual') assert os.path.isdir(DST_PATH) for file in SRC_FILES: assert os.path.isfile(os.path.join(DST_PATH, file)) @@ -78,21 +79,19 @@ class TestPublish(unittest.TestCase): 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, - PIC_NAME, 'individual') + publish('not_existing_folder', DST_PATH, FIG, PIC_NAME) 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') + publish([SRC_DIR, 4], DST_PATH, FIG, PIC_NAME) with self.assertRaises(TypeError): - publish(4, DST_PATH, FIG, PIC_NAME, 'individual') + publish(4, DST_PATH, FIG, PIC_NAME) 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') + publish(SRC_DIR, 'not_existing_folder', FIG, PIC_NAME) def test_script_exists(self): """ @@ -139,7 +138,8 @@ 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, + data_storage='individual') # Skip test if tests are run from command line. @unittest.skipIf(not os.path.isfile(sys.argv[0]), 'Publish is not called ' @@ -154,7 +154,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) # Skip test if tests are run from command line. @unittest.skipIf(not os.path.isfile(sys.argv[0]), 'Publish is not called ' @@ -169,7 +169,8 @@ 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, + data_storage='individual') # Skip test if tests are run from command line. @unittest.skipIf(not os.path.isfile(sys.argv[0]), 'Publish is not called ' @@ -185,28 +186,29 @@ class TestPublish(unittest.TestCase): # Mock user input as 'yes' with patch('builtins.input', return_value='yes'): with self.assertRaises(RuntimeError): - publish(SRC_DIR, DST_PATH, FIG, PIC_NAME, 'individual') + publish(SRC_DIR, DST_PATH, FIG, PIC_NAME) assert not os.path.isdir(INVISIBLE_PATH) 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, data_storage='individual') with self.assertRaises(TypeError): - publish(SRC_DIR, DST_PATH, FIG, (), 'individual') + publish(SRC_DIR, DST_PATH, FIG, (), data_storage='individual') with self.assertRaises(TypeError): - publish(SRC_DIR, DST_PATH, FIG, ['test', 3], 'individual') + publish(SRC_DIR, DST_PATH, FIG, ['test', 3]) 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, + data_storage='none_existing_method') with self.assertRaises(TypeError): - publish(SRC_DIR, DST_PATH, FIG, PIC_NAME, 3) + publish(SRC_DIR, DST_PATH, FIG, PIC_NAME, data_storage=3) with self.assertRaises(TypeError): - publish(SRC_DIR, DST_PATH, FIG, PIC_NAME, []) + publish(SRC_DIR, DST_PATH, FIG, PIC_NAME, data_storage=[]) def tearDown(self): """ Delete all files created in setUp. """ diff --git a/tests/test_tagplot.py b/tests/test_tagplot.py index ddbdd16..8651c5c 100644 --- a/tests/test_tagplot.py +++ b/tests/test_tagplot.py @@ -35,44 +35,44 @@ class TestTagplot(unittest.TestCase): """ Test if tagplot runs successful. """ - tagplot(FIGS_AS_LIST, PLOT_ENGINE, PROJECT_ID, METHOD) - tagplot(IMGS_AS_LIST, 'image', PROJECT_ID, METHOD, location='north') + tagplot(FIGS_AS_LIST, PLOT_ENGINE, prefix=PROJECT_ID, id_method=METHOD) + tagplot(IMGS_AS_LIST, 'image', location='north') def test_prefix(self): """ Test if Error is raised if prefix is not a string. """ with self.assertRaises(TypeError): - tagplot(FIGS_AS_LIST, PLOT_ENGINE, 3, METHOD, location='southeast') + tagplot(FIGS_AS_LIST, PLOT_ENGINE, location='southeast', + prefix=3, id_method=METHOD) def test_plotengine(self): """ Test if Errors are raised if the provided plot engine is not supported. """ with self.assertRaises(ValueError): - tagplot(FIGS_AS_LIST, 1, PROJECT_ID, METHOD, location='north') + tagplot(FIGS_AS_LIST, 1, location='north') with self.assertRaises(ValueError): - tagplot(FIGS_AS_LIST, 'xyz', PROJECT_ID, METHOD, location='south') + tagplot(FIGS_AS_LIST, 'xyz', location='south') def test_idmethod(self): """ Test if Errors are raised if the id_method is not an string. """ with self.assertRaises(TypeError): - tagplot(FIGS_AS_LIST, PLOT_ENGINE, PROJECT_ID, id_method=(0, 1), + tagplot(FIGS_AS_LIST, PLOT_ENGINE, id_method=(0, 1), location='west') with self.assertRaises(TypeError): - tagplot(FIGS_AS_LIST, PLOT_ENGINE, PROJECT_ID, id_method=1) + tagplot(FIGS_AS_LIST, PLOT_ENGINE, id_method=1) with self.assertRaises(TypeError): - tagplot(FIGS_AS_LIST, PLOT_ENGINE, PROJECT_ID, id_method=[0, 1]) + tagplot(FIGS_AS_LIST, PLOT_ENGINE, id_method=[0, 1]) def test_location(self): """ Test if Errors are raised if the provided location is not supported. """ with self.assertRaises(TypeError): - tagplot(FIGS_AS_LIST, PLOT_ENGINE, PROJECT_ID, METHOD, location=1) + tagplot(FIGS_AS_LIST, PLOT_ENGINE, location=1) with self.assertWarns(Warning): - tagplot(FIGS_AS_LIST, PLOT_ENGINE, PROJECT_ID, METHOD, - location='up') + tagplot(FIGS_AS_LIST, PLOT_ENGINE, location='up') def tearDown(self): os.remove(IMG1) diff --git a/tests/test_tagplot_image.py b/tests/test_tagplot_image.py index f899abc..76edc2a 100644 --- a/tests/test_tagplot_image.py +++ b/tests/test_tagplot_image.py @@ -39,8 +39,8 @@ class TestTagplotImage(unittest.TestCase): Test of returned objects. Check if they are png and jpg files, respectively. """ - options = PlotOptions(IMGS_AS_LIST, PROJECT_ID, METHOD, - ROTATION, POSITION) + options = PlotOptions(IMGS_AS_LIST, ROTATION, POSITION, + prefix=PROJECT_ID, id_method=METHOD) options.validate_input() [figs, _] = tagplot_image(options) self.assertIsInstance(figs[0], PngImagePlugin.PngImageFile) @@ -51,24 +51,22 @@ class TestTagplotImage(unittest.TestCase): Test of returned objects. Check if png files are returned, if a single matplot figure is given (not as a list). """ - options = PlotOptions(IMG1, PROJECT_ID, METHOD, ROTATION, - POSITION) + options = PlotOptions(IMG1, ROTATION, POSITION) options.validate_input() [figs, _] = tagplot_image(options) self.assertIsInstance(figs[0], PngImagePlugin.PngImageFile) def test_image_not_str(self): """ Test if Error is raised if wrong type of image is given. """ - options = PlotOptions(3, PROJECT_ID, METHOD, ROTATION, - POSITION) + options = PlotOptions(3, ROTATION, POSITION, + prefix=PROJECT_ID, id_method=METHOD) options.validate_input() with self.assertRaises(TypeError): tagplot_image(options) def test_image_not_file(self): """ Test if Error is raised if the image file does not exist. """ - options = PlotOptions('xyz', PROJECT_ID, METHOD, ROTATION, - POSITION) + options = PlotOptions('xyz', ROTATION, POSITION) options.validate_input() with self.assertRaises(TypeError): tagplot_image(options) diff --git a/tests/test_tagplot_matplotlib.py b/tests/test_tagplot_matplotlib.py index 604bc79..d7341b8 100644 --- a/tests/test_tagplot_matplotlib.py +++ b/tests/test_tagplot_matplotlib.py @@ -29,8 +29,8 @@ class TestTagplotMatplotlib(unittest.TestCase): def test_mplfigures(self): """ Test of returned objects. Check if they are matplotlib figures. """ - options = PlotOptions(FIGS_AS_LIST, PROJECT_ID, METHOD, - ROTATION, POSITION) + options = PlotOptions(FIGS_AS_LIST, ROTATION, POSITION, + prefix=PROJECT_ID, id_method=METHOD) options.validate_input() [figs, _] = tagplot_matplotlib(options) self.assertIsInstance(figs[0], Figure) @@ -41,16 +41,14 @@ class TestTagplotMatplotlib(unittest.TestCase): Test of returned objects. Check if matplotlib figures are returned, if a single matplot figure is given (not as a list). """ - options = PlotOptions(FIG1, PROJECT_ID, METHOD, ROTATION, - POSITION) + options = PlotOptions(FIG1, ROTATION, POSITION) options.validate_input() [figs, _] = tagplot_matplotlib(options) self.assertIsInstance(figs[0], Figure) def test_mplerror(self): """ Test if Error is raised if wrong type of figures is given. """ - options = PlotOptions(3, PROJECT_ID, METHOD, ROTATION, - POSITION) + options = PlotOptions(3, ROTATION, POSITION) options.validate_input() with self.assertRaises(TypeError): tagplot_matplotlib(options) -- GitLab From 654327a96413630bfd18710c22d93b5e2fce965d Mon Sep 17 00:00:00 2001 From: "Mayr, Hannes" Date: Fri, 9 Sep 2022 17:16:08 +0200 Subject: [PATCH 06/18] Make the figure_ids an attribute of PlotOptions. --- plotid/create_id.py | 2 +- plotid/example.py | 8 ++++---- plotid/plotoptions.py | 13 ++++++++++--- plotid/tagplot_image.py | 5 ++--- plotid/tagplot_matplotlib.py | 11 +++++------ 5 files changed, 22 insertions(+), 17 deletions(-) diff --git a/plotid/create_id.py b/plotid/create_id.py index 0388411..87d0085 100644 --- a/plotid/create_id.py +++ b/plotid/create_id.py @@ -28,7 +28,7 @@ def create_id(id_method): match id_method: case 'time': figure_id = time.time() # UNIX Time - figure_id = hex(int(figure_id)) # convert time to hexadecimal + figure_id = hex(int(figure_id)) # convert time to hexadecimal str time.sleep(0.5) # break for avoiding duplicate IDs case 'random': figure_id = str(uuid.uuid4()) # creates a random UUID diff --git a/plotid/example.py b/plotid/example.py index 1ef907b..4c946c8 100644 --- a/plotid/example.py +++ b/plotid/example.py @@ -44,12 +44,12 @@ FIGS_AS_LIST = [FIG1, FIG2] IMGS_AS_LIST = [IMG1, IMG2] # Example for how to use tagplot with matplotlib figures -[TAGGED_FIGS, ID] = tagplot(FIGS_AS_LIST, 'matplotlib', location='west', - id_method='random', prefix=PROJECT_ID) +# [TAGGED_FIGS, ID] = tagplot(FIGS_AS_LIST, 'matplotlib', location='west', +# id_method='random', prefix=PROJECT_ID) # Example for how to use tagplot with image files -# [TAGGED_FIGS, ID] = tagplot(IMGS_AS_LIST, 'image', prefix=PROJECT_ID, -# id_method='random', location='west') +[TAGGED_FIGS, ID] = tagplot(IMGS_AS_LIST, 'image', prefix=PROJECT_ID, + id_method='time', location='west') # %% Publish # Arguments: Source directory or files as list, destination directory, figures, diff --git a/plotid/plotoptions.py b/plotid/plotoptions.py index 5a10b04..710d729 100644 --- a/plotid/plotoptions.py +++ b/plotid/plotoptions.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -"""Contains the PlotOptions and PublishOptions class.""" +"""Contains the PlotOptions class.""" class PlotOptions: @@ -14,8 +14,10 @@ class PlotOptions: Attributes ---------- - figs : figure object - Figure that will be tagged. + figs : figure object or list of figures + Figures that will be tagged. + figure_ids: str or list of str + IDs that the figures are tagged with. rotation : int Rotation angle for the ID. position : tuple @@ -36,11 +38,16 @@ class PlotOptions: def __init__(self, figs, rotation, position, **kwargs): self.figs = figs + self.figure_ids = kwargs.get('figure_ids', []) self.rotation = rotation self.position = position self.prefix = kwargs.get('prefix', '') self.id_method = kwargs.get('id_method', 'time') + def __str__(self): + """Representation if an object of this class is printed.""" + return str(self.__class__) + ": " + str(self.__dict__) + def validate_input(self): """ Validate if input for PlotOptions is correct type. diff --git a/plotid/tagplot_image.py b/plotid/tagplot_image.py index 548c04a..01eb345 100644 --- a/plotid/tagplot_image.py +++ b/plotid/tagplot_image.py @@ -39,13 +39,12 @@ def tagplot_image(plotid_object): raise TypeError('File does not exist.') # Check if figs is a valid file is done by pillow internally - ids_as_list = [] color = (128, 128, 128) # grey font = ImageFont.load_default() for i, img in enumerate(plotid_object.figs): img_id = plotid_object.prefix + create_id(plotid_object.id_method) - ids_as_list.append(img_id) + plotid_object.figure_ids.append(img_id) img = Image.open(img) img_txt = Image.new('L', font.getsize(img_id)) @@ -57,4 +56,4 @@ def tagplot_image(plotid_object): int(img.height*(1-plotid_object.position[1]))), txt) plotid_object.figs[i] = img - return [plotid_object.figs, ids_as_list] + return [plotid_object.figs, plotid_object.figure_ids] diff --git a/plotid/tagplot_matplotlib.py b/plotid/tagplot_matplotlib.py index 8bfd791..abd0ada 100644 --- a/plotid/tagplot_matplotlib.py +++ b/plotid/tagplot_matplotlib.py @@ -39,18 +39,17 @@ def tagplot_matplotlib(plotid_object): fontsize = 'small' color = 'grey' - ids_as_list = [] # Loop to create and position the IDs for fig in plotid_object.figs: - figure_id = create_id(plotid_object.id_method) - figure_id = plotid_object.prefix + str(figure_id) - ids_as_list.append(figure_id) + fig_id = create_id(plotid_object.id_method) + fig_id = plotid_object.prefix + fig_id + plotid_object.figure_ids.append(fig_id) plt.figure(fig) plt.figtext(x=plotid_object.position[0], y=plotid_object.position[1], - s=figure_id, ha='left', wrap=True, + s=fig_id, ha='left', wrap=True, rotation=plotid_object.rotation, fontsize=fontsize, color=color) fig.tight_layout() - return [plotid_object.figs, ids_as_list] + return [plotid_object.figs, plotid_object.figure_ids] -- GitLab From 6708a678131b642cff8ef2045d91294f1e647e7b Mon Sep 17 00:00:00 2001 From: "Mayr, Hannes" Date: Tue, 13 Sep 2022 11:54:19 +0200 Subject: [PATCH 07/18] Restructure and introduce additional object to transfer figures and IDs from tagplot() to publish(). --- plotid/create_id.py | 2 +- plotid/example.py | 11 +++-- plotid/plotoptions.py | 28 ++++++++++- plotid/publish.py | 28 +++++++++-- plotid/tagplot_image.py | 8 +-- plotid/tagplot_matplotlib.py | 6 ++- tests/test_plotoptions.py | 61 +++++++++++++++++++++++ tests/test_publish.py | 84 ++++++++++++++++++++++---------- tests/test_tagplot.py | 2 +- tests/test_tagplot_image.py | 13 +++-- tests/test_tagplot_matplotlib.py | 10 ++-- 11 files changed, 200 insertions(+), 53 deletions(-) create mode 100644 tests/test_plotoptions.py diff --git a/plotid/create_id.py b/plotid/create_id.py index 87d0085..bfab1a5 100644 --- a/plotid/create_id.py +++ b/plotid/create_id.py @@ -29,7 +29,7 @@ def create_id(id_method): case 'time': figure_id = time.time() # UNIX Time figure_id = hex(int(figure_id)) # convert time to hexadecimal str - time.sleep(0.5) # break for avoiding duplicate IDs + time.sleep(1) # break for avoiding duplicate IDs case 'random': figure_id = str(uuid.uuid4()) # creates a random UUID figure_id = figure_id[0:8] # only use first 8 numbers diff --git a/plotid/example.py b/plotid/example.py index 4c946c8..23b01e1 100644 --- a/plotid/example.py +++ b/plotid/example.py @@ -48,13 +48,14 @@ IMGS_AS_LIST = [IMG1, IMG2] # id_method='random', prefix=PROJECT_ID) # Example for how to use tagplot with image files -[TAGGED_FIGS, ID] = tagplot(IMGS_AS_LIST, 'image', prefix=PROJECT_ID, - id_method='time', location='west') +FIGS_AND_IDS = tagplot(IMGS_AS_LIST, 'image', prefix=PROJECT_ID, + id_method='time', location='west') + +print(FIGS_AND_IDS.figure_ids) # %% Publish # Arguments: Source directory or files as list, destination directory, figures, # plots or images. -publish(['../README.md', '../docs', '../LICENSE'], - '/home/chief/Dokumente/fst/plotid_python/data/', - TAGGED_FIGS, 'Bild') +publish(FIGS_AND_IDS, ['../README.md', '../docs', '../LICENSE'], + '/home/chief/Dokumente/fst/plotid_python/data/', 'Bild') diff --git a/plotid/plotoptions.py b/plotid/plotoptions.py index 710d729..dee0f71 100644 --- a/plotid/plotoptions.py +++ b/plotid/plotoptions.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -"""Contains the PlotOptions class.""" +"""Contains the PlotOptions and PlotIDTransfer classes.""" class PlotOptions: @@ -75,3 +75,29 @@ class PlotOptions: self.figs = [self.figs] return 0 + + +class PlotIDTransfer: + """ + Container to transfer objects from tagplot() to publish(). + + Methods + ------- + __init__ + validate_input : Check if input is correct type. + + Attributes + ---------- + figs : figure object or list of figures + Tagged figures. + figure_ids: str or list of str + IDs that the figures are tagged with. + """ + + def __init__(self, figs, figure_ids): + self.figs = figs + self.figure_ids = figure_ids + + def __str__(self): + """Representation if an object of this class is printed.""" + return str(self.__class__) + ": " + str(self.__dict__) diff --git a/plotid/publish.py b/plotid/publish.py index 517d4e8..8866573 100644 --- a/plotid/publish.py +++ b/plotid/publish.py @@ -31,11 +31,13 @@ class PublishOptions: Export the plot and copy specified files to the destiantion folder. """ - def __init__(self, src_datapaths, dst_path, figure, plot_names, **kwargs): + def __init__(self, figs_and_ids, src_datapaths, dst_path, plot_names, + **kwargs): + self.figure = figs_and_ids.figs + self.figure_ids = figs_and_ids.figure_ids self.src_datapaths = src_datapaths self.dst_path = dst_path - self.figure = figure self.plot_names = plot_names self.data_storage = kwargs.get('data_storage', 'individual') self.dst_path_head, self.dst_dirname = os.path.split(self.dst_path) @@ -47,6 +49,10 @@ class PublishOptions: self.dst_path_head, self.dst_dirname = os.path.split( self.dst_path_head) + def __str__(self): + """Representation if an object of this class is printed.""" + return str(self.__class__) + ": " + str(self.__dict__) + def validate_input(self): """ Validate if input for PublishOptions is correct type. @@ -64,6 +70,18 @@ class PublishOptions: None. """ + # Check if IDs are str + if isinstance(self.figure_ids, str): + self.figure_ids = [self.figure_ids] + if isinstance(self.figure_ids, list): + for identifier in self.figure_ids: + if not isinstance(identifier, str): + raise TypeError('The list of figure_ids contains an object' + ' which is not a string.') + else: + raise TypeError('The specified figure_ids are neither a string nor' + ' a list of strings.') + if not os.path.isfile(sys.argv[0]): raise FileNotFoundError('Cannot copy original python script. ' 'Running publish from a shell is not ' @@ -107,7 +125,7 @@ class PublishOptions: def export(self): """ - Export the plot and copy specified files to the destiantion folder. + Export the plot and copy specified files to the destination folder. Raises ------ @@ -217,7 +235,7 @@ class PublishOptions: os.remove(path) -def publish(src_datapath, dst_path, figure, plot_name, **kwargs): +def publish(figs_and_ids, src_datapath, dst_path, plot_name, **kwargs): """ Save plot, data and measuring script. @@ -248,7 +266,7 @@ def publish(src_datapath, dst_path, figure, plot_name, **kwargs): None. """ - publish_container = PublishOptions(src_datapath, dst_path, figure, + publish_container = PublishOptions(figs_and_ids, src_datapath, dst_path, plot_name, **kwargs) publish_container.validate_input() publish_container.export() diff --git a/plotid/tagplot_image.py b/plotid/tagplot_image.py index 01eb345..decc9e5 100644 --- a/plotid/tagplot_image.py +++ b/plotid/tagplot_image.py @@ -8,7 +8,7 @@ Functions: import os from PIL import Image, ImageDraw, ImageFont, ImageOps from plotid.create_id import create_id -from plotid.plotoptions import PlotOptions +from plotid.plotoptions import PlotOptions, PlotIDTransfer def tagplot_image(plotid_object): @@ -24,7 +24,7 @@ def tagplot_image(plotid_object): Returns ------- - list with figures and IDs + PlotIDTransfer object """ # Check if plotid_object is a valid instance of PlotOptions if not isinstance(plotid_object, PlotOptions): @@ -56,4 +56,6 @@ def tagplot_image(plotid_object): int(img.height*(1-plotid_object.position[1]))), txt) plotid_object.figs[i] = img - return [plotid_object.figs, plotid_object.figure_ids] + + figs_and_ids = PlotIDTransfer(plotid_object.figs, plotid_object.figure_ids) + return figs_and_ids diff --git a/plotid/tagplot_matplotlib.py b/plotid/tagplot_matplotlib.py index abd0ada..6bc0038 100644 --- a/plotid/tagplot_matplotlib.py +++ b/plotid/tagplot_matplotlib.py @@ -9,7 +9,7 @@ Functions: import matplotlib import matplotlib.pyplot as plt from plotid.create_id import create_id -from plotid.plotoptions import PlotOptions +from plotid.plotoptions import PlotOptions, PlotIDTransfer def tagplot_matplotlib(plotid_object): @@ -52,4 +52,6 @@ def tagplot_matplotlib(plotid_object): rotation=plotid_object.rotation, fontsize=fontsize, color=color) fig.tight_layout() - return [plotid_object.figs, plotid_object.figure_ids] + + figs_and_ids = PlotIDTransfer(plotid_object.figs, plotid_object.figure_ids) + return figs_and_ids diff --git a/tests/test_plotoptions.py b/tests/test_plotoptions.py new file mode 100644 index 0000000..ee752c2 --- /dev/null +++ b/tests/test_plotoptions.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Unittests for plotoptions. +""" + +import unittest +from plotid.plotoptions import PlotOptions, PlotIDTransfer + +ROTATION = 270 +POSITION = (100, 200) + + +class TestTagplot(unittest.TestCase): + """ + Class for all unittests of the plotoptions module. + """ + + def test_validate_input(self): + """ + Test if input validation runs successful. + """ + PlotOptions('FIG', ROTATION, POSITION, prefix='xyz', + id_method='random').validate_input() + + def test_prefix(self): + """ Test if Error is raised if prefix is not a string. """ + with self.assertRaises(TypeError): + PlotOptions(['FIG'], ROTATION, POSITION, prefix=3).validate_input() + + def test_data_storage(self): + """ Test if Error is raised if id_method is not a string. """ + with self.assertRaises(TypeError): + PlotOptions(['FIG'], ROTATION, POSITION, + id_method=4).validate_input() + + def test_str_plotoptions(self): + """ + Test if the string representation of a PlotOptions object is correct. + """ + plot_obj = PlotOptions('FIG', ROTATION, POSITION, prefix='xyz', + id_method='random') + self.assertEqual(str(plot_obj), + ": {'figs': " + "'FIG', 'figure_ids': [], 'rotation': 270, 'position'" + ": (100, 200), 'prefix': 'xyz', 'id_method': " + "'random'}") + + def test_str_plotidtransfer(self): + """ + Test if the string representation of a PlotIDTransfer object is + correct. + """ + transfer_obj = PlotIDTransfer('FIG', []) + self.assertEqual(str(transfer_obj), + ": " + "{'figs': 'FIG', 'figure_ids': []}") + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_publish.py b/tests/test_publish.py index aa6fd92..8d1ce28 100644 --- a/tests/test_publish.py +++ b/tests/test_publish.py @@ -12,7 +12,8 @@ import shutil from subprocess import run, CalledProcessError from unittest.mock import patch import matplotlib.pyplot as plt -from plotid.publish import publish +from plotid.publish import publish, PublishOptions +from plotid.plotoptions import PlotIDTransfer SRC_DIR = 'test_src_folder' @@ -22,9 +23,11 @@ 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() -FIG2 = plt.figure() +FIG = plt.figure(figsize=[6.4, 4.8], dpi=100) +FIG2 = plt.figure(figsize=[6.4, 4.8], dpi=100) FIGS_AS_LIST = [FIG, FIG2] +IDS_AS_LIST = ['MR05_0x63203c6f', 'MR05_0x63203c70'] +FIGS_AND_IDS = PlotIDTransfer(FIGS_AS_LIST, IDS_AS_LIST) PIC_NAME_LIST = [PIC_NAME, 'second_picture'] @@ -46,8 +49,8 @@ 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, - data_storage='individual') + publish(PlotIDTransfer(FIG, 'testID'), SRC_DIR, DST_PATH + '/', + PIC_NAME, data_storage='individual') assert os.path.isfile(os.path.join(DST_PATH, PIC_NAME + '.png')) # Skip test if tests are run from command line. @@ -59,10 +62,17 @@ class TestPublish(unittest.TestCase): Test publish with multiple figures and check if all exported picture files exist. """ - publish(SRC_DIR, DST_PATH, FIGS_AS_LIST, PIC_NAME_LIST) + publish(FIGS_AND_IDS, SRC_DIR, DST_PATH, PIC_NAME_LIST) for name in PIC_NAME_LIST: assert os.path.isfile(os.path.join(DST_PATH, name + '.png')) + def test_wrong_ids(self): + """ Test if Error is raised if IDs are of wrong type. """ + with self.assertRaises(TypeError): + publish(PlotIDTransfer(FIG, 3), SRC_DIR, DST_PATH, PIC_NAME) + with self.assertRaises(TypeError): + publish(PlotIDTransfer(FIG, ['i', 4]), SRC_DIR, DST_PATH, PIC_NAME) + def test_publish_multiple_src_files(self): """ Test publish with multiple source files and check @@ -70,7 +80,7 @@ class TestPublish(unittest.TestCase): """ files_and_dir = list(SRC_FILES) files_and_dir.append(SRC_DIR) - publish(files_and_dir, DST_PATH, FIGS_AS_LIST, PIC_NAME_LIST, + publish(FIGS_AND_IDS, files_and_dir, DST_PATH, PIC_NAME_LIST, data_storage='individual') assert os.path.isdir(DST_PATH) for file in SRC_FILES: @@ -79,19 +89,19 @@ class TestPublish(unittest.TestCase): 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, PIC_NAME) + publish(FIGS_AND_IDS, 'not_existing_folder', DST_PATH, PIC_NAME) 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) + publish(FIGS_AND_IDS, [SRC_DIR, 4], DST_PATH, PIC_NAME) with self.assertRaises(TypeError): - publish(4, DST_PATH, FIG, PIC_NAME) + publish(FIGS_AND_IDS, 4, DST_PATH, PIC_NAME) 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) + publish(FIGS_AND_IDS, SRC_DIR, 'not_existing_folder', PIC_NAME) def test_script_exists(self): """ @@ -112,14 +122,17 @@ class TestPublish(unittest.TestCase): run([python, "-c", "import matplotlib.pyplot as plt\n" "from plotid.publish import publish\n" - "publish('test_src_folder', 'test_parent/test_dst_folder'," - " plt.figure(), 'test_picture')"], + "from plotid.plotoptions import PlotIDTransfer\n" + "publish(PlotIDTransfer(plt.figure(), 'testID2')," + " 'test_src_folder', 'test_parent/test_dst_folder'," + " 'test_picture')"], capture_output=True, check=True) process = run([python, "-c", "import matplotlib.pyplot as plt\n" "from plotid.publish import publish\n" - "publish('test_src_folder', " - "'test_parent/test_dst_folder', plt.figure(), " + "from plotid.plotoptions import PlotIDTransfer\n" + "publish(PlotIDTransfer(plt.figure(), 'testID2'), " + "'test_src_folder', 'test_parent/test_dst_folder', " "'test_picture')"], capture_output=True) assert ("FileNotFoundError: Cannot copy original python script. " @@ -138,7 +151,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, + publish(FIGS_AND_IDS, SRC_DIR, DST_PATH, PIC_NAME, data_storage='individual') # Skip test if tests are run from command line. @@ -154,7 +167,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) + publish(FIGS_AND_IDS, SRC_DIR, DST_PATH, PIC_NAME) # Skip test if tests are run from command line. @unittest.skipIf(not os.path.isfile(sys.argv[0]), 'Publish is not called ' @@ -169,7 +182,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, + publish(FIGS_AND_IDS, SRC_DIR, DST_PATH, PIC_NAME, data_storage='individual') # Skip test if tests are run from command line. @@ -186,29 +199,50 @@ class TestPublish(unittest.TestCase): # Mock user input as 'yes' with patch('builtins.input', return_value='yes'): with self.assertRaises(RuntimeError): - publish(SRC_DIR, DST_PATH, FIG, PIC_NAME) + publish(FIGS_AND_IDS, SRC_DIR, DST_PATH, PIC_NAME) assert not os.path.isdir(INVISIBLE_PATH) + os.remove('test_picture1.png') + os.remove('test_picture2.png') 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, data_storage='individual') + publish(FIGS_AND_IDS, SRC_DIR, DST_PATH, 7.6, + data_storage='individual') with self.assertRaises(TypeError): - publish(SRC_DIR, DST_PATH, FIG, (), data_storage='individual') + publish(FIGS_AND_IDS, SRC_DIR, DST_PATH, ()) with self.assertRaises(TypeError): - publish(SRC_DIR, DST_PATH, FIG, ['test', 3]) + publish(FIGS_AND_IDS, SRC_DIR, DST_PATH, ['test', 3]) 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, + publish(FIGS_AND_IDS, SRC_DIR, DST_PATH, PIC_NAME, data_storage='none_existing_method') with self.assertRaises(TypeError): - publish(SRC_DIR, DST_PATH, FIG, PIC_NAME, data_storage=3) + publish(FIGS_AND_IDS, SRC_DIR, DST_PATH, PIC_NAME, data_storage=3) with self.assertRaises(TypeError): - publish(SRC_DIR, DST_PATH, FIG, PIC_NAME, data_storage=[]) + publish(FIGS_AND_IDS, SRC_DIR, DST_PATH, PIC_NAME, data_storage=[]) + + def test_str(self): + """ + Test if the string representation of a PublishOptions object is + correct. + """ + self.maxDiff = None + publish_obj = PublishOptions(FIGS_AND_IDS, SRC_DIR, DST_PATH, PIC_NAME) + self.assertEqual(str(publish_obj), + ": {'figure': " + "[
,
], 'figure_ids': " + "['MR05_0x63203c6f', 'MR05_0x63203c70'], " + "'src_datapaths': 'test_src_folder', 'dst_path': " + "'test_parent/test_dst_folder', 'plot_names': " + "'test_picture', 'data_storage': 'individual', " + "'dst_path_head': 'test_parent', 'dst_dirname': " + "'test_dst_folder'}") def tearDown(self): """ Delete all files created in setUp. """ diff --git a/tests/test_tagplot.py b/tests/test_tagplot.py index 8651c5c..0eb60ac 100644 --- a/tests/test_tagplot.py +++ b/tests/test_tagplot.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- """ -Unittests for tagplot +Unittests for tagplot. """ import os diff --git a/tests/test_tagplot_image.py b/tests/test_tagplot_image.py index 76edc2a..ad42d82 100644 --- a/tests/test_tagplot_image.py +++ b/tests/test_tagplot_image.py @@ -42,9 +42,11 @@ class TestTagplotImage(unittest.TestCase): options = PlotOptions(IMGS_AS_LIST, ROTATION, POSITION, prefix=PROJECT_ID, id_method=METHOD) options.validate_input() - [figs, _] = tagplot_image(options) - self.assertIsInstance(figs[0], PngImagePlugin.PngImageFile) - self.assertIsInstance(figs[1], JpegImagePlugin.JpegImageFile) + figs_and_ids = tagplot_image(options) + self.assertIsInstance(figs_and_ids.figs[0], + PngImagePlugin.PngImageFile) + self.assertIsInstance(figs_and_ids.figs[1], + JpegImagePlugin.JpegImageFile) def test_single_image(self): """ @@ -53,8 +55,9 @@ class TestTagplotImage(unittest.TestCase): """ options = PlotOptions(IMG1, ROTATION, POSITION) options.validate_input() - [figs, _] = tagplot_image(options) - self.assertIsInstance(figs[0], PngImagePlugin.PngImageFile) + figs_and_ids = tagplot_image(options) + self.assertIsInstance(figs_and_ids.figs[0], + PngImagePlugin.PngImageFile) def test_image_not_str(self): """ Test if Error is raised if wrong type of image is given. """ diff --git a/tests/test_tagplot_matplotlib.py b/tests/test_tagplot_matplotlib.py index d7341b8..f6b9b4c 100644 --- a/tests/test_tagplot_matplotlib.py +++ b/tests/test_tagplot_matplotlib.py @@ -32,9 +32,9 @@ class TestTagplotMatplotlib(unittest.TestCase): options = PlotOptions(FIGS_AS_LIST, ROTATION, POSITION, prefix=PROJECT_ID, id_method=METHOD) options.validate_input() - [figs, _] = tagplot_matplotlib(options) - self.assertIsInstance(figs[0], Figure) - self.assertIsInstance(figs[1], Figure) + figs_and_ids = tagplot_matplotlib(options) + self.assertIsInstance(figs_and_ids.figs[0], Figure) + self.assertIsInstance(figs_and_ids.figs[1], Figure) def test_single_mplfigure(self): """ @@ -43,8 +43,8 @@ class TestTagplotMatplotlib(unittest.TestCase): """ options = PlotOptions(FIG1, ROTATION, POSITION) options.validate_input() - [figs, _] = tagplot_matplotlib(options) - self.assertIsInstance(figs[0], Figure) + figs_and_ids = tagplot_matplotlib(options) + self.assertIsInstance(figs_and_ids.figs[0], Figure) def test_mplerror(self): """ Test if Error is raised if wrong type of figures is given. """ -- GitLab From 40d475b428f6db2a5af2062c32b9276cde90fbea Mon Sep 17 00:00:00 2001 From: "Mayr, Hannes" Date: Tue, 13 Sep 2022 15:18:43 +0200 Subject: [PATCH 08/18] Publish data now in a subdirectory of the given destination folder with the corresponding ID as directory name. --- plotid/example.py | 2 -- plotid/publish.py | 80 +++++++++++++++++++++++-------------------- tests/test_publish.py | 32 +++++++++++------ 3 files changed, 64 insertions(+), 50 deletions(-) diff --git a/plotid/example.py b/plotid/example.py index 23b01e1..1bfe3ab 100644 --- a/plotid/example.py +++ b/plotid/example.py @@ -51,8 +51,6 @@ IMGS_AS_LIST = [IMG1, IMG2] FIGS_AND_IDS = tagplot(IMGS_AS_LIST, 'image', prefix=PROJECT_ID, id_method='time', location='west') -print(FIGS_AND_IDS.figure_ids) - # %% Publish # Arguments: Source directory or files as list, destination directory, figures, # plots or images. diff --git a/plotid/publish.py b/plotid/publish.py index 8866573..272df33 100644 --- a/plotid/publish.py +++ b/plotid/publish.py @@ -142,46 +142,53 @@ class PublishOptions: # 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') - - if overwrite_dir in ('yes', 'y'): - shutil.rmtree(self.dst_path) - else: - raise RuntimeError('publish has finished without an export.\n' - 'Rerun TagPlot if you need a new ID or ' - 'consider overwriting.') - - # Create invisible folder - dst_path_invisible = os.path.join(self.dst_path_head, - '.' + self.dst_dirname) - match self.data_storage: case 'centralized': self.centralized_data_storage() case 'individual': - try: - self.individual_data_storage(dst_path_invisible, - plot_paths) - except Exception as exc: - delete_dir = input('There was an error while publishing' - ' the data. Should the partially copied' - f' data at {dst_path_invisible} be' - ' removed? (yes/no[default])\n') - if delete_dir in ('yes', 'y'): - shutil.rmtree(dst_path_invisible) - raise RuntimeError('Publishing was unsuccessful. ' - 'Try re-running publish.') from exc + for i, plot in enumerate(plot_paths): + try: + # Create folder with ID as name + dst_path = os.path.join(self.dst_path, + self.figure_ids[i]) + dst_path_invisible = os.path.join(self.dst_path, '.' + + self.figure_ids[i]) + + # If dir with the same ID already exists ask user + # if it should be overwritten. + if os.path.isdir(dst_path): + warnings.warn(f'Folder "{dst_path}" already exists' + ' – plot has already been published.' + ) + 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) + else: + raise RuntimeError('publish has finished ' + 'without an export.\nRerun ' + 'tagplot if you need a new' + ' ID or consider ' + 'overwriting.') + + self.individual_data_storage(dst_path_invisible, plot) + # If export was successful, make the directory visible + os.rename(dst_path_invisible, dst_path) + except FileExistsError as exc: + delete_dir = input('There was an error while ' + 'publishing the data. Should the ' + 'partially copied data at ' + f'{dst_path_invisible} be' + ' removed? (yes/no[default])\n') + if delete_dir in ('yes', 'y'): + shutil.rmtree(dst_path_invisible) + raise RuntimeError('Publishing was unsuccessful. ' + 'Try re-running publish.') from exc case _: raise ValueError(f'The data storage method {self.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.') @@ -199,7 +206,7 @@ class PublishOptions: """ # Does nothing, not implemented yet - def individual_data_storage(self, destination, pic_paths): + def individual_data_storage(self, destination, pic_path): """ Store all the data in an individual directory. @@ -229,10 +236,9 @@ class PublishOptions: # Copy script that calls this function to folder 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) + if os.path.isfile(pic_path): + shutil.copy2(pic_path, destination) + os.remove(pic_path) def publish(figs_and_ids, src_datapath, dst_path, plot_name, **kwargs): diff --git a/tests/test_publish.py b/tests/test_publish.py index 8d1ce28..74c324c 100644 --- a/tests/test_publish.py +++ b/tests/test_publish.py @@ -22,7 +22,6 @@ 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(figsize=[6.4, 4.8], dpi=100) FIG2 = plt.figure(figsize=[6.4, 4.8], dpi=100) FIGS_AS_LIST = [FIG, FIG2] @@ -51,7 +50,8 @@ class TestPublish(unittest.TestCase): """ Test publish and check if an exported picture file exists. """ publish(PlotIDTransfer(FIG, 'testID'), SRC_DIR, DST_PATH + '/', PIC_NAME, data_storage='individual') - assert os.path.isfile(os.path.join(DST_PATH, PIC_NAME + '.png')) + assert os.path.isfile(os.path.join(DST_PATH, 'testID', + 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 ' @@ -63,8 +63,9 @@ class TestPublish(unittest.TestCase): files exist. """ publish(FIGS_AND_IDS, SRC_DIR, DST_PATH, PIC_NAME_LIST) - for name in PIC_NAME_LIST: - assert os.path.isfile(os.path.join(DST_PATH, name + '.png')) + for i, name in enumerate(PIC_NAME_LIST): + assert os.path.isfile(os.path.join(DST_PATH, IDS_AS_LIST[i], + name + '.png')) def test_wrong_ids(self): """ Test if Error is raised if IDs are of wrong type. """ @@ -82,9 +83,11 @@ class TestPublish(unittest.TestCase): files_and_dir.append(SRC_DIR) publish(FIGS_AND_IDS, files_and_dir, DST_PATH, PIC_NAME_LIST, data_storage='individual') - assert os.path.isdir(DST_PATH) - for file in SRC_FILES: - assert os.path.isfile(os.path.join(DST_PATH, file)) + for identifier in IDS_AS_LIST: + for file in SRC_FILES: + path = os.path.join(DST_PATH, identifier) + assert os.path.isdir(path) + assert os.path.isfile(os.path.join(path, file)) def test_src_directory(self): """ Test if Error is raised when source directory does not exist.""" @@ -149,6 +152,7 @@ class TestPublish(unittest.TestCase): destination directory. """ os.mkdir(DST_PATH) + os.mkdir(os.path.join(DST_PATH, IDS_AS_LIST[0])) # Mock user input as 'yes' with patch('builtins.input', return_value='yes'): publish(FIGS_AND_IDS, SRC_DIR, DST_PATH, PIC_NAME, @@ -164,6 +168,8 @@ class TestPublish(unittest.TestCase): an existing destination directory by user input 'no'. """ os.mkdir(DST_PATH) + os.mkdir(os.path.join(DST_PATH, IDS_AS_LIST[0])) + os.mkdir(os.path.join(DST_PATH, IDS_AS_LIST[1])) # Mock user input as 'no' with patch('builtins.input', return_value='no'): with self.assertRaises(RuntimeError): @@ -179,6 +185,8 @@ class TestPublish(unittest.TestCase): an existing destination directory by missing user input. """ os.mkdir(DST_PATH) + os.mkdir(os.path.join(DST_PATH, IDS_AS_LIST[0])) + os.mkdir(os.path.join(DST_PATH, IDS_AS_LIST[1])) # Mock user input as empty (no should be default). with patch('builtins.input', return_value=''): with self.assertRaises(RuntimeError): @@ -195,12 +203,14 @@ class TestPublish(unittest.TestCase): removed. To mock this, the invisible directory already exists. """ - os.mkdir(INVISIBLE_PATH) - # Mock user input as 'yes' - with patch('builtins.input', return_value='yes'): + os.mkdir(DST_PATH) + INVISIBLE_PATH1 = os.path.join(DST_PATH, '.' + IDS_AS_LIST[0]) + os.mkdir(INVISIBLE_PATH1) + # Mock user input as 'yes' for deleting the partial copied data + with patch('builtins.input', side_effect=['yes', 'yes']): with self.assertRaises(RuntimeError): publish(FIGS_AND_IDS, SRC_DIR, DST_PATH, PIC_NAME) - assert not os.path.isdir(INVISIBLE_PATH) + assert not os.path.isdir(INVISIBLE_PATH1) os.remove('test_picture1.png') os.remove('test_picture2.png') -- GitLab From f03217b3231835d6d0a034ac2fde1ee365ca91a7 Mon Sep 17 00:00:00 2001 From: "Mayr, Hannes" Date: Wed, 14 Sep 2022 10:26:46 +0200 Subject: [PATCH 09/18] Add validation test of PlotIDTransfer object, update documentation. --- plotid/publish.py | 13 +++++++++---- plotid/tagplot_matplotlib.py | 2 +- tests/test_publish.py | 14 +++++++++++--- 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/plotid/publish.py b/plotid/publish.py index 272df33..de95c9c 100644 --- a/plotid/publish.py +++ b/plotid/publish.py @@ -8,7 +8,8 @@ the plot is based on. Additionally, the script that produced the plot will be copied to the destination directory. Functions: - publish(str, str, figure, str) -> None + publish(PlotIDTransfer object, str or list of str, str or list of str) + -> None """ import os @@ -16,6 +17,7 @@ import shutil import sys import warnings from plotid.save_plot import save_plot +from plotid.plotoptions import PlotIDTransfer class PublishOptions: @@ -34,6 +36,9 @@ class PublishOptions: def __init__(self, figs_and_ids, src_datapaths, dst_path, plot_names, **kwargs): + if not isinstance(figs_and_ids, PlotIDTransfer): + raise RuntimeError('figs_and_ids is not an instance of ' + 'PlotIDTransfer.') self.figure = figs_and_ids.figs self.figure_ids = figs_and_ids.figure_ids self.src_datapaths = src_datapaths @@ -247,13 +252,13 @@ def publish(figs_and_ids, src_datapath, dst_path, plot_name, **kwargs): Parameters ---------- + figs_and_ids : PlotIDTransfer object + Contains figures tagged by tagplot() and their corresponding IDs. src_datapath : str or list of 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 or list of str + plot_name : str or list of str Name for the exported plot. **kwargs : dict, optional Extra arguments for additional publish options. diff --git a/plotid/tagplot_matplotlib.py b/plotid/tagplot_matplotlib.py index 6bc0038..1ba0572 100644 --- a/plotid/tagplot_matplotlib.py +++ b/plotid/tagplot_matplotlib.py @@ -25,7 +25,7 @@ def tagplot_matplotlib(plotid_object): Returns ------- - list with figures and IDs + PlotIDTransfer object """ # Check if plotid_object is a valid instance of PlotOptions if not isinstance(plotid_object, PlotOptions): diff --git a/tests/test_publish.py b/tests/test_publish.py index 74c324c..58611fb 100644 --- a/tests/test_publish.py +++ b/tests/test_publish.py @@ -67,6 +67,14 @@ class TestPublish(unittest.TestCase): assert os.path.isfile(os.path.join(DST_PATH, IDS_AS_LIST[i], name + '.png')) + def test_figs_and_ids(self): + """ + Test if RuntimeError is raised when figs_and_ids is not an + PlotIDTransfer object. + """ + with self.assertRaises(RuntimeError): + publish('FIGS_AND_IDS', SRC_DIR, DST_PATH, PIC_NAME_LIST) + def test_wrong_ids(self): """ Test if Error is raised if IDs are of wrong type. """ with self.assertRaises(TypeError): @@ -204,13 +212,13 @@ class TestPublish(unittest.TestCase): To mock this, the invisible directory already exists. """ os.mkdir(DST_PATH) - INVISIBLE_PATH1 = os.path.join(DST_PATH, '.' + IDS_AS_LIST[0]) - os.mkdir(INVISIBLE_PATH1) + invisible_path1 = os.path.join(DST_PATH, '.' + IDS_AS_LIST[0]) + os.mkdir(invisible_path1) # Mock user input as 'yes' for deleting the partial copied data with patch('builtins.input', side_effect=['yes', 'yes']): with self.assertRaises(RuntimeError): publish(FIGS_AND_IDS, SRC_DIR, DST_PATH, PIC_NAME) - assert not os.path.isdir(INVISIBLE_PATH1) + assert not os.path.isdir(invisible_path1) os.remove('test_picture1.png') os.remove('test_picture2.png') -- GitLab From 6a381c5cee8cb54ba2930d60b53d0eee2bdca156 Mon Sep 17 00:00:00 2001 From: "Mayr, Hannes" Date: Wed, 14 Sep 2022 12:17:42 +0200 Subject: [PATCH 10/18] Export plots to adjusted file names which include '.tmp.' and remove 'tmp' afterwards to avoid overwriting original pictures. --- plotid/example.py | 8 ++++---- plotid/publish.py | 18 +++++++++++++----- plotid/save_plot.py | 4 ++-- tests/test_publish.py | 4 ++-- tests/test_save_plot.py | 14 +++++++------- 5 files changed, 28 insertions(+), 20 deletions(-) diff --git a/plotid/example.py b/plotid/example.py index 1bfe3ab..4820b4d 100644 --- a/plotid/example.py +++ b/plotid/example.py @@ -44,16 +44,16 @@ FIGS_AS_LIST = [FIG1, FIG2] IMGS_AS_LIST = [IMG1, IMG2] # Example for how to use tagplot with matplotlib figures -# [TAGGED_FIGS, ID] = tagplot(FIGS_AS_LIST, 'matplotlib', location='west', -# id_method='random', prefix=PROJECT_ID) +# FIGS_AND_IDS = tagplot(FIGS_AS_LIST, 'matplotlib', location='west', +# id_method='random', prefix=PROJECT_ID) # Example for how to use tagplot with image files FIGS_AND_IDS = tagplot(IMGS_AS_LIST, 'image', prefix=PROJECT_ID, - id_method='time', location='west') + id_method='time', location='west') # %% Publish # Arguments: Source directory or files as list, destination directory, figures, # plots or images. publish(FIGS_AND_IDS, ['../README.md', '../docs', '../LICENSE'], - '/home/chief/Dokumente/fst/plotid_python/data/', 'Bild') + '/home/chief/Dokumente/fst/plotid_python/data/', 'image') diff --git a/plotid/publish.py b/plotid/publish.py index de95c9c..8d55b26 100644 --- a/plotid/publish.py +++ b/plotid/publish.py @@ -146,7 +146,7 @@ class PublishOptions: """ # Export plot figure to picture. plot_paths = save_plot(self.figure, self.plot_names) - + print(plot_paths) match self.data_storage: case 'centralized': self.centralized_data_storage() @@ -194,9 +194,9 @@ class PublishOptions: raise ValueError(f'The data storage method {self.data_storage}' ' is not available.') - 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.') + print(f'Publish was successful.\nYour plot(s), your' + f' data and your\nscript {sys.argv[0]}' + f'\nwere copied to {self.dst_path}.') def centralized_data_storage(self): """ @@ -240,10 +240,18 @@ class PublishOptions: # Copy script that calls this function to folder shutil.copy2(sys.argv[0], destination) - # Copy plot files to folder + if os.path.isfile(pic_path): + # Copy plot file to folder shutil.copy2(pic_path, destination) + # Remove by plotID exported .tmp plot os.remove(pic_path) + # Remove .tmp. from file name in destinaion + name_tmp, orig_ext = os.path.splitext(pic_path) + orig_name, _ = os.path.splitext(name_tmp) + final_file_path = orig_name + orig_ext + os.rename(os.path.join(destination, pic_path), + os.path.join(destination, final_file_path)) def publish(figs_and_ids, src_datapath, dst_path, plot_name, **kwargs): diff --git a/plotid/save_plot.py b/plotid/save_plot.py index 2c04585..5cf6f3b 100644 --- a/plotid/save_plot.py +++ b/plotid/save_plot.py @@ -57,10 +57,10 @@ def save_plot(figures, plot_names, extension='png'): for i, fig in enumerate(figures): if isinstance(fig, matplotlib.figure.Figure): plt.figure(fig) - plot_path.append(plot_names[i] + '.' + extension) + plot_path.append(plot_names[i] + '.tmp.' + extension) plt.savefig(plot_path[i]) elif all(x in str(type(fig)) for x in ['PIL', 'ImageFile']): - plot_path.append(plot_names[i] + '.' + extension) + plot_path.append(plot_names[i] + '.tmp.' + extension) fig.save(plot_path[i]) else: raise TypeError(f'Figure number {i} is not a valid figure object.') diff --git a/tests/test_publish.py b/tests/test_publish.py index 58611fb..7a9c5d8 100644 --- a/tests/test_publish.py +++ b/tests/test_publish.py @@ -219,8 +219,8 @@ class TestPublish(unittest.TestCase): with self.assertRaises(RuntimeError): publish(FIGS_AND_IDS, SRC_DIR, DST_PATH, PIC_NAME) assert not os.path.isdir(invisible_path1) - os.remove('test_picture1.png') - os.remove('test_picture2.png') + os.remove('test_picture1.tmp.png') + os.remove('test_picture2.tmp.png') def test_plot_names(self): """ Test if Error is raised if plot_name is not a string. """ diff --git a/tests/test_save_plot.py b/tests/test_save_plot.py index fbb9f22..a9f9f05 100644 --- a/tests/test_save_plot.py +++ b/tests/test_save_plot.py @@ -32,7 +32,7 @@ class TestSavePlot(unittest.TestCase): """ plot_paths = save_plot(FIGURE, [PLOT_NAME], extension='jpg') self.assertIsInstance(plot_paths, list) - os.remove(PLOT_NAME + '.jpg') + os.remove(PLOT_NAME + '.tmp.jpg') def test_save_plot_image_png(self): """ @@ -42,7 +42,7 @@ class TestSavePlot(unittest.TestCase): img1 = Image.open(IMG1) plot_paths = save_plot(img1, [PLOT_NAME]) self.assertIsInstance(plot_paths, list) - os.remove(PLOT_NAME + '.png') + os.remove(PLOT_NAME + '.tmp.png') def test_save_plot_image_jpg(self): """ @@ -53,8 +53,8 @@ class TestSavePlot(unittest.TestCase): imgs_as_list = [img2, img2] plot_paths = save_plot(imgs_as_list, [PLOT_NAME], extension='jpg') self.assertIsInstance(plot_paths, list) - os.remove(PLOT_NAME + '1.jpg') - os.remove(PLOT_NAME + '2.jpg') + os.remove(PLOT_NAME + '1.tmp.jpg') + os.remove(PLOT_NAME + '2.tmp.jpg') def test_more_figs_than_names(self): """ @@ -64,8 +64,8 @@ class TestSavePlot(unittest.TestCase): 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') + assert os.path.isfile(PLOT_NAME + f'{i}.tmp.png') + os.remove(PLOT_NAME + f'{i}.tmp.png') def test_more_names_than_figs(self): """ Test if Error is raised if more names than figures are given. """ @@ -84,7 +84,7 @@ class TestSavePlot(unittest.TestCase): """ with self.assertRaises(TypeError): save_plot([FIGURE, 'figure', FIGURE], 'PLOT_NAME', extension='jpg') - os.remove('PLOT_NAME1.jpg') + os.remove('PLOT_NAME1.tmp.jpg') def tearDown(self): os.remove(IMG1) -- GitLab From d397666fa0d93ea215562852da591c2af63eaf2e Mon Sep 17 00:00:00 2001 From: "Mayr, Hannes" Date: Wed, 14 Sep 2022 12:20:10 +0200 Subject: [PATCH 11/18] Linting. --- plotid/example.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plotid/example.py b/plotid/example.py index 4820b4d..153e2f7 100644 --- a/plotid/example.py +++ b/plotid/example.py @@ -49,7 +49,7 @@ IMGS_AS_LIST = [IMG1, IMG2] # Example for how to use tagplot with image files FIGS_AND_IDS = tagplot(IMGS_AS_LIST, 'image', prefix=PROJECT_ID, - id_method='time', location='west') + id_method='time', location='west') # %% Publish # Arguments: Source directory or files as list, destination directory, figures, -- GitLab From ff690e1cee914c198074765f12954f798defd2b1 Mon Sep 17 00:00:00 2001 From: "Mayr, Hannes" Date: Wed, 14 Sep 2022 15:38:34 +0200 Subject: [PATCH 12/18] Rework examples' structure and split them into two separate examples. --- examples/example_image1.png | Bin 0 -> 27441 bytes examples/example_image2.png | Bin 0 -> 26581 bytes examples/image_example.py | 40 ++++++++++++++++++ .../matplotlib_example.py | 21 +++------ 4 files changed, 47 insertions(+), 14 deletions(-) create mode 100644 examples/example_image1.png create mode 100644 examples/example_image2.png create mode 100644 examples/image_example.py rename plotid/example.py => examples/matplotlib_example.py (61%) diff --git a/examples/example_image1.png b/examples/example_image1.png new file mode 100644 index 0000000000000000000000000000000000000000..fe0d91339e7e24fbdf4a01aeee61634c28643026 GIT binary patch literal 27441 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 OC{yF3Mx}=C3I7iX0s-y- literal 0 HcmV?d00001 diff --git a/examples/example_image2.png b/examples/example_image2.png new file mode 100644 index 0000000000000000000000000000000000000000..ca9bf477a3e9ab3720bdff6b39887425145749ec GIT binary patch literal 26581 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%0D}3@#Zi{IXo>cE(G!zB^{lnv-3$rK0q<%CZBv5 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 NbTkaqU#Qwd{4czf(x?Cc literal 0 HcmV?d00001 diff --git a/examples/image_example.py b/examples/image_example.py new file mode 100644 index 0000000..29f173d --- /dev/null +++ b/examples/image_example.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +""" +Example workflow for integrating plotID with jpg or png images. + +With tagplot() an ID can be generated and printed on the plot. To export the +plot along with the corresponding research data and the plot generating +script use the function publish(). +""" + +# %% Import modules +from plotid.tagplot import tagplot +from plotid.publish import publish + +# %% Set optional Project ID, which will be placed in front of the generated ID +PROJECT_ID = "MR05_" + +# %% Read example images +IMG1 = 'example_image1.png' +IMG2 = 'example_image2.png' + +# %% TagPlot + +# If multiple images should be tagged, they must be provided as list. +IMGS_AS_LIST = [IMG1, IMG2] + +# Example for how to use tagplot with image files +FIGS_AND_IDS = tagplot(IMGS_AS_LIST, 'image', prefix=PROJECT_ID, + id_method='time', location='west') +# Required arguments: tagplot(images as list, desired plot engine) + + +# %% Publish + +# Export your tagged images, copy the research data that generated the images, +# specify the destination folder and give a name for the exported image files. + +publish(FIGS_AND_IDS, ['../README.md', '../docs', '../LICENSE'], + './data', 'my_image') +# Required arguments: publish(output of tagplot(), list of files, +# path to destination folder, name(s) for the resulting images) diff --git a/plotid/example.py b/examples/matplotlib_example.py similarity index 61% rename from plotid/example.py rename to examples/matplotlib_example.py index 153e2f7..7dbe6b7 100644 --- a/plotid/example.py +++ b/examples/matplotlib_example.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- """ -Example workflow for integrating plotID. +Example workflow for integrating plotID with matplotlib figures. With tagplot() an ID can be generated and printed on the plot. To export the plot along with the corresponding research data and the plot generating @@ -24,36 +24,29 @@ y_2 = np.sin(x) + 2 # %% Create sample figures # 1. figure -IMG1 = 'image1.png' FIG1 = plt.figure() plt.plot(x, y, color='black') plt.plot(x, y_2, color='yellow') -plt.savefig(IMG1) # 2. figure -IMG2 = 'image2.png' FIG2 = plt.figure() plt.plot(x, y, color='blue') plt.plot(x, y_2, color='red') -plt.savefig(IMG2) # %% TagPlot # If multiple figures should be tagged, figures must be provided as list. FIGS_AS_LIST = [FIG1, FIG2] -IMGS_AS_LIST = [IMG1, IMG2] # Example for how to use tagplot with matplotlib figures -# FIGS_AND_IDS = tagplot(FIGS_AS_LIST, 'matplotlib', location='west', -# id_method='random', prefix=PROJECT_ID) +FIGS_AND_IDS = tagplot(FIGS_AS_LIST, 'matplotlib', location='west', + id_method='random', prefix=PROJECT_ID) +# Required arguments: tagplot(images as list, desired plot engine) -# Example for how to use tagplot with image files -FIGS_AND_IDS = tagplot(IMGS_AS_LIST, 'image', prefix=PROJECT_ID, - id_method='time', location='west') # %% Publish -# Arguments: Source directory or files as list, destination directory, figures, -# plots or images. publish(FIGS_AND_IDS, ['../README.md', '../docs', '../LICENSE'], - '/home/chief/Dokumente/fst/plotid_python/data/', 'image') + './data', 'my_plot') +# Required arguments: publish(output of tagplot(), list of files, +# path to destination folder, name(s) for the resulting images) -- GitLab From 15cba7a6f816bbc579afba32279fa9e93cfe3112 Mon Sep 17 00:00:00 2001 From: "Mayr, Hannes" Date: Wed, 21 Sep 2022 09:31:58 +0200 Subject: [PATCH 13/18] Resolve "PNG file is removed from working directory upon export" --- plotid/example.py | 6 +++--- plotid/publish.py | 18 +++++++++++++----- plotid/save_plot.py | 4 ++-- tests/test_publish.py | 4 ++-- tests/test_save_plot.py | 14 +++++++------- 5 files changed, 27 insertions(+), 19 deletions(-) diff --git a/plotid/example.py b/plotid/example.py index 1bfe3ab..153e2f7 100644 --- a/plotid/example.py +++ b/plotid/example.py @@ -44,8 +44,8 @@ FIGS_AS_LIST = [FIG1, FIG2] IMGS_AS_LIST = [IMG1, IMG2] # Example for how to use tagplot with matplotlib figures -# [TAGGED_FIGS, ID] = tagplot(FIGS_AS_LIST, 'matplotlib', location='west', -# id_method='random', prefix=PROJECT_ID) +# FIGS_AND_IDS = tagplot(FIGS_AS_LIST, 'matplotlib', location='west', +# id_method='random', prefix=PROJECT_ID) # Example for how to use tagplot with image files FIGS_AND_IDS = tagplot(IMGS_AS_LIST, 'image', prefix=PROJECT_ID, @@ -56,4 +56,4 @@ FIGS_AND_IDS = tagplot(IMGS_AS_LIST, 'image', prefix=PROJECT_ID, # plots or images. publish(FIGS_AND_IDS, ['../README.md', '../docs', '../LICENSE'], - '/home/chief/Dokumente/fst/plotid_python/data/', 'Bild') + '/home/chief/Dokumente/fst/plotid_python/data/', 'image') diff --git a/plotid/publish.py b/plotid/publish.py index de95c9c..8d55b26 100644 --- a/plotid/publish.py +++ b/plotid/publish.py @@ -146,7 +146,7 @@ class PublishOptions: """ # Export plot figure to picture. plot_paths = save_plot(self.figure, self.plot_names) - + print(plot_paths) match self.data_storage: case 'centralized': self.centralized_data_storage() @@ -194,9 +194,9 @@ class PublishOptions: raise ValueError(f'The data storage method {self.data_storage}' ' is not available.') - 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.') + print(f'Publish was successful.\nYour plot(s), your' + f' data and your\nscript {sys.argv[0]}' + f'\nwere copied to {self.dst_path}.') def centralized_data_storage(self): """ @@ -240,10 +240,18 @@ class PublishOptions: # Copy script that calls this function to folder shutil.copy2(sys.argv[0], destination) - # Copy plot files to folder + if os.path.isfile(pic_path): + # Copy plot file to folder shutil.copy2(pic_path, destination) + # Remove by plotID exported .tmp plot os.remove(pic_path) + # Remove .tmp. from file name in destinaion + name_tmp, orig_ext = os.path.splitext(pic_path) + orig_name, _ = os.path.splitext(name_tmp) + final_file_path = orig_name + orig_ext + os.rename(os.path.join(destination, pic_path), + os.path.join(destination, final_file_path)) def publish(figs_and_ids, src_datapath, dst_path, plot_name, **kwargs): diff --git a/plotid/save_plot.py b/plotid/save_plot.py index 2c04585..5cf6f3b 100644 --- a/plotid/save_plot.py +++ b/plotid/save_plot.py @@ -57,10 +57,10 @@ def save_plot(figures, plot_names, extension='png'): for i, fig in enumerate(figures): if isinstance(fig, matplotlib.figure.Figure): plt.figure(fig) - plot_path.append(plot_names[i] + '.' + extension) + plot_path.append(plot_names[i] + '.tmp.' + extension) plt.savefig(plot_path[i]) elif all(x in str(type(fig)) for x in ['PIL', 'ImageFile']): - plot_path.append(plot_names[i] + '.' + extension) + plot_path.append(plot_names[i] + '.tmp.' + extension) fig.save(plot_path[i]) else: raise TypeError(f'Figure number {i} is not a valid figure object.') diff --git a/tests/test_publish.py b/tests/test_publish.py index 58611fb..7a9c5d8 100644 --- a/tests/test_publish.py +++ b/tests/test_publish.py @@ -219,8 +219,8 @@ class TestPublish(unittest.TestCase): with self.assertRaises(RuntimeError): publish(FIGS_AND_IDS, SRC_DIR, DST_PATH, PIC_NAME) assert not os.path.isdir(invisible_path1) - os.remove('test_picture1.png') - os.remove('test_picture2.png') + os.remove('test_picture1.tmp.png') + os.remove('test_picture2.tmp.png') def test_plot_names(self): """ Test if Error is raised if plot_name is not a string. """ diff --git a/tests/test_save_plot.py b/tests/test_save_plot.py index fbb9f22..a9f9f05 100644 --- a/tests/test_save_plot.py +++ b/tests/test_save_plot.py @@ -32,7 +32,7 @@ class TestSavePlot(unittest.TestCase): """ plot_paths = save_plot(FIGURE, [PLOT_NAME], extension='jpg') self.assertIsInstance(plot_paths, list) - os.remove(PLOT_NAME + '.jpg') + os.remove(PLOT_NAME + '.tmp.jpg') def test_save_plot_image_png(self): """ @@ -42,7 +42,7 @@ class TestSavePlot(unittest.TestCase): img1 = Image.open(IMG1) plot_paths = save_plot(img1, [PLOT_NAME]) self.assertIsInstance(plot_paths, list) - os.remove(PLOT_NAME + '.png') + os.remove(PLOT_NAME + '.tmp.png') def test_save_plot_image_jpg(self): """ @@ -53,8 +53,8 @@ class TestSavePlot(unittest.TestCase): imgs_as_list = [img2, img2] plot_paths = save_plot(imgs_as_list, [PLOT_NAME], extension='jpg') self.assertIsInstance(plot_paths, list) - os.remove(PLOT_NAME + '1.jpg') - os.remove(PLOT_NAME + '2.jpg') + os.remove(PLOT_NAME + '1.tmp.jpg') + os.remove(PLOT_NAME + '2.tmp.jpg') def test_more_figs_than_names(self): """ @@ -64,8 +64,8 @@ class TestSavePlot(unittest.TestCase): 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') + assert os.path.isfile(PLOT_NAME + f'{i}.tmp.png') + os.remove(PLOT_NAME + f'{i}.tmp.png') def test_more_names_than_figs(self): """ Test if Error is raised if more names than figures are given. """ @@ -84,7 +84,7 @@ class TestSavePlot(unittest.TestCase): """ with self.assertRaises(TypeError): save_plot([FIGURE, 'figure', FIGURE], 'PLOT_NAME', extension='jpg') - os.remove('PLOT_NAME1.jpg') + os.remove('PLOT_NAME1.tmp.jpg') def tearDown(self): os.remove(IMG1) -- GitLab From a91cb997aba14fe3c4da23424181450b790d76c6 Mon Sep 17 00:00:00 2001 From: "Mayr, Hannes" Date: Wed, 21 Sep 2022 09:39:05 +0200 Subject: [PATCH 14/18] QR Codes now work with mpl and image engine. --- plotid/create_id.py | 22 ++++++++++++++++++++++ plotid/example.py | 3 ++- plotid/plotoptions.py | 3 +++ plotid/tagplot.py | 3 +-- plotid/tagplot_image.py | 6 +++++- plotid/tagplot_matplotlib.py | 10 ++++++++-- requirements.txt | 1 + tests/test_create_id.py | 7 +++++++ tests/test_plotoptions.py | 2 +- tests/test_tagplot_image.py | 2 +- tests/test_tagplot_matplotlib.py | 2 +- 11 files changed, 52 insertions(+), 9 deletions(-) diff --git a/plotid/create_id.py b/plotid/create_id.py index bfab1a5..f28d94a 100644 --- a/plotid/create_id.py +++ b/plotid/create_id.py @@ -7,6 +7,7 @@ Functions: """ import time import uuid +import qrcode def create_id(id_method): @@ -40,3 +41,24 @@ def create_id(id_method): '"time": Unix time converted to hexadecimal\n' '"random": Random UUID') return figure_id + + +def create_qrcode(figure_id): + """ + Create a QR Code from an identifier. + + Parameters + ---------- + figure_id : str + Identifier which will be embedded in the qrcode. + + Returns + ------- + QR Code as PilImage. + """ + qrc = qrcode.QRCode(version=1, box_size=10, border=0) + qrc.add_data(figure_id) + qrc.make(fit=True) + img = qrc.make_image(fill_color="black", back_color="white") + + return img diff --git a/plotid/example.py b/plotid/example.py index 153e2f7..f137a5a 100644 --- a/plotid/example.py +++ b/plotid/example.py @@ -37,7 +37,7 @@ plt.plot(x, y, color='blue') plt.plot(x, y_2, color='red') plt.savefig(IMG2) -# %% TagPlot +# %% tagplot # If multiple figures should be tagged, figures must be provided as list. FIGS_AS_LIST = [FIG1, FIG2] @@ -51,6 +51,7 @@ IMGS_AS_LIST = [IMG1, IMG2] FIGS_AND_IDS = tagplot(IMGS_AS_LIST, 'image', prefix=PROJECT_ID, id_method='time', location='west') + # %% Publish # Arguments: Source directory or files as list, destination directory, figures, # plots or images. diff --git a/plotid/plotoptions.py b/plotid/plotoptions.py index dee0f71..41e4ba8 100644 --- a/plotid/plotoptions.py +++ b/plotid/plotoptions.py @@ -33,6 +33,8 @@ class PlotOptions: id_method for creating the ID. Create an ID by Unix time is referenced as 'time', create a random ID with id_method='random'. The default is 'time'. + qrcode : bool, optional + Experimental status. Print qrcode on exported plot. Default: False. """ def __init__(self, figs, rotation, position, **kwargs): @@ -43,6 +45,7 @@ class PlotOptions: self.position = position self.prefix = kwargs.get('prefix', '') self.id_method = kwargs.get('id_method', 'time') + self.qrcode = kwargs.get('qrcode', False) def __str__(self): """Representation if an object of this class is printed.""" diff --git a/plotid/tagplot.py b/plotid/tagplot.py index bb16c1a..41d4ae6 100644 --- a/plotid/tagplot.py +++ b/plotid/tagplot.py @@ -80,8 +80,7 @@ def tagplot(figs, engine, location='east', **kwargs): position = (0.75, 0.015) case 'custom': # TODO: Get rotation and position from user input & check if valid - rotation = 0 - position = (0.5, 0.5) + pass case _: warnings.warn(f'Location "{location}" is not a defined ' 'location, TagPlot uses location "east" ' diff --git a/plotid/tagplot_image.py b/plotid/tagplot_image.py index decc9e5..05b4ab1 100644 --- a/plotid/tagplot_image.py +++ b/plotid/tagplot_image.py @@ -7,7 +7,7 @@ Functions: """ import os from PIL import Image, ImageDraw, ImageFont, ImageOps -from plotid.create_id import create_id +from plotid.create_id import create_id, create_qrcode from plotid.plotoptions import PlotOptions, PlotIDTransfer @@ -55,6 +55,10 @@ def tagplot_image(plotid_object): (int(img.width*plotid_object.position[0]), int(img.height*(1-plotid_object.position[1]))), txt) + if plotid_object.qrcode: + qrcode = create_qrcode(img_id) + qrcode.thumbnail((100, 100), Image.ANTIALIAS) + img.paste(qrcode, box=(img.width-100, img.height-100)) plotid_object.figs[i] = img figs_and_ids = PlotIDTransfer(plotid_object.figs, plotid_object.figure_ids) diff --git a/plotid/tagplot_matplotlib.py b/plotid/tagplot_matplotlib.py index 1ba0572..eef060c 100644 --- a/plotid/tagplot_matplotlib.py +++ b/plotid/tagplot_matplotlib.py @@ -8,7 +8,8 @@ Functions: import matplotlib import matplotlib.pyplot as plt -from plotid.create_id import create_id +from PIL import Image +from plotid.create_id import create_id, create_qrcode from plotid.plotoptions import PlotOptions, PlotIDTransfer @@ -51,7 +52,12 @@ def tagplot_matplotlib(plotid_object): s=fig_id, ha='left', wrap=True, rotation=plotid_object.rotation, fontsize=fontsize, color=color) - fig.tight_layout() + + if plotid_object.qrcode: + qrcode = create_qrcode(fig_id) + qrcode.thumbnail((100, 100), Image.ANTIALIAS) + fig.figimage(qrcode, fig.bbox.xmax - 100, 0, cmap='bone') + fig.tight_layout() figs_and_ids = PlotIDTransfer(plotid_object.figs, plotid_object.figure_ids) return figs_and_ids diff --git a/requirements.txt b/requirements.txt index a70ca0a..2852cbf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,4 +8,5 @@ packaging==21.3 Pillow==9.1.0 pyparsing==3.0.8 python-dateutil==2.8.2 +qrcode==7.3.1 six==1.16.0 diff --git a/tests/test_create_id.py b/tests/test_create_id.py index b47ae91..2f8e4b2 100644 --- a/tests/test_create_id.py +++ b/tests/test_create_id.py @@ -5,7 +5,9 @@ Unittests for create_id """ import unittest +import qrcode import plotid.create_id as cid +from plotid.create_id import create_qrcode class TestCreateID(unittest.TestCase): @@ -30,6 +32,11 @@ class TestCreateID(unittest.TestCase): self.assertEqual(len(cid.create_id('time')), 10) self.assertEqual(len(cid.create_id('random')), 8) + def test_qrcode(self): + """Test if qrcode returns a image.""" + self.assertIsInstance(create_qrcode('test_ID'), + qrcode.image.pil.PilImage) + if __name__ == '__main__': unittest.main() diff --git a/tests/test_plotoptions.py b/tests/test_plotoptions.py index ee752c2..87b4f30 100644 --- a/tests/test_plotoptions.py +++ b/tests/test_plotoptions.py @@ -44,7 +44,7 @@ class TestTagplot(unittest.TestCase): ": {'figs': " "'FIG', 'figure_ids': [], 'rotation': 270, 'position'" ": (100, 200), 'prefix': 'xyz', 'id_method': " - "'random'}") + "'random', 'qrcode': False}") def test_str_plotidtransfer(self): """ diff --git a/tests/test_tagplot_image.py b/tests/test_tagplot_image.py index ad42d82..2c236c5 100644 --- a/tests/test_tagplot_image.py +++ b/tests/test_tagplot_image.py @@ -40,7 +40,7 @@ class TestTagplotImage(unittest.TestCase): respectively. """ options = PlotOptions(IMGS_AS_LIST, ROTATION, POSITION, - prefix=PROJECT_ID, id_method=METHOD) + prefix=PROJECT_ID, id_method=METHOD, qrcode=True) options.validate_input() figs_and_ids = tagplot_image(options) self.assertIsInstance(figs_and_ids.figs[0], diff --git a/tests/test_tagplot_matplotlib.py b/tests/test_tagplot_matplotlib.py index f6b9b4c..87cc22b 100644 --- a/tests/test_tagplot_matplotlib.py +++ b/tests/test_tagplot_matplotlib.py @@ -30,7 +30,7 @@ class TestTagplotMatplotlib(unittest.TestCase): def test_mplfigures(self): """ Test of returned objects. Check if they are matplotlib figures. """ options = PlotOptions(FIGS_AS_LIST, ROTATION, POSITION, - prefix=PROJECT_ID, id_method=METHOD) + prefix=PROJECT_ID, id_method=METHOD, qrcode=True) options.validate_input() figs_and_ids = tagplot_matplotlib(options) self.assertIsInstance(figs_and_ids.figs[0], Figure) -- GitLab From d2905ccd676b8b7987942ffc533aa0f6453011ec Mon Sep 17 00:00:00 2001 From: "Mayr, Hannes" Date: Wed, 21 Sep 2022 16:05:11 +0200 Subject: [PATCH 15/18] Include Readme into docs. Closes #75. --- .gitlab-ci.yml | 2 +- docs/source/conf.py | 3 ++- docs/source/index.rst | 2 ++ docs/source/readme_link.rst | 5 +++++ setup.cfg | 7 +++++-- 5 files changed, 15 insertions(+), 4 deletions(-) create mode 100644 docs/source/readme_link.rst diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 3972355..27acee3 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -64,7 +64,7 @@ test: pages: stage: docs script: - - pip install -U sphinx sphinx-autoapi sphinx_rtd_theme # sphinx_panels + - pip install -U sphinx sphinx-autoapi sphinx_rtd_theme myst-parser # sphinx_panels - cd docs - make html - mv build/html/ ../public diff --git a/docs/source/conf.py b/docs/source/conf.py index fe798ee..a866324 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -36,7 +36,8 @@ release = __version__ # ones. extensions = [ 'sphinx.ext.napoleon', - 'autoapi.extension' + 'autoapi.extension', + "myst_parser" ] autoapi_type = 'python' autoapi_dirs = ['../../plotid', '../../tests'] diff --git a/docs/source/index.rst b/docs/source/index.rst index 34d0792..925e89d 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -10,6 +10,8 @@ Welcome to PlotID's documentation! :maxdepth: 2 :caption: Contents: + README + Indices and tables ================== diff --git a/docs/source/readme_link.rst b/docs/source/readme_link.rst new file mode 100644 index 0000000..5b96f4d --- /dev/null +++ b/docs/source/readme_link.rst @@ -0,0 +1,5 @@ +Readme File +----------- + +.. include:: ../../README.md + :parser: myst_parser.sphinx_ \ No newline at end of file diff --git a/setup.cfg b/setup.cfg index 5abba63..92f342b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -32,5 +32,8 @@ install_requires = [options.extras_require] test = coverage - - +docs = + sphinx + sphinx-autoapi + sphinx-rtd-theme + myst-parser -- GitLab From 667276079f2daf5945b30ee22164453696c0b1e9 Mon Sep 17 00:00:00 2001 From: "Mayr, Hannes" Date: Wed, 21 Sep 2022 16:18:59 +0200 Subject: [PATCH 16/18] Update documentation. --- README.md | 16 +++++++++------- plotid/tagplot.py | 2 ++ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 1695685..bdaee1d 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ The argument "plot_engine" defines which plot engine was used to create the figu - 'matplotlib', which processes figures created by matplotlib. - 'image', which processes pictures with common extensions (jpg, png, etc.). -tagplot returns a list that contains two lists each with as many entries as figures were given. The first list contains the tagged figures. The second list contains the corresponding IDs as strings +tagplot returns a PlotIDTransfer object that contains the tagged figures and the corresponding IDs as strings. Optional parameters can be used to customize the tag process. - prefix : str, optional @@ -59,23 +59,25 @@ Optional parameters can be used to customize the tag process. id_method for creating the ID. Create an ID by Unix time is referenced as 'time', create a random ID with id_method='random'. The default is 'time'. - location : string, optional Location for ID to be displayed on the plot. Default is 'east'. +- qrcode : boolean, optional + Experimental support for encoding the ID in a QR Code. Example: ```python FIG1 = plt.figure() FIG2 = plt.figure() FIGS_AS_LIST = [FIG1, FIG2] -[TAGGED_FIGS, ID] = tagplot(FIGS_AS_LIST, 'matplotlib', prefix='XY23_', id_method='random', location='west') +FIGS_AND_IDS = tagplot(FIGS_AS_LIST, 'matplotlib', prefix='XY23_', id_method='random', location='west') ``` ### publish() Save plot, data and measuring script. It is possible to export multiple figures at once. -`publish(src_datapath, dst_path, figure, plot_name)` - +`publish(figs_and_ids, src_datapath, dst_path, plot_name)` + +- "figs_and_ids" must be a PlotIDTransfer object. Therefore, it can be directly passed from tagplot() to publish(). - "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 or a list of figures that were tagged and now should be saved as pictures. If image files were tagged, all of them need to have the same file extension. - "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. @@ -84,7 +86,7 @@ Optional parameters can be used to customize the publish process. - 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. Example: -`publish('/home/user/Documents/research_data', '/home/user/Documents/exported_data', FIG1, 'EnergyOverTime-Plot')` +`publish(figs_and_ids, '/home/user/Documents/research_data', '/home/user/Documents/exported_data', 'EnergyOverTime-Plot')` ## Build If you want to build plotID yourself, follow these steps: @@ -100,4 +102,4 @@ If you want to build plotID yourself, follow these steps: ## Documentation If you have more questions about plotID, please have a look at the [documentation](https://plotid.pages.rwth-aachen.de/plotid_python). -Also have a look at the [example.py](plotid/example.py) that is shipped with plotID. +Also have a look at the [examples](./examples) that are shipped with plotID. diff --git a/plotid/tagplot.py b/plotid/tagplot.py index 41d4ae6..b0f365e 100644 --- a/plotid/tagplot.py +++ b/plotid/tagplot.py @@ -42,6 +42,8 @@ def tagplot(figs, engine, location='east', **kwargs): id_method for creating the ID. Create an ID by Unix time is referenced as 'time', create a random ID with id_method='random'. The default is 'time'. + qcode : boolean, optional + Experimental support for encoding the ID in a QR Code. Raises ------ -- GitLab From 91faf58df73d14779556f9a9fdb6749276409a38 Mon Sep 17 00:00:00 2001 From: "Mayr, Hannes" Date: Wed, 21 Sep 2022 16:24:23 +0200 Subject: [PATCH 17/18] Bump version number. --- plotid/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plotid/__init__.py b/plotid/__init__.py index b9b886c..d7cb4c7 100644 --- a/plotid/__init__.py +++ b/plotid/__init__.py @@ -10,5 +10,5 @@ research data, the plot is based on. Additionally, the script that created the plot will also be copied to the directory. """ -__version__ = '0.1.2' +__version__ = '0.2.0' __author__ = 'Institut Fluidsystemtechnik within nfdi4ing at TU Darmstadt' -- GitLab From 6c1469c672bf5f87adc3832d28ae321631f872ce Mon Sep 17 00:00:00 2001 From: "Mayr, Hannes" Date: Wed, 21 Sep 2022 16:41:02 +0200 Subject: [PATCH 18/18] Add qrcode package as dependency and update documentation. --- README.md | 2 +- plotid/__init__.py | 2 +- setup.cfg | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index bdaee1d..1c1b247 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ venv\Scripts\Activate.ps1 # Windows PowerShell ### From PyPi with pip 2. Install plotID -`pip install plotid --extra-index-url=https://test.pypi.org/simple/` +`pip install plotid` If you also want to run the unittests use `plotid[test]` instead of `plotid`. ### From source diff --git a/plotid/__init__.py b/plotid/__init__.py index d7cb4c7..306280a 100644 --- a/plotid/__init__.py +++ b/plotid/__init__.py @@ -10,5 +10,5 @@ research data, the plot is based on. Additionally, the script that created the plot will also be copied to the directory. """ -__version__ = '0.2.0' +__version__ = '0.2.1' __author__ = 'Institut Fluidsystemtechnik within nfdi4ing at TU Darmstadt' diff --git a/setup.cfg b/setup.cfg index 92f342b..dcda034 100644 --- a/setup.cfg +++ b/setup.cfg @@ -28,6 +28,7 @@ install_requires = Pillow matplotlib numpy + qrcode [options.extras_require] test = -- GitLab