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/.gitignore b/.gitignore index 11614af2870733183efe883810764d8708bddf8f..e01aca0bfc95772299812615a69de3808b12ba9a 100644 --- a/.gitignore +++ b/.gitignore @@ -90,7 +90,7 @@ celerybeat-schedule # Environments .env .venv -env/ +env*/ venv/ ENV/ env.bak/ diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000000000000000000000000000000000000..27b46ba817eb967e1f15d6cf89969048cf57f75e --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,84 @@ +# This file is a template, and might need editing before it works on your project. +# To contribute improvements to CI/CD templates, please follow the Development guide at: +# https://docs.gitlab.com/ee/development/cicd/templates.html +# This specific template is located at: +# https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Python.gitlab-ci.yml + +# Official language image. Look for the different tagged releases at: +# https://hub.docker.com/r/library/python/tags/ +image: python:latest + +stages: + - linting + - testing + +# Change pip's cache directory to be inside the project directory since we can +# only cache local items. +variables: + PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip" + +# Pip's cache doesn't store the python packages +# https://pip.pypa.io/en/stable/reference/pip_install/#caching +# +# If you want to also cache the installed packages, you have to install +# them in a virtualenv and cache it as well. +cache: + paths: +# - .cache/pip # +# - venv/ # + + + + +before_script: + - python --version # For debugging + - pip install -r requirements.txt # install dependencies from file + - pip install flake8 + - pip install pylint +# - pip install virtualenv +# - virtualenv venv +# - source venv/bin/activate + +PEP8: + stage: linting + # allow_failure: true + script: flake8 --count . # PEP8 linting + + +Pylint: + stage: linting + # 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 + tags: + - docker + script: +# - python -m unittest discover -s ./tests/ -p "test*" # deprecated unittest command + - python tests/runner_tests.py +# - pip install tox flake8 # you can also use tox +# - tox -e py36,flake8 + +# Commenting out all other stages and jobs +#run: +# script: +# - python setup.py bdist_wheel +# # an alternative approach is to install and run: +# - pip install dist/* +# # run the command here +# artifacts: +# paths: +# - dist/*.whl + +#pages: +# script: +# - pip install sphinx sphinx-rtd-theme +# - cd doc +# - make html +# - mv build/html/ ../public/ +# artifacts: +# paths: +# - public +# rules: +# - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH diff --git a/HDF5_externalLink.py b/HDF5_externalLink.py deleted file mode 100644 index 40fa5fa3fbdcb47dfd9578e5e4f3145cc6ae6285..0000000000000000000000000000000000000000 --- a/HDF5_externalLink.py +++ /dev/null @@ -1,9 +0,0 @@ -import h5py - -#Shows how to create an externalLink (symbolic) to a hdf5 File -#Documentation available at: https://docs.h5py.org/en/stable/high/group.html#external-links -myfile = h5py.File('./example.h5','w') -myfile['ext link'] = h5py.ExternalLink("testdata_2.h5", "/") - -myfile.close() - diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..69a3786913abc56d373be8f9b431b79aa3e33639 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2022 PlotID + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md index 58de42ddc60a1be7d4c653ab83c188a1a4a3d723..8d481de3ce3336fb936684ef9f5748d771bc4c36 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,107 @@ -# plot_ID_python +# PlotID for Python -This is the python PlotID project \ No newline at end of file +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. + +**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/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/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/plotid/filecompare.py b/plotid/filecompare.py new file mode 100644 index 0000000000000000000000000000000000000000..2ccc81a36933b07cbcf6897a8112843e7f7c02a2 --- /dev/null +++ b/plotid/filecompare.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- +"""Compare if data already exists in published folder to avoid redundancy.""" + +# Test des Moduls filecmp — File and Directory Comparisons + +import filecmp + +a = filecmp.cmp("2.py", "test_git.py") +print(a) diff --git a/plotid/hdf5_external_link.py b/plotid/hdf5_external_link.py new file mode 100644 index 0000000000000000000000000000000000000000..09b96fa9ccd5b9fa84faa19826410a29ac413701 --- /dev/null +++ b/plotid/hdf5_external_link.py @@ -0,0 +1,11 @@ +"""Handling of hdf5 data files.""" + +import h5py + +# Shows how to create an externalLink (symbolic) to a hdf5 File +# Documentation available at: +# https://docs.h5py.org/en/stable/high/group.html#external-links +myfile = h5py.File('./example.h5', 'w') +myfile['ext link'] = h5py.ExternalLink("testdata_2.h5", "/") + +myfile.close() 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/plotid/publish.py b/plotid/publish.py new file mode 100644 index 0000000000000000000000000000000000000000..c5c53dd58da59cca6e3adb56149b3ca9e071726b --- /dev/null +++ b/plotid/publish.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Publish a tagged plot along the corresponding research data. + +Export the tagged figure to a directory that also contains the raw data where +the plot is based on. Additionally, the script that produced the plot will be +copied to the destination directory. + +Functions: + publish(path-like, path-like, figure, string, string) -> None +""" + +import os +import shutil +import sys +import warnings +from plotid.save_plot import save_plot + + +def publish(src_datapath, dst_path, figure, plot_name, + data_storage='individual'): + """ + Save plot, data and measuring script. + + Parameters + ---------- + src_datapath : string + Path to data that should be published. + dst_path : string + Path to the destination directory. + figure : figure object + Figure that was tagged and now should be saved as picture. + plot_name: string + Name for the exported plot. + data_storage: string + Method how the data should be stored. Available options: + centralized: The raw data will copied only once. All other plots + will reference this data via sym link. + individual: The complete raw data will be copied to a folder for + every plot, respectively. This is the default value. + + Returns + ------- + None. + + """ + # Check if source directory exists + if not os.path.exists(src_datapath): + raise FileNotFoundError('The specified source directory ' + 'does not exist.') + + # Check if destination directory is allowed path + dst_path_head, dst_dirname = os.path.split(dst_path) + if not os.path.exists(dst_path_head): + raise FileNotFoundError('The specified destination directory ' + 'does not exist.') + + # If dst dir already exists ask user if it should be overwritten or not. + if os.path.isdir(dst_path): + warnings.warn(f'Folder "{dst_dirname}" already exists – ' + '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('PlotID has finished without an export.\n' + 'Rerun TagPlot if you need a new ID or ' + 'consider overwriting.') + + # Export plot figure to picture. + plot_path = save_plot(figure, plot_name) + + # Create invisible folder + dst_path_invisible = os.path.join(dst_path_head, '.' + dst_dirname) + # If invisible Folder exists, delete it (publish was not successful before) + if os.path.isdir(dst_path_invisible): + shutil.rmtree(dst_path_invisible) + + match data_storage: + case 'centralized': + # Does nothing, not implemented yet + pass + case 'individual': + # 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 + 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.') + + print('Copying data finished. Starting to clean up.') + + # If export was successful, make the directory visible + os.rename(dst_path_invisible, dst_path) + print(f'Publish was successful.\nYour plot "{plot_path}",\n' + f'your data "{src_datapath}"\nand your script "{sys.argv[0]}"\n' + f'were copied to {dst_path}\nin {data_storage} mode.') diff --git a/plotid/save_plot.py b/plotid/save_plot.py new file mode 100644 index 0000000000000000000000000000000000000000..0292e6155608e2d7b76b6b32c1915f7957c1fa3d --- /dev/null +++ b/plotid/save_plot.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Export a plot figure to a picture file. + +Functions: + save_plot(figure, string) -> path-like +""" + +import matplotlib +import matplotlib.pyplot as plt + + +def save_plot(figure, plot_name, extension='png'): + """ + Export plot. + + Parameters + ---------- + figure : figure object + Figure that was tagged and now should be saved as picture. + plot_name: Name of the file where the plot will be saved to. + extension: string + File extension for the plot export. + + Returns + ------- + plot_path: Name of the created picture. + + """ + match type(figure): + case matplotlib.figure.Figure: + plt.figure(figure) + plot_path = plot_name + '.' + extension + plt.savefig(plot_path) + case _: + raise TypeError('The given figure is not a valid figure object.') + + return plot_path diff --git a/plotid/tagplot.py b/plotid/tagplot.py new file mode 100644 index 0000000000000000000000000000000000000000..ba660b153fdfbd6fc6b17d02d9a5d156710a67bf --- /dev/null +++ b/plotid/tagplot.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Tag your plot with an ID. + +For publishing the tagged plot along your research data have a look at the +module publish. + +Functions: + TagPlot(figure object, string) -> list +""" + +import warnings +from plotid.plotoptions import PlotOptions +from plotid.tagplot_matplotlib import tagplot_matplotlib + + +def tagplot(figs, engine, prefix='', id_method='time', location='east'): + """ + Tag your figure/plot with an ID. + + After determining the plot engine, TagPlot calls the corresponding + function which tags the plot. + + Parameters + ---------- + figs : list + Figures that should be tagged. + engine : string + Plot engine which should be used to tag the plot. + prefix : string + Will be added as prefix to the ID. + id_method : string, 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 + ------ + RuntimeWarning + DESCRIPTION. + + Returns + ------- + list + The resulting list 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. + """ + if isinstance(location, str): + pass + else: + raise TypeError("Location is not a string.") + + match location: + case 'north': + rotation = 0 + position = (0.35, 0.975) + case 'east': + rotation = 90 + position = (0.975, 0.35) + case 'south': + rotation = 0 + position = (0.35, 0.015) + case 'west': + rotation = 90 + position = (0.025, 0.35) + case 'southeast': + rotation = 0 + 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) + case _: + warnings.warn(f'Location "{location}" is not a defined ' + 'location, TagPlot uses location "east" ' + 'instead.') + rotation = 90 + position = (0.975, 0.35) + + option_container = PlotOptions(figs, prefix, id_method, + rotation, position) + option_container.validate_input() + + match engine: + case 'matplotlib' | 'pyplot': + return tagplot_matplotlib(option_container) + case _: + raise ValueError( + f'The plot engine "{engine}" is not supported.') diff --git a/plotid/tagplot_matplotlib.py b/plotid/tagplot_matplotlib.py new file mode 100644 index 0000000000000000000000000000000000000000..7230f884b99199ea22511509b787e6bcbf50d0ce --- /dev/null +++ b/plotid/tagplot_matplotlib.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- +""" +Tag your matplotlib plot with an ID. + +Functions: + tagplot_matplotlib(figure object, string) -> list +""" + +import matplotlib +import matplotlib.pyplot as plt +import plotid.create_id as create_id +from plotid.plotoptions import PlotOptions + + +def tagplot_matplotlib(plotid_object): + """ + Add IDs to figures with matplotlib. + + The ID is placed visual on the figure window and + as Tag (Property of figure). + TagPlot can tag multiple figures at once + """ + # Check if plotid_object is a valid instance of PlotOptions + if not isinstance(plotid_object, PlotOptions): + raise TypeError('The given options container is not an instance' + 'of PlotOptions.') + # Check if figs is a valid figure or a list of valid figures + if isinstance(plotid_object.figs, matplotlib.figure.Figure): + plotid_object.figs = [plotid_object.figs] + elif isinstance(plotid_object.figs, list): + for figure in plotid_object.figs: + if isinstance(figure, matplotlib.figure.Figure): + pass + else: + raise TypeError('Figure is not a valid matplotlib-figure.') + + fontsize = 'small' + color = 'grey' + all_ids_as_list = [] + + # Loop to create and position the IDs + for fig in plotid_object.figs: + 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=figure_id, ha='left', wrap=True, + rotation=plotid_object.rotation, + fontsize=fontsize, color=color) + fig.tight_layout() + return [plotid_object.figs, all_ids_as_list] 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/requirements.txt b/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..6891c1fbed005067ba464b8b0882c55b2a87425f --- /dev/null +++ b/requirements.txt @@ -0,0 +1,11 @@ +coverage==6.3.2 +cycler==0.11.0 +fonttools==4.32.0 +kiwisolver==1.4.2 +matplotlib==3.5.1 +numpy==1.22.3 +packaging==21.3 +Pillow==9.1.0 +pyparsing==3.0.8 +python-dateutil==2.8.2 +six==1.16.0 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/tests/runner_tests.py b/tests/runner_tests.py new file mode 100644 index 0000000000000000000000000000000000000000..de973b1e9d01e9c67946cb520ee063c7a59cd660 --- /dev/null +++ b/tests/runner_tests.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +""" +Runner for all tests regarding the plotid project. + +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('.') +testRunner = unittest.runner.TextTestRunner(verbosity=2) +result = testRunner.run(tests) + +cov.stop() +cov.save() +cov.report(show_missing=True) + +if result.wasSuccessful(): + covered = cov.report() + assert covered > 90, "Not enough coverage." + sys.exit(0) +else: + sys.exit(1) diff --git a/tests/test_create_id.py b/tests/test_create_id.py new file mode 100644 index 0000000000000000000000000000000000000000..589405785640b5cd8ba74f69b9c8245ddfb92190 --- /dev/null +++ b/tests/test_create_id.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Unittests for CreateID +""" + +import unittest +import plotid.create_id as cid + + +class TestCreateID(unittest.TestCase): + """ + Class for all unittests of the create_id module. + """ + + def test_existence(self): + """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): + cid.create_id(3) + with self.assertRaises(ValueError): + cid.create_id('h') + + def test_length(self): + """ 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__': + unittest.main() diff --git a/tests/test_publish.py b/tests/test_publish.py new file mode 100644 index 0000000000000000000000000000000000000000..d09b7419f1793f9bae0a47788ebc06a1b1b60c74 --- /dev/null +++ b/tests/test_publish.py @@ -0,0 +1,126 @@ +# -*- coding: utf-8 -*- + +""" +Unittests for publish +""" + +import unittest +import os +import sys +import shutil +from unittest.mock import patch +import matplotlib.pyplot as plt +from plotid.publish import publish + + +SRC_DIR = 'test_src_folder' +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() + + +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) + + # 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') + + # 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_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'): + publish(SRC_DIR, DST_PATH, fig, PIC_NAME, 'individual') + + # Skip test if tests are run from command line. + @unittest.skipIf(not os.path.isfile(sys.argv[0]), 'Publish is not called ' + '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'): + with self.assertRaises(RuntimeError): + publish(SRC_DIR, DST_PATH, fig, PIC_NAME, 'individual') + + # Skip test if tests are run from command line. + @unittest.skipIf(not os.path.isfile(sys.argv[0]), 'Publish is not called ' + '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=''): + with self.assertRaises(RuntimeError): + publish(SRC_DIR, DST_PATH, fig, PIC_NAME, 'individual') + + # Skip test if tests are run from command line. + @unittest.skipIf(not os.path.isfile(sys.argv[0]), 'Publish is not called ' + '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_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) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_save_plot.py b/tests/test_save_plot.py new file mode 100644 index 0000000000000000000000000000000000000000..0f28ae73c00ffffc2deb9d178ea277b2a8730667 --- /dev/null +++ b/tests/test_save_plot.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- + +""" +Unittests for save_plot +""" + +import os +import unittest +import matplotlib.pyplot as plt +from plotid.save_plot import save_plot + +FIGURE = plt.figure() +PLOT_NAME = 'PLOT_NAME' + + +class TestSavePlot(unittest.TestCase): + """ + Class for all unittests of the save_plot module. + """ + + def test_save_plot(self): + """ 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') + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_tagplot.py b/tests/test_tagplot.py new file mode 100644 index 0000000000000000000000000000000000000000..cb71137911a71cccc28e348985a6f8973273f664 --- /dev/null +++ b/tests/test_tagplot.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Unittests for tagplot +""" + +import unittest +import matplotlib.pyplot as plt +from tagplot import tagplot + +# Constants for tests +FIG1 = plt.figure() +FIG2 = plt.figure() +FIGS_AS_LIST = [FIG1, FIG2] + +PROJECT_ID = "MR01" +PLOT_ENGINE = "matplotlib" +METHOD = 'time' + + +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(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(FIGS_AS_LIST, 1, PROJECT_ID, METHOD, location='north') + with self.assertRaises(ValueError): + 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(FIGS_AS_LIST, PLOT_ENGINE, PROJECT_ID, id_method=(0, 1), + location='west') + with self.assertRaises(TypeError): + tagplot(FIGS_AS_LIST, PLOT_ENGINE, PROJECT_ID, id_method=1) + with self.assertRaises(TypeError): + 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(FIGS_AS_LIST, PLOT_ENGINE, PROJECT_ID, METHOD, location=1) + with self.assertWarns(Warning): + tagplot(FIGS_AS_LIST, PLOT_ENGINE, PROJECT_ID, METHOD, + location='up') + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_tagplot_matplotlib.py b/tests/test_tagplot_matplotlib.py new file mode 100644 index 0000000000000000000000000000000000000000..8f81b6f742b3c6f0b8d176341a9e643afec44f8e --- /dev/null +++ b/tests/test_tagplot_matplotlib.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Unittests for TagPlot_matplotlib +""" + +import unittest +import matplotlib.pyplot as plt +from matplotlib.figure import Figure +from plotid.tagplot_matplotlib import tagplot_matplotlib +from plotid.plotoptions import PlotOptions + + +FIG1 = plt.figure() +FIG2 = plt.figure() +FIGS_AS_LIST = [FIG1, FIG2] + +# Constants for tests +PROJECT_ID = "MR01" +METHOD = 'time' +ROTATION = 90 +POSITION = (0.975, 0.35) + + +class TestTagplotMatplotlib(unittest.TestCase): + """ + Class for all unittests of the tagplot_matplotlib module. + """ + + def test_mplfigures(self): + """ 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): + """ 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') + + +if __name__ == '__main__': + unittest.main()