Skip to content

Commit

Permalink
πŸš§πŸš€πŸš‡β™»οΈ Shape up for release (#40)
Browse files Browse the repository at this point in the history
πŸš€ Prepare pyglotaran-extras for a release on PyPI πŸš€

* 🧹🩹 Only include pyglotaran_extras folder itself to setup package lookup

* πŸš‡ Added integration tests

* ✨ Added basic deprecation functionality

Slightly adjusted copy paste from pyglotaran
https://github.com/glotaran/pyglotaran/blob/d1e36a93d2c75ec1a93d4eacea5eba2d49b6f130/glotaran/deprecation/deprecation_utils.py#L1

* ♻️ Renamed 'boilerplate' -> 'setup_case_study' and export load_data from io

* β™»οΈπŸ‘Œ Reexport overview plot functions from plotting package

* πŸš‡πŸ‘Œ Use new 'install_extras: false' option in itegration test workflow

* ♻️ Renamed 'plotting.data' -> 'plotting.plot_data'

* πŸ‘Œ Improved typing

* πŸ‘Œ Added 'figsize' argument to all functions which create figues

And adjusted res arg for 'plot_simple_overview' to be DatasetConvertible, to be consistent with 'plot_overview'

* 🧹 Added cylcer arg to plot functions so users have more control over style

This should also prevent style resetting

* 🧹 Changed package version to '0.5.0rc1'

Ref.: https://www.python.org/dev/peps/pep-0440/#pre-releases

* πŸ—‘οΈ First step deprecation of plot functions only returning the Figure

* πŸ‘Œ Export commonly used functions from package top level

* β¬†οΈπŸ”§ Update pre-commit config
  • Loading branch information
s-weigand authored Oct 24, 2021
1 parent 2964f2a commit 90daa5b
Show file tree
Hide file tree
Showing 28 changed files with 903 additions and 242 deletions.
48 changes: 48 additions & 0 deletions .github/workflows/integration-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
name: "Run Examples"

on:
push:
pull_request:
workflow_dispatch:

jobs:
run-examples:
name: "Run Example: "
runs-on: ubuntu-latest
strategy:
matrix:
example_name:
[
quick-start,
fluorescence,
transient-absorption,
transient-absorption-two-datasets,
spectral-constraints,
spectral-guidance,
two-datasets,
sim-3d-disp,
sim-3d-nodisp,
sim-3d-weight,
sim-6d-disp,
]
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.8
uses: actions/setup-python@v2
with:
python-version: 3.8
- name: Install pyglotaran-extras
run: |
pip install wheel
pip install .
- name: ${{ matrix.example_name }}
id: example-run
uses: glotaran/pyglotaran-examples@main
with:
example_name: ${{ matrix.example_name }}
install_extras: false
- name: Upload Example Plots Artifact
uses: actions/upload-artifact@v2
with:
name: example-plots
path: ${{ steps.example-run.outputs.plots-path }}
15 changes: 8 additions & 7 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ repos:
- id: absolufy-imports

- repo: https://github.com/asottile/pyupgrade
rev: v2.26.0
rev: v2.29.0
hooks:
- id: pyupgrade
args: [--py38-plus]
Expand All @@ -36,13 +36,13 @@ repos:
minimum_pre_commit_version: 2.9.0

- repo: https://github.com/asottile/yesqa
rev: v1.2.3
rev: v1.3.0
hooks:
- id: yesqa
additional_dependencies: [flake8-docstrings]

- repo: https://github.com/asottile/setup-cfg-fmt
rev: v1.17.0
rev: v1.18.0
hooks:
- id: setup-cfg-fmt

Expand Down Expand Up @@ -76,7 +76,7 @@ repos:
# Linters

- repo: https://github.com/pre-commit/mirrors-mypy
rev: v0.910
rev: v0.910-1
hooks:
- id: mypy
exclude: ^docs
Expand All @@ -90,7 +90,7 @@ repos:
# pass_filenames: false

- repo: https://github.com/PyCQA/flake8
rev: 3.9.2
rev: 4.0.1
hooks:
- id: flake8
args:
Expand All @@ -111,5 +111,6 @@ repos:
rev: v2.1.0
hooks:
- id: codespell
files: ".py|.rst"
args: [-L pyglotaran]
types: [file]
types_or: [python, pyi, markdown, rst, jupyter]
args: ["--ignore-words-list=doas"]
20 changes: 19 additions & 1 deletion pyglotaran_extras/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,19 @@
__version__ = "0.3.3"
from pyglotaran_extras.io.load_data import load_data
from pyglotaran_extras.io.setup_case_study import setup_case_study
from pyglotaran_extras.plotting.plot_data import plot_data_overview
from pyglotaran_extras.plotting.plot_overview import plot_overview
from pyglotaran_extras.plotting.plot_overview import plot_simple_overview
from pyglotaran_extras.plotting.plot_traces import plot_fitted_traces
from pyglotaran_extras.plotting.plot_traces import select_plot_wavelengths

__all__ = [
"load_data",
"setup_case_study",
"plot_data_overview",
"plot_overview",
"plot_simple_overview",
"plot_fitted_traces",
"select_plot_wavelengths",
]

__version__ = "0.5.0rc1"
4 changes: 4 additions & 0 deletions pyglotaran_extras/deprecation/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
"""Module containing deprecation functionality."""
from pyglotaran_extras.deprecation.deprecation_utils import warn_deprecated

__all__ = ["warn_deprecated"]
152 changes: 152 additions & 0 deletions pyglotaran_extras/deprecation/deprecation_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
from __future__ import annotations

from importlib.metadata import distribution
from warnings import warn

FIG_ONLY_WARNING = (
"In the future plot functions which create figures, will return a tuple "
"of Figure AND the Axes. Please set ``figure_only=False`` and adjust your code.\n"
"This usage will be an error in version: 0.7.0."
)


class OverDueDeprecation(Exception):
"""Error thrown when a deprecation should have been removed.
See Also
--------
deprecate
warn_deprecated
deprecate_module_attribute
deprecate_submodule
deprecate_dict_entry
"""


class PyglotaranExtrasApiDeprecationWarning(UserWarning):
"""Warning to give users about API changes.
See Also
--------
warn_deprecated
"""


def pyglotaran_extras_version() -> str:
"""Version of the distribution.
This is basically the same as ``pyglotaran_extras.__version__`` but independent
from pyglotaran_extras.
This way all of the deprecation functionality can be used even in
``pyglotaran_extras.__init__.py`` without moving the import below the definition of
``__version__`` or causeing a circular import issue.
Returns
-------
str
The version string.
"""
return distribution("pyglotaran-extras").version


def parse_version(version_str: str) -> tuple[int, int, int]:
"""Parse version string to tuple of three ints for comparison.
Parameters
----------
version_str : str
Fully qualified version string of the form 'major.minor.patch'.
Returns
-------
tuple[int, int, int]
Version as tuple.
Raises
------
ValueError
If ``version_str`` has less that three elements separated by ``.``.
ValueError
If ``version_str`` 's first three elements can not be casted to int.
"""
error_message = (
"version_str needs to be a fully qualified version consisting of "
f"int parts (e.g. '0.0.1'), got {version_str!r}"
)
split_version = version_str.partition("-")[0].split(".")
if len(split_version) < 3:
raise ValueError(error_message)
try:
return tuple(
map(int, (*split_version[:2], split_version[2].partition("rc")[0]))
) # type:ignore[return-value]
except ValueError:
raise ValueError(error_message)


def check_overdue(deprecated_qual_name_usage: str, to_be_removed_in_version: str) -> None:
"""Check if a deprecation is overdue for removal.
Parameters
----------
deprecated_qual_name_usage : str
Old usage with fully qualified name e.g.:
``'glotaran.read_model_from_yaml(model_yml_str)'``
to_be_removed_in_version : str
Version the support for this usage will be removed.
Raises
------
OverDueDeprecation
If the current version is greater or equal to ``to_be_removed_in_version``.
"""
if (
parse_version(pyglotaran_extras_version()) >= parse_version(to_be_removed_in_version)
and "dev" not in pyglotaran_extras_version()
):
raise OverDueDeprecation(
f"Support for {deprecated_qual_name_usage.partition('(')[0]!r} was "
f"supposed to be dropped in version: {to_be_removed_in_version!r}.\n"
f"Current version is: {pyglotaran_extras_version()!r}"
)


def warn_deprecated(
*,
deprecated_qual_name_usage: str,
new_qual_name_usage: str,
to_be_removed_in_version: str,
stacklevel: int = 2,
) -> None:
"""Raise deprecation warning with change information.
The change information are old / new usage information and end of support version.
Parameters
----------
deprecated_qual_name_usage : str
Old usage with fully qualified name e.g.:
``'glotaran.read_model_from_yaml(model_yml_str)'``
new_qual_name_usage : str
New usage as fully qualified name e.g.:
``'glotaran.io.load_model(model_yml_str, format_name="yml_str")'``
to_be_removed_in_version : str
Version the support for this usage will be removed.
stacklevel: int
Stack at which the warning should be shown as raise. Default: 2
Raises
------
OverDueDeprecation
If the current version is greater or equal to ``to_be_removed_in_version``.
"""
check_overdue(deprecated_qual_name_usage, to_be_removed_in_version)
warn(
PyglotaranExtrasApiDeprecationWarning(
f"Usage of {deprecated_qual_name_usage!r} was deprecated, "
f"use {new_qual_name_usage!r} instead.\n"
f"This usage will be an error in version: {to_be_removed_in_version!r}."
),
stacklevel=stacklevel,
)
5 changes: 4 additions & 1 deletion pyglotaran_extras/io/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
""" pyglotaran-extras io package """
from pyglotaran_extras.io.boilerplate import setup_case_study
from pyglotaran_extras.io.load_data import load_data
from pyglotaran_extras.io.setup_case_study import setup_case_study

__all__ = ["setup_case_study", "load_data"]
93 changes: 12 additions & 81 deletions pyglotaran_extras/io/boilerplate.py
Original file line number Diff line number Diff line change
@@ -1,81 +1,12 @@
from __future__ import annotations

import inspect
from os import PathLike
from pathlib import Path


def setup_case_study(
output_folder_name: str = "pyglotaran_results",
results_folder_root: None | str | PathLike[str] = None,
) -> tuple[Path, Path]:
"""Convenience function to to quickly get folders for a case study.
This is an execution environment independent (works in python script files
and notebooks, independent of where the python runtime was called from)
way to get the folder the analysis code resides in and also creates the
``results_folder`` in case it didn't exist before.
Parameters
----------
output_folder_name : str
Name of the base folder for the results., by default "pyglotaran_results"
results_folder_root : None
Parent folder the ``output_folder_name`` should be put in
, by default None which results in the users Home folder being used
Returns
-------
tuple[Path, Path]
results_folder, script_folder:
results_folder:
Folder to be used to save results in of the pattern
(``results_folder_root`` / ``output_folder_name`` / ``analysis_folder.parent``).
analysis_folder:
Folder the script or Notebook resides in.
"""
analysis_folder = get_script_dir(nesting=1)
print(f"Setting up case study for folder: {analysis_folder}")
if results_folder_root is None:
results_folder_root = Path.home() / output_folder_name
else:
results_folder_root = Path(str(results_folder_root))
script_folder_rel = analysis_folder.relative_to(analysis_folder.parent)
results_folder = (results_folder_root / script_folder_rel).resolve()
results_folder.mkdir(parents=True, exist_ok=True)
print(f"Results will be saved in: {results_folder}")
return results_folder, analysis_folder.resolve()


def get_script_dir(*, nesting: int = 0) -> Path:
"""Gets parent folder a script is executed in.
This is a helper function for cross compatibility with jupyter notebooks.
In notebooks the global ``__file__`` variable isn't set, thus we need different
means to get the folder a script is defined in, which doesn't change with the
current working director the ``python interpreter`` was called from.
Parameters
----------
nesting : int
Number to go up in the call stack to get to the initially calling function.
This is only needed for library code and not for user code.
, by default 0 (direct call)
Returns
-------
Path
Path to the folder the script was resides in.
See Also
--------
setup_case_study
"""
calling_frame = inspect.stack()[nesting + 1].frame
file_var = calling_frame.f_globals.get("__file__", ".")
file_path = Path(file_var).resolve()
if file_var == ".": # pragma: no cover
return file_path
else:
return file_path.parent
"""Deprecated module."""
from pyglotaran_extras.deprecation import warn_deprecated
from pyglotaran_extras.io.setup_case_study import setup_case_study

__all__ = ["setup_case_study"]

warn_deprecated(
deprecated_qual_name_usage="pyglotaran_extras.io.boilerplate",
new_qual_name_usage="pyglotaran_extras.io",
to_be_removed_in_version="0.7.0",
stacklevel=3,
)
Loading

0 comments on commit 90daa5b

Please sign in to comment.