diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000000000000000000000000000000000000..096ae14ebab27380feedb28bf923c29e112ab4ec --- /dev/null +++ b/.coveragerc @@ -0,0 +1,4 @@ +[run] +omit = + *example.py + */usr/* diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 1147c71c14b0dd9b485704e085cd0ecdd1e6f8d8..27b46ba817eb967e1f15d6cf89969048cf57f75e 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -47,8 +47,8 @@ PEP8: Pylint: stage: linting - allow_failure: true - script: find . -type f -name '*.py' | xargs pylint -rn --fail-under=7 # Find all python files and check the code with pylint. + # allow_failure: true + script: find . -type f -name '*.py' | xargs pylint -rn --rcfile='plotid/.pylintrc' # Find all python files and check the code with pylint. test: stage: testing @@ -56,7 +56,7 @@ test: - docker script: # - python -m unittest discover -s ./tests/ -p "test*" # deprecated unittest command - - python runner_tests.py + - python tests/runner_tests.py # - pip install tox flake8 # you can also use tox # - tox -e py36,flake8 diff --git a/README.md b/README.md index e873d62fc0f29ae08ed91cf1131273e856e1c5fe..8d481de3ce3336fb936684ef9f5748d771bc4c36 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,107 @@ -# plot_ID_python +# PlotID for Python This is the python PlotID project. +PlotID is a program connected to Research Data Management (RDM). It has two main functionalities: +1. Tag your plot with an identifier. +2. Export the resulting file to a specified directory along the corresponding research data, the plot is based on. Additionally, the script that created the plot will also be copied to the directory. -To run the program python version 3.10 is required. \ No newline at end of file +**Note:** To run PlotID python version ≥ 3.10 is required. + +## Installation +Currently there are two options to run PlotID. Either install it via pip from the Python Package Index (PyPi) or install PlotID from the source code. + +### From PyPi with pip +1. [Optional] Create a virtual environment and activate it: +```bash +pip install venv +mkdir venv +python3 -m venv +source venv/bin/activate +``` +2. Install PlotID +`pip install --upgrade --index-url https://test.pypi.org/simple/ example-package-plotid-test` + +### From source +1. Download the source code from [Gitlab](https://git.rwth-aachen.de/plotid/plotid_python): +`git clone https://git.rwth-aachen.de/plotid/plotid_python.git` +`cd plotid_python` +2. [Optional] Create a virtual environment: +```bash +pip install venv +mkdir venv +python3 -m venv +source venv/bin/activate +``` +3. Install dependencies +`pip install -r requirements.txt` +4. Install PlotID +`pip install .` + +## Usage +PlotID has two main functionalities: +1. Tag your plot with an identifier. +2. Export the resulting file to a specified directory along the corresponding research data, the plot is based on. Additionally, the script that created the plot will also be copied to the directory. + +### tagplot() +Tag your figure/plot with an ID. +`tagplot(figures, plot_engine)` +The variable "figures" can be a single figure or a list of multiple figures. +The argument "plot_engine" defines which plot engine was used to create the figures. It also determines which plot engine PlotID uses to place the ID on the plot. Currently supported plot engines are: +- 'matplotlib' + +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 + +Optional parameters can be used to customize the tag process. +- 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'. +- location : string, optional + Location for ID to be displayed on the plot. Default is 'east'. + +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') +``` + + +### publish() +Save plot, data and measuring script. +`publish(src_datapath, dst_path, figure, plot_name)` + +- "src_datapath" specifies the path to (raw) data that should be published. +- "dst_path" is the path to the destination directory, where all the data should be copied/exported to. +- "figure" expects the figure that was tagged and now should be saved as picture. +- "plot_name" will be the file name for the exported plot. + +Optional parameters can be used to customize the publish process. +- 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. + - 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') + +## Build +If you want to build PlotID yourself, follow these steps: +1. Download the source code from [Gitlab](https://git.rwth-aachen.de/plotid/plotid_python): +`git clone https://git.rwth-aachen.de/plotid/plotid_python.git` +`cd plotid_python` +2. [Optional] Create a virtual environment: +```bash +pip install venv +mkdir venv +python3 -m venv +source venv/bin/activate +``` +3. [Optional] Run unittests and coverage: +`python3 tests/runner_tests.py` +4. Build the package +`python3 -m build` + +## Documentation +If you have more questions about PlotID, please have a look at the [documentation](link-to-docs). +Also have a look at the example.py that is shipped with PlotID. diff --git a/create_id.py b/create_id.py deleted file mode 100644 index 84f2b83202d84345531128ca24dbbc89e7248952..0000000000000000000000000000000000000000 --- a/create_id.py +++ /dev/null @@ -1,36 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Create an identifier to print it on a plot. - -Functions: - create_id(int) -> string -""" -import time -import uuid - - -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 - - Returns - ------- - ID - """ - if id_method == 1: - ID = time.time() # UNIX Time - ID = hex(int(ID)) # convert time to hexadecimal - time.sleep(0.5) # break for avoiding duplicate IDs - elif id_method == 2: - ID = str(uuid.uuid4()) # creates a random UUID - ID = ID[0:8] # only use first 8 numbers - else: - raise ValueError( - f'Your chosen ID method "{id_method}" is not supported.\n' - 'At the moment these methods are available:\n' - '"1": Unix time converted to hexadecimal\n' - '"2": Random UUID') - return ID diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000000000000000000000000000000000000..d0c3cbf1020d5c292abdedf27627c6abe25e2293 --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = source +BUILDDIR = build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000000000000000000000000000000000000..747ffb7b3033659bdd2d1e6eae41ecb00358a45e --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=source +set BUILDDIR=build + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.https://www.sphinx-doc.org/ + exit /b 1 +) + +if "%1" == "" goto help + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/docs/source/conf.py b/docs/source/conf.py new file mode 100644 index 0000000000000000000000000000000000000000..7ca2459f4a60345e7602be2281fb6ed590a463be --- /dev/null +++ b/docs/source/conf.py @@ -0,0 +1,58 @@ +# Configuration file for the Sphinx documentation builder. +# +# This file only contains a selection of the most common options. For a full +# list see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +# import os +# import sys +# sys.path.insert(0, os.path.abspath('../../plotid')) + + +# -- Project information ----------------------------------------------------- + +project = 'PlotID' +copyright = '2022, Example Author' +author = 'Example Author' + +# The full version, including alpha/beta/rc tags +release = '0.0.6' + + +# -- General configuration --------------------------------------------------- + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'autoapi.extension' +] +autoapi_type = 'python' +autoapi_dirs = ['../../plotid', '../../tests'] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = [] + + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = 'alabaster' + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] diff --git a/docs/source/index.rst b/docs/source/index.rst new file mode 100644 index 0000000000000000000000000000000000000000..34d079234c933658bce40762274688153c631d04 --- /dev/null +++ b/docs/source/index.rst @@ -0,0 +1,19 @@ +.. PlotID documentation master file, created by + sphinx-quickstart on Tue Jun 21 14:09:27 2022. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to PlotID's documentation! +================================== + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/docs/source/modules.rst b/docs/source/modules.rst new file mode 100644 index 0000000000000000000000000000000000000000..dd354c8698ea6d990ff1b421ad0822ef78a8bb65 --- /dev/null +++ b/docs/source/modules.rst @@ -0,0 +1,7 @@ +plotid +====== + +.. toctree:: + :maxdepth: 4 + + plotid diff --git a/docs/source/plotid.rst b/docs/source/plotid.rst new file mode 100644 index 0000000000000000000000000000000000000000..005afcdb7f40bef97789edbedf0d42196589d5b4 --- /dev/null +++ b/docs/source/plotid.rst @@ -0,0 +1,85 @@ +plotid package +============== + +Submodules +---------- + +plotid.create\_id module +------------------------ + +.. automodule:: plotid.create_id + :members: + :undoc-members: + :show-inheritance: + +plotid.example module +--------------------- + +.. automodule:: plotid.example + :members: + :undoc-members: + :show-inheritance: + +plotid.filecompare module +------------------------- + +.. automodule:: plotid.filecompare + :members: + :undoc-members: + :show-inheritance: + +plotid.hdf5\_external\_link module +---------------------------------- + +.. automodule:: plotid.hdf5_external_link + :members: + :undoc-members: + :show-inheritance: + +plotid.plotoptions module +------------------------- + +.. automodule:: plotid.plotoptions + :members: + :undoc-members: + :show-inheritance: + +plotid.publish module +--------------------- + +.. automodule:: plotid.publish + :members: + :undoc-members: + :show-inheritance: + +plotid.save\_plot module +------------------------ + +.. automodule:: plotid.save_plot + :members: + :undoc-members: + :show-inheritance: + +plotid.tagplot module +--------------------- + +.. automodule:: plotid.tagplot + :members: + :undoc-members: + :show-inheritance: + +plotid.tagplot\_matplotlib module +--------------------------------- + +.. automodule:: plotid.tagplot_matplotlib + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: plotid + :members: + :undoc-members: + :show-inheritance: diff --git a/example.py b/example.py deleted file mode 100644 index b6302f7d7895616b2435d252b0c8a3777d1399e2..0000000000000000000000000000000000000000 --- a/example.py +++ /dev/null @@ -1,61 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Example workflow for integrating plotID. - -With tagplot an ID can be generated an 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 -import numpy as np -from numpy import random -# import h5py as h5 -# import matplotlib -import matplotlib.pyplot as plt -from tagplot import tagplot -from publish import publish - -# %% Project ID -ProjectID = "MR04_" - -# %% Plot engine -plot_engine = "matplotlib" - -# %% Create sample data -x = np.linspace(0, 10, 100) -y = random.rand(100) + 2 -y_2 = np.sin(x) + 2 - -# %% Create figure - -# Create plot -color1 = 'black' -color2 = 'yellow' -# 1.Figure -fig1 = plt.figure() -plt.plot(x, y, color=color1) -plt.plot(x, y_2, color=color2) - -# 2.Figure -fig2 = plt.figure() -plt.plot(x, y, color=color2) -plt.plot(x, y_2, color=color1) - -fig = [fig1, fig2] - -# %% TagPlot -# p1 = PlotOptions(fig, plot_engine, prefix=ProjectID, -# method='2', location='east') -# [figs, ID] = p1.tagplot() -[figs, ID] = tagplot(fig, plot_engine, prefix=ProjectID, - id_method='2', location='west') - -# %% Figure als tiff-Datei abspeichern -for i, figure in enumerate(figs): - name = "Test"+str(i)+".tiff" - figure.savefig(name) - -# %% Publish -publish('tests', '/home/chief/Dokumente/fst/plotid_python/data', - fig1, 'Bild', 'individual') diff --git a/plotid/.pylintrc b/plotid/.pylintrc new file mode 100644 index 0000000000000000000000000000000000000000..40743c0f4e5afcd7e44a1512d484bda76b655e1f --- /dev/null +++ b/plotid/.pylintrc @@ -0,0 +1,4 @@ +[MASTER] +init-hook='import sys; sys.path.append("./plotid")' +fail-under=9 +ignore=conf.py diff --git a/plotid/__init__.py b/plotid/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/plotid/create_id.py b/plotid/create_id.py new file mode 100644 index 0000000000000000000000000000000000000000..450e2dae6c2608a210849d5120c692986c3a7da4 --- /dev/null +++ b/plotid/create_id.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +""" +Create an identifier to print it on a plot. + +Functions: + create_id(str) -> string +""" +import time +import uuid + + +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 + + Returns + ------- + figure_id + """ + match id_method: + case 'time': + figure_id = time.time() # UNIX Time + figure_id = hex(int(figure_id)) # convert time to hexadecimal + time.sleep(0.5) # 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 + case _: + raise ValueError( + f'Your chosen ID method "{id_method}" is not supported.\n' + 'At the moment these methods are available:\n' + '"time": Unix time converted to hexadecimal\n' + '"random": Random UUID') + return figure_id diff --git a/plotid/example.py b/plotid/example.py new file mode 100644 index 0000000000000000000000000000000000000000..fcf7713916dc9ffc92ff7b9508f8b2d8778dcb7a --- /dev/null +++ b/plotid/example.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- +""" +Example workflow for integrating plotID. + +With tagplot an ID can be generated an 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 +import numpy as np +# import h5py as h5 +import matplotlib.pyplot as plt +from plotid.tagplot import tagplot +from plotid.publish import publish + +# %% Set Project ID +PROJECT_ID = "MR04_" + +# %% Choose Plot engine +PLOT_ENGINE = "matplotlib" + +# %% Create sample data +x = np.linspace(0, 10, 100) +y = np.random.rand(100) + 2 +y_2 = np.sin(x) + 2 + +# %% Create figures + +# 1. figure +FIG1 = plt.figure() +plt.plot(x, y, color='black') +plt.plot(x, y_2, color='yellow') + +# 2. figure +FIG2 = plt.figure() +plt.plot(x, y, color='blue') +plt.plot(x, y_2, color='red') + +# %% TagPlot + +# If multiple figures should be tagged, figures must be provided as list. +FIGS_AS_LIST = [FIG1, FIG2] + +[TAGGED_FIGS, ID] = tagplot(FIGS_AS_LIST, PLOT_ENGINE, prefix=PROJECT_ID, + id_method='random', location='west') + +# %% Save figure as tiff-file, but publish also exports the plot to a picture +# file in the destination folder. +for i, figure in enumerate(TAGGED_FIGS): + NAME = "Test"+str(i)+".tiff" + figure.savefig(NAME) + +# %% Publish +# Arguments: Source directory, destination directory, figure, plot name, +# publish-mode). +publish('../../tests', '/home/chief/Dokumente/fst/plotid_python/data', + FIG1, 'Bild', 'individual') diff --git a/filecompare.py b/plotid/filecompare.py similarity index 100% rename from filecompare.py rename to plotid/filecompare.py diff --git a/HDF5_externalLink.py b/plotid/hdf5_external_link.py similarity index 100% rename from HDF5_externalLink.py rename to plotid/hdf5_external_link.py diff --git a/plotid/plotoptions.py b/plotid/plotoptions.py new file mode 100644 index 0000000000000000000000000000000000000000..6503422c73fe66f0fcd5a5fe9f44cdbd5ded1c7c --- /dev/null +++ b/plotid/plotoptions.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""Contains the PlotOption class.""" + + +class PlotOptions: + """ + Container objects which include all plot options provided by plotid. + + Methods + ------- + __init__ + validate_input: Check if input is correct type. + + Attributes + ---------- + figs : figure object + Figure that will be tagged. + prefix : str + Prefix that is placed before the ID. + id_method : int + 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). + """ + + def __init__(self, figs, prefix, id_method, rotation, position): + + self.figs = figs + self.prefix = prefix + self.id_method = id_method + self.rotation = rotation + self.position = position + + def validate_input(self): + """ + Validate if input for PlotOptions is correct type. + + Raises + ------ + TypeError + TypeError is thrown if one of the attributes is not the correct + type. + + Returns + ------- + 0, if all checks succeed. + + """ + # %% Validate inputs + # Input validation for figs is done in submodules tagplot_$engine.py + if isinstance(self.prefix, str): + pass + else: + raise TypeError("Prefix is not a string.") + + if isinstance(self.id_method, str): + pass + else: + raise TypeError('The chosen id_method is not a string.') + + return 0 diff --git a/publish.py b/plotid/publish.py similarity index 90% rename from publish.py rename to plotid/publish.py index 77486c82c5e465a771d89af3d03138c77337bece..c5c53dd58da59cca6e3adb56149b3ca9e071726b 100644 --- a/publish.py +++ b/plotid/publish.py @@ -15,10 +15,11 @@ import os import shutil import sys import warnings -from save_plot import save_plot +from plotid.save_plot import save_plot -def publish(src_datapath, dst_path, figure, plot_name, data_storage): +def publish(src_datapath, dst_path, figure, plot_name, + data_storage='individual'): """ Save plot, data and measuring script. @@ -37,7 +38,7 @@ def publish(src_datapath, dst_path, figure, plot_name, data_storage): 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. + every plot, respectively. This is the default value. Returns ------- @@ -55,10 +56,6 @@ def publish(src_datapath, dst_path, figure, plot_name, data_storage): raise FileNotFoundError('The specified destination directory ' 'does not exist.') - # Check if handed over figure is not empty. - if not figure: - raise TypeError('No figure was given. ') - # If dst dir already exists ask user if it should be overwritten or not. if os.path.isdir(dst_path): warnings.warn(f'Folder "{dst_dirname}" already exists – ' @@ -87,15 +84,18 @@ def publish(src_datapath, dst_path, figure, plot_name, data_storage): # Does nothing, not implemented yet pass case 'individual': - # Copy data to invisible folder + # Copy all files to destination directory print('Copying data has been started. Depending on the size of ' 'your data this may take a while...') + # Copy data to invisible folder shutil.copytree(src_datapath, dst_path_invisible) # Copy script that calls this function to folder shutil.copy2(sys.argv[0], dst_path_invisible) # Copy plot file to folder - shutil.move(plot_path, dst_path_invisible) + if os.path.isfile(plot_path): + shutil.copy2(plot_path, dst_path_invisible) + os.remove(plot_path) case _: raise ValueError('The data storage method {data_storage} ' 'is not available.') @@ -107,4 +107,3 @@ def publish(src_datapath, dst_path, figure, plot_name, data_storage): print(f'Publish was successful.\nYour plot "{plot_path}",\n' f'your data "{src_datapath}"\nand your script "{sys.argv[0]}"\n' f'were copied to {dst_path}\nin {data_storage} mode.') - return diff --git a/save_plot.py b/plotid/save_plot.py similarity index 100% rename from save_plot.py rename to plotid/save_plot.py diff --git a/tagplot.py b/plotid/tagplot.py similarity index 77% rename from tagplot.py rename to plotid/tagplot.py index ff089604f13caf96282e413c8f748e41d110e3f0..ba660b153fdfbd6fc6b17d02d9a5d156710a67bf 100644 --- a/tagplot.py +++ b/plotid/tagplot.py @@ -11,11 +11,11 @@ Functions: """ import warnings -from plotoptions import PlotOptions -from tagplot_matplotlib import tagplot_matplotlib +from plotid.plotoptions import PlotOptions +from plotid.tagplot_matplotlib import tagplot_matplotlib -def tagplot(figs, engine, prefix='', id_method=1, location='east'): +def tagplot(figs, engine, prefix='', id_method='time', location='east'): """ Tag your figure/plot with an ID. @@ -30,9 +30,10 @@ def tagplot(figs, engine, prefix='', id_method=1, location='east'): Plot engine which should be used to tag the plot. prefix : string Will be added as prefix to the ID. - id_method : int, optional + id_method : string, optional id_method for creating the ID. Create an ID by Unix time is referenced - as 1, create a random ID with id_method=2. The default is 1. + 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'. @@ -48,23 +49,6 @@ def tagplot(figs, engine, prefix='', id_method=1, location='east'): figures were given. The first list contains the tagged figures. The second list contains the corresponding IDs as strings. """ - # %% Validate inputs - if isinstance(prefix, str): - pass - else: - raise TypeError("Prefix is not a string.") - - if isinstance(figs, list): - pass - else: - raise TypeError("Figures are not a list.") - - # TODO: Change id_method key from integer to (more meaningful) string. - try: - id_method = int(id_method) - except ValueError: - raise TypeError('The chosen ID id_method is not an integer.') - if isinstance(location, str): pass else: @@ -99,6 +83,7 @@ def tagplot(figs, engine, prefix='', id_method=1, location='east'): option_container = PlotOptions(figs, prefix, id_method, rotation, position) + option_container.validate_input() match engine: case 'matplotlib' | 'pyplot': diff --git a/tagplot_matplotlib.py b/plotid/tagplot_matplotlib.py similarity index 73% rename from tagplot_matplotlib.py rename to plotid/tagplot_matplotlib.py index a94e2b43d66a56fe7ed526a441f73d7d54ae6611..7230f884b99199ea22511509b787e6bcbf50d0ce 100644 --- a/tagplot_matplotlib.py +++ b/plotid/tagplot_matplotlib.py @@ -3,13 +3,13 @@ Tag your matplotlib plot with an ID. Functions: - TagPlot_matplotlib(figure object, string) -> list + tagplot_matplotlib(figure object, string) -> list """ import matplotlib import matplotlib.pyplot as plt -import create_id -from plotoptions import PlotOptions +import plotid.create_id as create_id +from plotid.plotoptions import PlotOptions def tagplot_matplotlib(plotid_object): @@ -26,7 +26,7 @@ def tagplot_matplotlib(plotid_object): 'of PlotOptions.') # Check if figs is a valid figure or a list of valid figures if isinstance(plotid_object.figs, matplotlib.figure.Figure): - pass + plotid_object.figs = [plotid_object.figs] elif isinstance(plotid_object.figs, list): for figure in plotid_object.figs: if isinstance(figure, matplotlib.figure.Figure): @@ -36,18 +36,18 @@ def tagplot_matplotlib(plotid_object): fontsize = 'small' color = 'grey' - IDs = [] + all_ids_as_list = [] # Loop to create and position the IDs for fig in plotid_object.figs: - ID = create_id.create_id(plotid_object.id_method) - ID = plotid_object.prefix + str(ID) - IDs.append(ID) + figure_id = create_id.create_id(plotid_object.id_method) + figure_id = plotid_object.prefix + str(figure_id) + all_ids_as_list.append(figure_id) plt.figure(fig.number) plt.figtext(x=plotid_object.position[0], y=plotid_object.position[1], - s=ID, ha='left', wrap=True, + s=figure_id, ha='left', wrap=True, rotation=plotid_object.rotation, fontsize=fontsize, color=color) fig.tight_layout() - return [plotid_object.figs, IDs] + return [plotid_object.figs, all_ids_as_list] diff --git a/plotoptions.py b/plotoptions.py deleted file mode 100644 index 52699fb0b3c7e18c807febfca2c17992d8ac98ba..0000000000000000000000000000000000000000 --- a/plotoptions.py +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -"""Contains the PlotOption class.""" - - -class PlotOptions: - """ - Container objects which include all plot options provided by plotid. - - Methods - ------- - __init__figs : figure object - Figure that will be tagged. - prefix : str - Prefix that is placed before the ID. - id_method : int - 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). - """ - - def __init__(self, figs, prefix, id_method, rotation, position): - - self.figs = figs - self.prefix = prefix - self.id_method = id_method - self.rotation = rotation - self.position = position diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000000000000000000000000000000000000..b519f7854682b15071afddae1cb25b862aa48f2b --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["setuptools>=43", "wheel"] +build-backend = "setuptools.build_meta" diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000000000000000000000000000000000000..cf36c1f25ffc3e5e220ee13d3aa140f91f33c470 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,31 @@ +[metadata] +name = plotID +version = 0.1.0 +author = Institut Fluidsystemtechnik within nfdi4ing at TU Darmstadt +author_email = nfdi4ing@fst.tu-darmstadt.de +description = The plotID toolkit supports researchers in tracking and storing relevant data in plots. Plots are labelled with an ID and the corresponding data is stored. +long_description = file: README.md +long_description_content_type = text/markdown +url = https://git.rwth-aachen.de/plotid/plotid_python +project_urls = + Bug Tracker = https://git.rwth-aachen.de/plotid/plotid_python/-/issues +classifiers = + Programming Language :: Python :: 3.10 + License :: OSI Approved :: Apache Software License + Operating System :: OS Independent + Development Status :: 2 - Pre-Alpha + Intended Audience :: Science/Research + Topic :: Scientific/Engineering :: Visualization + +[options] +packages = plotid +python_requires = >=3.10 +install_requires = + matplotlib + numpy + +[options.extras_require] +test = + coverage + + diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/runner_tests.py b/tests/runner_tests.py similarity index 85% rename from runner_tests.py rename to tests/runner_tests.py index cb159c4f6d24d86dddab007a15675160b5bdc835..de973b1e9d01e9c67946cb520ee063c7a59cd660 100644 --- a/runner_tests.py +++ b/tests/runner_tests.py @@ -6,17 +6,21 @@ Includes starting all tests and measuring the code coverage. """ import sys +import os import unittest import coverage +path = os.path.abspath('plotid') +sys.path.append(path) + cov = coverage.Coverage() cov.start() loader = unittest.TestLoader() -tests = loader.discover('tests') +tests = loader.discover('.') testRunner = unittest.runner.TextTestRunner(verbosity=2) - result = testRunner.run(tests) + cov.stop() cov.save() cov.report(show_missing=True) diff --git a/tests/test_create_id.py b/tests/test_create_id.py index 68a8a8d77d9acba6dd1444e1c3a5585e7e62f803..589405785640b5cd8ba74f69b9c8245ddfb92190 100644 --- a/tests/test_create_id.py +++ b/tests/test_create_id.py @@ -1,27 +1,34 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -''' +""" Unittests for CreateID -''' +""" + import unittest -from create_id import create_id +import plotid.create_id as cid class TestCreateID(unittest.TestCase): + """ + Class for all unittests of the create_id module. + """ def test_existence(self): - self.assertIsInstance(create_id(1), str) - self.assertIsInstance(create_id(2), str) + """Test if create_id returns a string.""" + self.assertIsInstance(cid.create_id('time'), str) + self.assertIsInstance(cid.create_id('random'), str) def test_errors(self): + """ Test if Errors are raised when id_method is wrong. """ with self.assertRaises(ValueError): - create_id(3) + cid.create_id(3) with self.assertRaises(ValueError): - create_id('h') + cid.create_id('h') def test_length(self): - self.assertEqual(len(create_id(1)), 10) - self.assertEqual(len(create_id(2)), 8) + """ Test if figure_id has the correct length. """ + self.assertEqual(len(cid.create_id('time')), 10) + self.assertEqual(len(cid.create_id('random')), 8) if __name__ == '__main__': diff --git a/tests/test_publish.py b/tests/test_publish.py index a01e727cad948944ee6e55f196d7c1b9d1b3b280..d09b7419f1793f9bae0a47788ebc06a1b1b60c74 100644 --- a/tests/test_publish.py +++ b/tests/test_publish.py @@ -1,20 +1,19 @@ # -*- coding: utf-8 -*- -''' -Unittests for Publish -''' +""" +Unittests for publish +""" import unittest import os import sys import shutil -import base64 -import matplotlib.pyplot as plt from unittest.mock import patch -from publish import publish +import matplotlib.pyplot as plt +from plotid.publish import publish + SRC_DIR = 'test_src_folder' -IMG_DATA = b'iVBORw0KGgoAAAANSUhEUgAAAUAAAAFAAgMAAACw/k05AAAACXBIWXMAAA7DAAAOwwHHb6hkAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAAAxQTFRFAAAAHBwcVFRU////6irJIAAAAIVJREFUeNrt3TERACAQBLHTgQlMU6GQDkz8MF9kBcTCJmrY2IWtJPMWdoBAIBAIBAKBQCAQCAQCgUAgEAgEAoFAIBAIBAKBQCAQCAQCgUAgEAgEAoFAIBAIBAKBQCAQCAQCgUAgEAgEAoFAIBAIBAKBQCAQCAQCgUAgEAgEAv+A5RMHtesBZRvjTSF8ofkAAAAASUVORK5CYII=' # noqa: E501 PIC_NAME = 'test_picture' DST_DIR = 'test_dst_folder' DST_PARENT_DIR = 'test_parent' @@ -24,27 +23,32 @@ fig = plt.figure() class TestPublish(unittest.TestCase): + """ + Class for all unittests of the publish module. + """ def setUp(self): + """ Generate source and destination directory and test image. """ os.makedirs(SRC_DIR, exist_ok=True) os.makedirs(DST_PARENT_DIR, exist_ok=True) - with open(PIC_NAME, "wb") as test_pic: - test_pic.write(base64.decodebytes(IMG_DATA)) # Skip test if tests are run from command line. @unittest.skipIf(not os.path.isfile(sys.argv[0]), 'Publish is not called ' 'from a Python script. Therefore, the script cannot be ' 'copied.') def test_publish(self): + """ Test publish and check if an exported picture file exists. """ publish(SRC_DIR, DST_PATH, fig, PIC_NAME, 'individual') assert os.path.isfile(os.path.join(DST_PATH, PIC_NAME + '.png')) def test_src_directory(self): + """ Test if Error is raised when source directory does not exist.""" with self.assertRaises(FileNotFoundError): publish('not_existing_folder', DST_PATH, fig, PIC_NAME, 'individual') def test_dst_directory(self): + """ Test if Error is raised when destination dir does not exist.""" with self.assertRaises(FileNotFoundError): publish(SRC_DIR, 'not_existing_folder', fig, PIC_NAME, 'individual') @@ -54,6 +58,10 @@ class TestPublish(unittest.TestCase): 'from a Python script. Therefore, the script cannot be ' 'copied.') def test_dst_already_exists_yes(self): + """ + Test if publish succeeds if the user wants to overwrite an existing + destination directory. + """ os.mkdir(DST_PATH) # Mock user input as 'yes' with patch('builtins.input', return_value='yes'): @@ -64,6 +72,10 @@ class TestPublish(unittest.TestCase): 'from a Python script. Therefore, the script cannot be ' 'copied.') def test_dst_already_exists_no(self): + """ + Test if publish exits with error if the user does not want to overwrite + an existing destination directory by user input 'no'. + """ os.mkdir(DST_PATH) # Mock user input as 'no' with patch('builtins.input', return_value='no'): @@ -75,6 +87,10 @@ class TestPublish(unittest.TestCase): 'from a Python script. Therefore, the script cannot be ' 'copied.') def test_dst_already_exists_empty(self): + """ + Test if publish exits with error if the user does not want to overwrite + an existing destination directory by missing user input. + """ os.mkdir(DST_PATH) # Mock user input as empty (no should be default). with patch('builtins.input', return_value=''): @@ -86,21 +102,24 @@ class TestPublish(unittest.TestCase): 'from a Python script. Therefore, the script cannot be ' 'copied.') def test_dst__invisible_already_exists(self): + """ + Test if publish succeeds when there is already an invisible + directory from a previous run (delete the folder and proceed). + """ os.mkdir(INVISIBLE_PATH) publish(SRC_DIR, DST_PATH, fig, PIC_NAME, 'individual') - def test_picture(self): - with self.assertRaises(TypeError): - publish(SRC_DIR, DST_PATH, 'fig', PIC_NAME, 'individual') - def test_data_storage(self): + """ + Test if Error is raised when unsupported storage method was chosen. + """ with self.assertRaises(ValueError): publish(SRC_DIR, DST_PATH, fig, PIC_NAME, 'none_existing_method') def tearDown(self): + """ Delete all files created in setUp. """ shutil.rmtree(SRC_DIR) shutil.rmtree(DST_PARENT_DIR) - os.remove(PIC_NAME) if __name__ == '__main__': diff --git a/tests/test_save_plot.py b/tests/test_save_plot.py index 767875110242c2613087b6ba4f087ac74dd9fbf7..0f28ae73c00ffffc2deb9d178ea277b2a8730667 100644 --- a/tests/test_save_plot.py +++ b/tests/test_save_plot.py @@ -1,25 +1,30 @@ # -*- coding: utf-8 -*- -''' +""" Unittests for save_plot -''' +""" import os import unittest import matplotlib.pyplot as plt -from save_plot import save_plot +from plotid.save_plot import save_plot -figure = plt.figure() -plot_name = 'PLOT_NAME' +FIGURE = plt.figure() +PLOT_NAME = 'PLOT_NAME' -class TestSave_Plot(unittest.TestCase): +class TestSavePlot(unittest.TestCase): + """ + Class for all unittests of the save_plot module. + """ def test_save_plot(self): - save_plot(figure, plot_name, extension='jpg') - os.remove(plot_name + '.jpg') + """ Test if save_plot succeeds with valid arguments. """ + save_plot(FIGURE, PLOT_NAME, extension='jpg') + os.remove(PLOT_NAME + '.jpg') def test_wrong_fig_type(self): + """ Test if Error is raised when not a figure object is given. """ with self.assertRaises(TypeError): save_plot('figure', 'PLOT_NAME', extension='jpg') diff --git a/tests/test_tagplot.py b/tests/test_tagplot.py index 25c1ee2aa59bef2305bba0227938e8f8bb743ecd..cb71137911a71cccc28e348985a6f8973273f664 100644 --- a/tests/test_tagplot.py +++ b/tests/test_tagplot.py @@ -1,72 +1,63 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -''' +""" Unittests for tagplot -''' +""" + import unittest -import numpy as np import matplotlib.pyplot as plt from tagplot import tagplot - -# %% Create data -x = np.linspace(0, 10, 100) -y = np.random.rand(100) + 2 -y_2 = np.sin(x) + 2 - -# %% Create figure -color1 = 'black' -color2 = 'yellow' - -# 1.Figure -fig1 = plt.figure() -plt.plot(x, y, color=color1) -plt.plot(x, y_2, color=color2) - -# 2.Figure -fig2 = plt.figure() -plt.plot(x, y, color=color2) -plt.plot(x, y_2, color=color1) - -fig = [fig1, fig2] - # Constants for tests -ProjectID = "MR01" -plot_engine = "matplotlib" -method = 1 +FIG1 = plt.figure() +FIG2 = plt.figure() +FIGS_AS_LIST = [FIG1, FIG2] +PROJECT_ID = "MR01" +PLOT_ENGINE = "matplotlib" +METHOD = 'time' -class Test_tagplot(unittest.TestCase): - def test_figures(self): - with self.assertRaises(TypeError): - tagplot('fig', ProjectID, plot_engine, method) - with self.assertRaises(TypeError): - tagplot(fig1, plot_engine, prefix=ProjectID) +class TestTagplot(unittest.TestCase): + """ + Class for all unittests of the tagplot module. + """ def test_prefix(self): + """ Test if Error is raised if prefix is not a string. """ with self.assertRaises(TypeError): - tagplot(fig, plot_engine, 3, method) + tagplot(FIGS_AS_LIST, PLOT_ENGINE, 3, METHOD, location='southeast') def test_plotengine(self): + """ + Test if Errors are raised if the provided plot engine is not supported. + """ with self.assertRaises(ValueError): - tagplot(fig, 1, ProjectID, method) + tagplot(FIGS_AS_LIST, 1, PROJECT_ID, METHOD, location='north') with self.assertRaises(ValueError): - tagplot(fig, 'xyz', ProjectID, method) + tagplot(FIGS_AS_LIST, 'xyz', PROJECT_ID, METHOD, location='south') def test_idmethod(self): + """ + Test if Errors are raised if the id_method is not an string. + """ with self.assertRaises(TypeError): - tagplot(fig, plot_engine, ProjectID, method='(0,1)') + tagplot(FIGS_AS_LIST, PLOT_ENGINE, PROJECT_ID, id_method=(0, 1), + location='west') with self.assertRaises(TypeError): - tagplot(fig, plot_engine, ProjectID, method='h') + tagplot(FIGS_AS_LIST, PLOT_ENGINE, PROJECT_ID, id_method=1) with self.assertRaises(TypeError): - tagplot(fig, plot_engine, ProjectID, method='[0,1]') + tagplot(FIGS_AS_LIST, PLOT_ENGINE, PROJECT_ID, 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(fig, plot_engine, ProjectID, method, location=1) + tagplot(FIGS_AS_LIST, PLOT_ENGINE, PROJECT_ID, METHOD, location=1) with self.assertWarns(Warning): - tagplot(fig, plot_engine, ProjectID, method, location='up') + tagplot(FIGS_AS_LIST, PLOT_ENGINE, PROJECT_ID, METHOD, + location='up') if __name__ == '__main__': diff --git a/tests/test_tagplot_matplotlib.py b/tests/test_tagplot_matplotlib.py index a73c95591591eeae6f4cf2ea3edfb15d5f725645..8f81b6f742b3c6f0b8d176341a9e643afec44f8e 100644 --- a/tests/test_tagplot_matplotlib.py +++ b/tests/test_tagplot_matplotlib.py @@ -5,54 +5,55 @@ Unittests for TagPlot_matplotlib """ import unittest -from tagplot_matplotlib import tagplot_matplotlib -import numpy as np import matplotlib.pyplot as plt from matplotlib.figure import Figure -from tagplot import PlotOptions +from plotid.tagplot_matplotlib import tagplot_matplotlib +from plotid.plotoptions import PlotOptions -# %% Create data -x = np.linspace(0, 10, 100) -y = np.random.rand(100) + 2 -y_2 = np.sin(x) + 2 -# %% Create figure -color1 = 'black' -color2 = 'yellow' - -# 1.Figure -fig1 = plt.figure() -plt.plot(x, y, color=color1) -plt.plot(x, y_2, color=color2) - -# 2.Figure -fig2 = plt.figure() -plt.plot(x, y, color=color2) -plt.plot(x, y_2, color=color1) - -fig = [fig1, fig2] +FIG1 = plt.figure() +FIG2 = plt.figure() +FIGS_AS_LIST = [FIG1, FIG2] # Constants for tests -ProjectID = "MR01" -method = 1 -rotation = 90 -position = (0.975, 0.35) +PROJECT_ID = "MR01" +METHOD = 'time' +ROTATION = 90 +POSITION = (0.975, 0.35) -class Test_tagplot_matplotlib(unittest.TestCase): +class TestTagplotMatplotlib(unittest.TestCase): + """ + Class for all unittests of the tagplot_matplotlib module. + """ def test_mplfigures(self): - options = PlotOptions(fig, ProjectID, method, rotation, position) - [figs, ID] = tagplot_matplotlib(options) + """ Test of returned objects. Check if they are matplotlib figures. """ + options = PlotOptions(FIGS_AS_LIST, PROJECT_ID, METHOD, ROTATION, + POSITION) + [figs, _] = tagplot_matplotlib(options) self.assertIsInstance(figs[0], Figure) self.assertIsInstance(figs[1], Figure) + def test_single_mplfigure(self): + """ + 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) + [figs, _] = tagplot_matplotlib(options) + self.assertIsInstance(figs[0], Figure) + def test_mplerror(self): - options = PlotOptions(3, ProjectID, method, rotation, position) + """ Test if Error is raised if wrong type of figures is given. """ + options = PlotOptions(3, PROJECT_ID, METHOD, ROTATION, POSITION) with self.assertRaises(TypeError): tagplot_matplotlib(options) def test_mpl_plotoptions(self): + """ + Test if Error is raised if not an instance of PlotOptions is passed. + """ with self.assertRaises(TypeError): tagplot_matplotlib('wrong_object')