From a9f754ceaf99496ec1df58d1a93781a58bd8d432 Mon Sep 17 00:00:00 2001 From: vivaansinghvi07 Date: Fri, 14 Feb 2025 00:41:17 -0500 Subject: [PATCH 01/15] initial commit --- teeplot/teeplot.py | 136 +++++++++++++++++++++++++++++++++++---------- 1 file changed, 107 insertions(+), 29 deletions(-) diff --git a/teeplot/teeplot.py b/teeplot/teeplot.py index a43ec01..f25bdcc 100644 --- a/teeplot/teeplot.py +++ b/teeplot/teeplot.py @@ -1,6 +1,8 @@ from collections import abc, Counter from contextlib import contextmanager import copy +import functools +import inspect import os import pathlib import typing @@ -16,16 +18,15 @@ def _is_running_on_ci() -> bool: - ci_envs = ['CI', 'TRAVIS', 'GITHUB_ACTIONS', 'GITLAB_CI', 'JENKINS_URL'] + ci_envs = ["CI", "TRAVIS", "GITHUB_ACTIONS", "GITLAB_CI", "JENKINS_URL"] return any(env in os.environ for env in ci_envs) + draftmode: bool = False -oncollision: typext.Literal[ - "error", "fix", "ignore", "warn" -] = os.environ.get( +oncollision: typext.Literal["error", "fix", "ignore", "warn"] = os.environ.get( "TEEPLOT_ONCOLLISION", - "warn" if (_is_running_on_ci() or not hasattr(sys, 'ps1')) else "ignore", + "warn" if (_is_running_on_ci() or not hasattr(sys, "ps1")) else "ignore", ).lower() if not oncollision in ("error", "fix", "ignore", "warn"): raise RuntimeError( @@ -51,8 +52,8 @@ def _is_running_on_ci() -> bool: # see https://gecco-2021.sigevo.org/Paper-Submission-Instructions @matplotlib.rc_context( { - 'pdf.fonttype': 42, - 'ps.fonttype': 42, + "pdf.fonttype": 42, + "ps.fonttype": 42, }, ) def tee( @@ -61,7 +62,8 @@ def tee( teeplot_callback: bool = False, teeplot_dpi: int = 300, teeplot_oncollision: typing.Optional[ - typext.Literal["error", "fix", "ignore", "warn"]] = None, + typext.Literal["error", "fix", "ignore", "warn"] + ] = None, teeplot_outattrs: typing.Dict[str, str] = {}, teeplot_outdir: str = "teeplots", teeplot_outinclude: typing.Iterable[str] = tuple(), @@ -69,10 +71,10 @@ def tee( teeplot_postprocess: typing.Union[str, typing.Callable] = "", teeplot_save: typing.Union[typing.Iterable[str], bool] = True, teeplot_show: typing.Optional[bool] = None, - teeplot_subdir: str = '', + teeplot_subdir: str = "", teeplot_transparent: bool = True, teeplot_verbose: bool = True, - **kwargs: typing.Any + **kwargs: typing.Any, ) -> typing.Any: """Executes a plotting function and saves the resulting plot to specified formats using a descriptive filename automatically generated from plotting @@ -183,12 +185,11 @@ def tee( elif isinstance(teeplot_save, str): if not teeplot_save in formats: raise ValueError( - f"only {[*formats]} save formats are supported, " - f"not {teeplot_save}", + f"only {[*formats]} save formats are supported, " f"not {teeplot_save}", ) # remove explicitly disabled outputs blacklist = set(k for k, v in formats.items() if v is False) - exclusions = {teeplot_save} & blacklist + exclusions = {teeplot_save} & blacklist if teeplot_verbose and exclusions: print(f"skipping {exclusions}") teeplot_save = {teeplot_save} - exclusions @@ -201,7 +202,7 @@ def tee( ) # remove explicitly disabled outputs blacklist = set(k for k, v in formats.items() if v is False) - exclusions = set(teeplot_save) & blacklist + exclusions = set(teeplot_save) & blacklist if teeplot_verbose and exclusions: print(f"skipping {exclusions}") teeplot_save = set(teeplot_save) - exclusions @@ -266,29 +267,33 @@ def tee( incl = [*teeplot_outinclude] attr_maker = lambda ext: { **{ - slugify(k) : slugify(str(v)) + slugify(k): slugify(str(v)) for k, v in kwargs.items() if isinstance(v, str) or k in incl }, **{ - 'viz' : slugify(plotter.__name__), - 'ext' : ext, + "viz": slugify(plotter.__name__), + "ext": ext, }, **( {"post": teeplot_postprocess.__name__} if teeplot_postprocess and isinstance(teeplot_postprocess, abc.Callable) - else {"post": slugify(teeplot_postprocess)} - if teeplot_postprocess and not teeplot_postprocess.endswith(";") - else {} + else ( + {"post": slugify(teeplot_postprocess)} + if teeplot_postprocess and not teeplot_postprocess.endswith(";") + else {} + ) ), **teeplot_outattrs, } excl = [*teeplot_outexclude] - out_filenamer = lambda ext: kn.pack({ - k : v - for k, v in attr_maker(ext).items() - if not k.startswith('_') and not k in excl - }) + out_filenamer = lambda ext: kn.pack( + { + k: v + for k, v in attr_maker(ext).items() + if not k.startswith("_") and not k in excl + } + ) out_folder = pathlib.Path(teeplot_outdir, teeplot_subdir) out_folder.mkdir(parents=True, exist_ok=True) @@ -315,7 +320,7 @@ def save_callback(): count = _history[out_path] suffix = f"ext={ext}" assert str(out_path).endswith(suffix) - out_path = str(out_path)[:-len(suffix)] + f"#={count}+" + suffix + out_path = str(out_path)[: -len(suffix)] + f"#={count}+" + suffix elif teeplot_oncollision == "ignore": pass elif teeplot_oncollision == "warn": @@ -333,7 +338,7 @@ def save_callback(): print(out_path) plt.savefig( str(out_path), - bbox_inches='tight', + bbox_inches="tight", transparent=teeplot_transparent, dpi=teeplot_dpi, # see https://matplotlib.org/2.1.1/users/whats_new.html#reproducible-ps-pdf-and-svg-output @@ -347,7 +352,7 @@ def save_callback(): }, ) - if teeplot_show or (teeplot_show is None and hasattr(sys, 'ps1')): + if teeplot_show or (teeplot_show is None and hasattr(sys, "ps1")): plt.show() return teed @@ -359,7 +364,7 @@ def save_callback(): @contextmanager -def teed(*args: list, **kwargs: dict): +def teed(*args, **kwargs): """Context manager interface to `teeplot.tee`. Plot save is dispatched upon exiting the context. Return value is the @@ -377,3 +382,76 @@ def teed(*args: list, **kwargs: dict): yield handle finally: saveit() + + +def validate_teewrap_kwargs(teeplot_kwargs): + params = {k for k in inspect.signature(tee).parameters if k.startswith("teeplot")} + if not all(k in params for k in teeplot_kwargs): + raise ValueError( + "The only keyword arguments passed into the `teewrap` decorator can be teeplot arguments" + ) + if "teeplot_outattrs" in params: + raise ValueError( + "`teeplot_outattrs` cannot be used with `teewrap`. Use `teeplot_outattr_names` instead." + ) + + +def teewrap( + *, + teeplot_outattr_names: typing.Optional[typing.Iterable[str]] = None, + **teeplot_kwargs: object, +): + """Decorator interface to `teeplot.tee` + + Works by returning a decorator that wraps `f` by calling `teeplot.tee` using + `f` and any passed in arguments and keyword arguments. However `teeplot_outattrs` + is not allowed with this function, as it would not make sense to have hardcoded + attributes as a decorator. Instead, we use `teeplot_outattr_names` to give names + of the parameters of `f` (except any variadic arguments, as they are all unnamed) + to use as `teeplot_outattrs` internally. + """ + validate_teewrap_kwargs(teeplot_kwargs) + + def decorator(f: typing.Callable): + @functools.wraps(f) + def inner(*args, **kwargs): + + # build a list of arguments up until anything variadic + arg_dict = {} + try: + for i, (pname, pval) in enumerate( + inspect.signature(f).parameters.items() + ): + if pval.kind == inspect.Parameter.VAR_POSITIONAL: + break + arg_dict[pname] = args[i] + # something must have gone wrong with getting the signature + except ValueError as e: + warnings.warn( + f"teeplot: Something went wrong with parsing parameter names:\n\033[33m{e}\033[0m" + ) + arg_dict = {} # reset dict to avoid weird behavior + + if teeplot_outattr_names is None: + return tee( + f, + *args, + **teeplot_kwargs, + teeplot_outattrs=arg_dict | kwargs, + **kwargs, + ) + return tee( + f, + *args, + **teeplot_kwargs, + teeplot_outattrs={ + k: v + for k, v in (arg_dict | kwargs).items() + if k in teeplot_outattr_names + }, + **kwargs, + ) + + return inner + + return decorator From ef19222884574887a7c2d8ea325d651a3c7690a9 Mon Sep 17 00:00:00 2001 From: vivaansinghvi07 Date: Fri, 14 Feb 2025 16:16:28 -0500 Subject: [PATCH 02/15] use kwargs as default instead of both --- teeplot/__init__.py | 4 ++-- teeplot/teeplot.py | 17 +++++++++-------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/teeplot/__init__.py b/teeplot/__init__.py index 53fa7a2..bba9a13 100644 --- a/teeplot/__init__.py +++ b/teeplot/__init__.py @@ -1,5 +1,5 @@ """Top-level package for teeplot.""" __author__ = """Matthew Andres Moreno""" -__email__ = 'm.more500@gmail.com' -__version__ = '1.2.0' +__email__ = "m.more500@gmail.com" +__version__ = "1.2.0" diff --git a/teeplot/teeplot.py b/teeplot/teeplot.py index f25bdcc..c48b8c6 100644 --- a/teeplot/teeplot.py +++ b/teeplot/teeplot.py @@ -416,6 +416,15 @@ def decorator(f: typing.Callable): @functools.wraps(f) def inner(*args, **kwargs): + if teeplot_outattr_names is None: + return tee( + f, + *args, + **teeplot_kwargs, + teeplot_outattrs=kwargs, + **kwargs, + ) + # build a list of arguments up until anything variadic arg_dict = {} try: @@ -432,14 +441,6 @@ def inner(*args, **kwargs): ) arg_dict = {} # reset dict to avoid weird behavior - if teeplot_outattr_names is None: - return tee( - f, - *args, - **teeplot_kwargs, - teeplot_outattrs=arg_dict | kwargs, - **kwargs, - ) return tee( f, *args, From a623011fc20a7e2fae1b3fb45475fe649c23e6e8 Mon Sep 17 00:00:00 2001 From: vivaansinghvi07 Date: Fri, 14 Feb 2025 16:32:25 -0500 Subject: [PATCH 03/15] add int and float to printable params --- teeplot/teeplot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/teeplot/teeplot.py b/teeplot/teeplot.py index c48b8c6..83bfbe9 100644 --- a/teeplot/teeplot.py +++ b/teeplot/teeplot.py @@ -269,7 +269,7 @@ def tee( **{ slugify(k): slugify(str(v)) for k, v in kwargs.items() - if isinstance(v, str) or k in incl + if isinstance(v, (str, int, float)) or k in incl }, **{ "viz": slugify(plotter.__name__), From 43e9618b457ef03241bf72a233618e33a5f19c3d Mon Sep 17 00:00:00 2001 From: vivaansinghvi07 Date: Fri, 14 Feb 2025 16:32:38 -0500 Subject: [PATCH 04/15] use builtin behavior of teeplot_outinclude --- teeplot/teeplot.py | 29 +++-------------------------- 1 file changed, 3 insertions(+), 26 deletions(-) diff --git a/teeplot/teeplot.py b/teeplot/teeplot.py index 83bfbe9..8cfee1f 100644 --- a/teeplot/teeplot.py +++ b/teeplot/teeplot.py @@ -397,8 +397,6 @@ def validate_teewrap_kwargs(teeplot_kwargs): def teewrap( - *, - teeplot_outattr_names: typing.Optional[typing.Iterable[str]] = None, **teeplot_kwargs: object, ): """Decorator interface to `teeplot.tee` @@ -406,9 +404,8 @@ def teewrap( Works by returning a decorator that wraps `f` by calling `teeplot.tee` using `f` and any passed in arguments and keyword arguments. However `teeplot_outattrs` is not allowed with this function, as it would not make sense to have hardcoded - attributes as a decorator. Instead, we use `teeplot_outattr_names` to give names - of the parameters of `f` (except any variadic arguments, as they are all unnamed) - to use as `teeplot_outattrs` internally. + attributes as a decorator. Instead, see `teeplot_outinclude` in `teeplot.tee`. + `teeplot.teewrap` defaults to including all keyword arguments. """ validate_teewrap_kwargs(teeplot_kwargs) @@ -416,6 +413,7 @@ def decorator(f: typing.Callable): @functools.wraps(f) def inner(*args, **kwargs): + teeplot_outattr_names = teeplot_kwargs.get("teeplot_outinclude") if teeplot_outattr_names is None: return tee( f, @@ -425,31 +423,10 @@ def inner(*args, **kwargs): **kwargs, ) - # build a list of arguments up until anything variadic - arg_dict = {} - try: - for i, (pname, pval) in enumerate( - inspect.signature(f).parameters.items() - ): - if pval.kind == inspect.Parameter.VAR_POSITIONAL: - break - arg_dict[pname] = args[i] - # something must have gone wrong with getting the signature - except ValueError as e: - warnings.warn( - f"teeplot: Something went wrong with parsing parameter names:\n\033[33m{e}\033[0m" - ) - arg_dict = {} # reset dict to avoid weird behavior - return tee( f, *args, **teeplot_kwargs, - teeplot_outattrs={ - k: v - for k, v in (arg_dict | kwargs).items() - if k in teeplot_outattr_names - }, **kwargs, ) From c866a0fb6d45641be7726d90fa90f6f10b27c6d0 Mon Sep 17 00:00:00 2001 From: vivaansinghvi07 Date: Sat, 15 Feb 2025 16:01:17 -0500 Subject: [PATCH 05/15] use bool and str only, not numerical arguments --- teeplot/teeplot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/teeplot/teeplot.py b/teeplot/teeplot.py index 8cfee1f..8388830 100644 --- a/teeplot/teeplot.py +++ b/teeplot/teeplot.py @@ -269,7 +269,7 @@ def tee( **{ slugify(k): slugify(str(v)) for k, v in kwargs.items() - if isinstance(v, (str, int, float)) or k in incl + if isinstance(v, (str, bool)) or k in incl }, **{ "viz": slugify(plotter.__name__), From 803011e6383832165dc246bb9736f75dcb38078f Mon Sep 17 00:00:00 2001 From: vivaansinghvi07 Date: Sat, 15 Feb 2025 16:05:51 -0500 Subject: [PATCH 06/15] reverted to old code for anything other than teewrap --- teeplot/teeplot.py | 81 ++++++++++++++++++---------------------------- 1 file changed, 32 insertions(+), 49 deletions(-) diff --git a/teeplot/teeplot.py b/teeplot/teeplot.py index 8388830..ae18ea8 100644 --- a/teeplot/teeplot.py +++ b/teeplot/teeplot.py @@ -2,7 +2,6 @@ from contextlib import contextmanager import copy import functools -import inspect import os import pathlib import typing @@ -18,15 +17,16 @@ def _is_running_on_ci() -> bool: - ci_envs = ["CI", "TRAVIS", "GITHUB_ACTIONS", "GITLAB_CI", "JENKINS_URL"] + ci_envs = ['CI', 'TRAVIS', 'GITHUB_ACTIONS', 'GITLAB_CI', 'JENKINS_URL'] return any(env in os.environ for env in ci_envs) - draftmode: bool = False -oncollision: typext.Literal["error", "fix", "ignore", "warn"] = os.environ.get( +oncollision: typext.Literal[ + "error", "fix", "ignore", "warn" +] = os.environ.get( "TEEPLOT_ONCOLLISION", - "warn" if (_is_running_on_ci() or not hasattr(sys, "ps1")) else "ignore", + "warn" if (_is_running_on_ci() or not hasattr(sys, 'ps1')) else "ignore", ).lower() if not oncollision in ("error", "fix", "ignore", "warn"): raise RuntimeError( @@ -52,8 +52,8 @@ def _is_running_on_ci() -> bool: # see https://gecco-2021.sigevo.org/Paper-Submission-Instructions @matplotlib.rc_context( { - "pdf.fonttype": 42, - "ps.fonttype": 42, + 'pdf.fonttype': 42, + 'ps.fonttype': 42, }, ) def tee( @@ -62,8 +62,7 @@ def tee( teeplot_callback: bool = False, teeplot_dpi: int = 300, teeplot_oncollision: typing.Optional[ - typext.Literal["error", "fix", "ignore", "warn"] - ] = None, + typext.Literal["error", "fix", "ignore", "warn"]] = None, teeplot_outattrs: typing.Dict[str, str] = {}, teeplot_outdir: str = "teeplots", teeplot_outinclude: typing.Iterable[str] = tuple(), @@ -71,10 +70,10 @@ def tee( teeplot_postprocess: typing.Union[str, typing.Callable] = "", teeplot_save: typing.Union[typing.Iterable[str], bool] = True, teeplot_show: typing.Optional[bool] = None, - teeplot_subdir: str = "", + teeplot_subdir: str = '', teeplot_transparent: bool = True, teeplot_verbose: bool = True, - **kwargs: typing.Any, + **kwargs: typing.Any ) -> typing.Any: """Executes a plotting function and saves the resulting plot to specified formats using a descriptive filename automatically generated from plotting @@ -185,11 +184,12 @@ def tee( elif isinstance(teeplot_save, str): if not teeplot_save in formats: raise ValueError( - f"only {[*formats]} save formats are supported, " f"not {teeplot_save}", + f"only {[*formats]} save formats are supported, " + f"not {teeplot_save}", ) # remove explicitly disabled outputs blacklist = set(k for k, v in formats.items() if v is False) - exclusions = {teeplot_save} & blacklist + exclusions = {teeplot_save} & blacklist if teeplot_verbose and exclusions: print(f"skipping {exclusions}") teeplot_save = {teeplot_save} - exclusions @@ -202,7 +202,7 @@ def tee( ) # remove explicitly disabled outputs blacklist = set(k for k, v in formats.items() if v is False) - exclusions = set(teeplot_save) & blacklist + exclusions = set(teeplot_save) & blacklist if teeplot_verbose and exclusions: print(f"skipping {exclusions}") teeplot_save = set(teeplot_save) - exclusions @@ -267,33 +267,29 @@ def tee( incl = [*teeplot_outinclude] attr_maker = lambda ext: { **{ - slugify(k): slugify(str(v)) + slugify(k) : slugify(str(v)) for k, v in kwargs.items() - if isinstance(v, (str, bool)) or k in incl + if isinstance(v, str) or k in incl }, **{ - "viz": slugify(plotter.__name__), - "ext": ext, + 'viz' : slugify(plotter.__name__), + 'ext' : ext, }, **( {"post": teeplot_postprocess.__name__} if teeplot_postprocess and isinstance(teeplot_postprocess, abc.Callable) - else ( - {"post": slugify(teeplot_postprocess)} - if teeplot_postprocess and not teeplot_postprocess.endswith(";") - else {} - ) + else {"post": slugify(teeplot_postprocess)} + if teeplot_postprocess and not teeplot_postprocess.endswith(";") + else {} ), **teeplot_outattrs, } excl = [*teeplot_outexclude] - out_filenamer = lambda ext: kn.pack( - { - k: v - for k, v in attr_maker(ext).items() - if not k.startswith("_") and not k in excl - } - ) + out_filenamer = lambda ext: kn.pack({ + k : v + for k, v in attr_maker(ext).items() + if not k.startswith('_') and not k in excl + }) out_folder = pathlib.Path(teeplot_outdir, teeplot_subdir) out_folder.mkdir(parents=True, exist_ok=True) @@ -320,7 +316,7 @@ def save_callback(): count = _history[out_path] suffix = f"ext={ext}" assert str(out_path).endswith(suffix) - out_path = str(out_path)[: -len(suffix)] + f"#={count}+" + suffix + out_path = str(out_path)[:-len(suffix)] + f"#={count}+" + suffix elif teeplot_oncollision == "ignore": pass elif teeplot_oncollision == "warn": @@ -338,7 +334,7 @@ def save_callback(): print(out_path) plt.savefig( str(out_path), - bbox_inches="tight", + bbox_inches='tight', transparent=teeplot_transparent, dpi=teeplot_dpi, # see https://matplotlib.org/2.1.1/users/whats_new.html#reproducible-ps-pdf-and-svg-output @@ -352,7 +348,7 @@ def save_callback(): }, ) - if teeplot_show or (teeplot_show is None and hasattr(sys, "ps1")): + if teeplot_show or (teeplot_show is None and hasattr(sys, 'ps1')): plt.show() return teed @@ -364,7 +360,7 @@ def save_callback(): @contextmanager -def teed(*args, **kwargs): +def teed(*args: list, **kwargs: dict): """Context manager interface to `teeplot.tee`. Plot save is dispatched upon exiting the context. Return value is the @@ -383,14 +379,12 @@ def teed(*args, **kwargs): finally: saveit() - def validate_teewrap_kwargs(teeplot_kwargs): - params = {k for k in inspect.signature(tee).parameters if k.startswith("teeplot")} - if not all(k in params for k in teeplot_kwargs): + if not all(k.startwith("teeplot") for k in teeplot_kwargs): raise ValueError( "The only keyword arguments passed into the `teewrap` decorator can be teeplot arguments" ) - if "teeplot_outattrs" in params: + if "teeplot_outattrs" in teeplot_kwargs: raise ValueError( "`teeplot_outattrs` cannot be used with `teewrap`. Use `teeplot_outattr_names` instead." ) @@ -412,17 +406,6 @@ def teewrap( def decorator(f: typing.Callable): @functools.wraps(f) def inner(*args, **kwargs): - - teeplot_outattr_names = teeplot_kwargs.get("teeplot_outinclude") - if teeplot_outattr_names is None: - return tee( - f, - *args, - **teeplot_kwargs, - teeplot_outattrs=kwargs, - **kwargs, - ) - return tee( f, *args, From ee57e5d6cf891bf57baebf68ad16a62e149f64c2 Mon Sep 17 00:00:00 2001 From: vivaansinghvi07 Date: Sat, 15 Feb 2025 16:08:17 -0500 Subject: [PATCH 07/15] add newline between functions --- teeplot/teeplot.py | 1 + 1 file changed, 1 insertion(+) diff --git a/teeplot/teeplot.py b/teeplot/teeplot.py index ae18ea8..f476ef6 100644 --- a/teeplot/teeplot.py +++ b/teeplot/teeplot.py @@ -379,6 +379,7 @@ def teed(*args: list, **kwargs: dict): finally: saveit() + def validate_teewrap_kwargs(teeplot_kwargs): if not all(k.startwith("teeplot") for k in teeplot_kwargs): raise ValueError( From f5f06be6f31b0da32f25fe5db24b31532d64dd41 Mon Sep 17 00:00:00 2001 From: vivaansinghvi07 Date: Sat, 15 Feb 2025 16:08:47 -0500 Subject: [PATCH 08/15] remove outdated part of documentation --- teeplot/teeplot.py | 1 - 1 file changed, 1 deletion(-) diff --git a/teeplot/teeplot.py b/teeplot/teeplot.py index f476ef6..c366ab9 100644 --- a/teeplot/teeplot.py +++ b/teeplot/teeplot.py @@ -400,7 +400,6 @@ def teewrap( `f` and any passed in arguments and keyword arguments. However `teeplot_outattrs` is not allowed with this function, as it would not make sense to have hardcoded attributes as a decorator. Instead, see `teeplot_outinclude` in `teeplot.tee`. - `teeplot.teewrap` defaults to including all keyword arguments. """ validate_teewrap_kwargs(teeplot_kwargs) From 864bc82b2b54a26c2104cd56893916440ac221db Mon Sep 17 00:00:00 2001 From: vivaansinghvi07 Date: Sat, 15 Feb 2025 16:14:54 -0500 Subject: [PATCH 09/15] fix type hints --- teeplot/teeplot.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/teeplot/teeplot.py b/teeplot/teeplot.py index c366ab9..f7a6af7 100644 --- a/teeplot/teeplot.py +++ b/teeplot/teeplot.py @@ -360,7 +360,7 @@ def save_callback(): @contextmanager -def teed(*args: list, **kwargs: dict): +def teed(*args, **kwargs): """Context manager interface to `teeplot.tee`. Plot save is dispatched upon exiting the context. Return value is the @@ -380,8 +380,8 @@ def teed(*args: list, **kwargs: dict): saveit() -def validate_teewrap_kwargs(teeplot_kwargs): - if not all(k.startwith("teeplot") for k in teeplot_kwargs): +def validate_teewrap_kwargs(teeplot_kwargs: dict[str, object]): + if not all(k.startswith("teeplot") for k in teeplot_kwargs): raise ValueError( "The only keyword arguments passed into the `teewrap` decorator can be teeplot arguments" ) From 39518606c4ddb1b4c0a4e71673898533fe9c2623 Mon Sep 17 00:00:00 2001 From: vivaansinghvi07 Date: Sat, 15 Feb 2025 16:18:48 -0500 Subject: [PATCH 10/15] allow teewrap outattrs for more flexibility and general flags --- teeplot/teeplot.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/teeplot/teeplot.py b/teeplot/teeplot.py index f7a6af7..e9cd14b 100644 --- a/teeplot/teeplot.py +++ b/teeplot/teeplot.py @@ -385,10 +385,6 @@ def validate_teewrap_kwargs(teeplot_kwargs: dict[str, object]): raise ValueError( "The only keyword arguments passed into the `teewrap` decorator can be teeplot arguments" ) - if "teeplot_outattrs" in teeplot_kwargs: - raise ValueError( - "`teeplot_outattrs` cannot be used with `teewrap`. Use `teeplot_outattr_names` instead." - ) def teewrap( From 913d0ba2a70b44890d6ced2fa5f836d05aad67d9 Mon Sep 17 00:00:00 2001 From: vivaansinghvi07 Date: Sat, 15 Feb 2025 16:20:14 -0500 Subject: [PATCH 11/15] update documentation --- teeplot/teeplot.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/teeplot/teeplot.py b/teeplot/teeplot.py index e9cd14b..5afd6f1 100644 --- a/teeplot/teeplot.py +++ b/teeplot/teeplot.py @@ -393,9 +393,10 @@ def teewrap( """Decorator interface to `teeplot.tee` Works by returning a decorator that wraps `f` by calling `teeplot.tee` using - `f` and any passed in arguments and keyword arguments. However `teeplot_outattrs` - is not allowed with this function, as it would not make sense to have hardcoded - attributes as a decorator. Instead, see `teeplot_outinclude` in `teeplot.tee`. + `f` and any passed in arguments and keyword arguments. However, using + `teeplot_outattrs` like in `teeplot.tee` will cause printed attributes to be + the same across function calls. For printing attributes on a per-call basis, + see `teeplot_outinclude` in `teeplot.tee`. """ validate_teewrap_kwargs(teeplot_kwargs) From dab6baeed0e5f0ef0c4bd052f2c52abcc9d12d68 Mon Sep 17 00:00:00 2001 From: vivaansinghvi07 Date: Sat, 15 Feb 2025 16:32:18 -0500 Subject: [PATCH 12/15] added tests for teewrap --- tests/test_teewrap.py | 96 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 tests/test_teewrap.py diff --git a/tests/test_teewrap.py b/tests/test_teewrap.py new file mode 100644 index 0000000..35094bb --- /dev/null +++ b/tests/test_teewrap.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python + +''' +`tee` tests for `teeplot` package. +''' + +import functools +import os + +import numpy as np +import pytest +import seaborn as sns + +from teeplot import teeplot as tp + + +@tp.teewrap( + teeplot_outattrs={ + 'additional' : 'teedmetadata', + 'for' : 'output-filename', + '_one-for' : 'exclusion', + }, +) +@functools.wraps(sns.lineplot) +def teed_snslineplot_outattrs(*args, **kwargs): + return sns.lineplot(*args, **kwargs) + +def test(): + + teed_snslineplot_outattrs( + x='timepoint', + y='signal', + hue='region', + style='event', + data=sns.load_dataset('fmri'), + ) + + for ext in '.pdf', '.png': + assert os.path.exists( + os.path.join('teeplots', f'additional=teedmetadata+for=output-filename+hue=region+style=event+viz=lineplot+x=timepoint+y=signal+ext={ext}'), + ) + + +@pytest.mark.parametrize("format", [".png", ".pdf", ".ps", ".eps", ".svg"]) +def test_outformat(format): + + # adapted from https://seaborn.pydata.org/generated/seaborn.lineplot.html + np.random.seed(1) + x, y = np.random.normal(size=(2, 5000)).cumsum(axis=1) + + @tp.teewrap( + teeplot_outattrs={ + 'outformat' : 'teedmetadata', + }, + teeplot_subdir='mydirectory', + teeplot_save={format}, + ) + @functools.wraps(sns.lineplot) + def teed_lineplot_outformat(*args, **kwargs): + return sns.lineplot(*args, **kwargs) + + teed_lineplot_outformat( + x=x, + y=y, + sort=False, + lw=1, + ) + + assert os.path.exists( + os.path.join('teeplots', 'mydirectory', f'outformat=teedmetadata+viz=lineplot+ext={format}'), + ) + + +@tp.teewrap(teeplot_outinclude=['a', 'b']) +@functools.wraps(sns.lineplot) +def teed_snslineplot_extra_args(*args, a, b, **kwargs): + return sns.lineplot(*args, **kwargs) + + +@pytest.mark.parametrize('a', [False, 1, 1]) +@pytest.mark.parametrize('b', ['asdf', '']) +def test_included_outattrs(a, b): + + teed_snslineplot_extra_args( + a=a, + b=b, + x='timepoint', + y='signal', + hue='region', + data=sns.load_dataset('fmri'), + ) + + for ext in '.pdf', '.png': + assert os.path.exists( + os.path.join('teeplots', f'a={a}+b={b}+hue=region+viz=lineplot+x=timepoint+y=signal+ext={ext}'), + ) From 3f0fa08ada54a0dc9376cad022cad983f790aea5 Mon Sep 17 00:00:00 2001 From: Vivaan Singhvi <122464874+vivaansinghvi07@users.noreply.github.com> Date: Sat, 15 Feb 2025 17:27:45 -0500 Subject: [PATCH 13/15] Update teeplot/teeplot.py Co-authored-by: Matthew Andres Moreno --- teeplot/teeplot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/teeplot/teeplot.py b/teeplot/teeplot.py index 5afd6f1..22f5d21 100644 --- a/teeplot/teeplot.py +++ b/teeplot/teeplot.py @@ -381,7 +381,7 @@ def teed(*args, **kwargs): def validate_teewrap_kwargs(teeplot_kwargs: dict[str, object]): - if not all(k.startswith("teeplot") for k in teeplot_kwargs): + if not all(k.startswith("teeplot_") for k in teeplot_kwargs): raise ValueError( "The only keyword arguments passed into the `teewrap` decorator can be teeplot arguments" ) From 8838962cb1b1fef2250c2f3832053eae0bb0fbbe Mon Sep 17 00:00:00 2001 From: Vivaan Singhvi <122464874+vivaansinghvi07@users.noreply.github.com> Date: Sat, 15 Feb 2025 17:28:05 -0500 Subject: [PATCH 14/15] Update teeplot/teeplot.py Co-authored-by: Matthew Andres Moreno --- teeplot/teeplot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/teeplot/teeplot.py b/teeplot/teeplot.py index 22f5d21..f8ac357 100644 --- a/teeplot/teeplot.py +++ b/teeplot/teeplot.py @@ -383,7 +383,7 @@ def teed(*args, **kwargs): def validate_teewrap_kwargs(teeplot_kwargs: dict[str, object]): if not all(k.startswith("teeplot_") for k in teeplot_kwargs): raise ValueError( - "The only keyword arguments passed into the `teewrap` decorator can be teeplot arguments" + "The `teewrap` decorator only accepts teeplot_* keyword arguments" ) From 5eec37c8e2a376879c71fcf68a549fd14c507bac Mon Sep 17 00:00:00 2001 From: vivaansinghvi07 Date: Sat, 15 Feb 2025 17:28:39 -0500 Subject: [PATCH 15/15] inline argument validation --- teeplot/teeplot.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/teeplot/teeplot.py b/teeplot/teeplot.py index f8ac357..82e30f2 100644 --- a/teeplot/teeplot.py +++ b/teeplot/teeplot.py @@ -380,13 +380,6 @@ def teed(*args, **kwargs): saveit() -def validate_teewrap_kwargs(teeplot_kwargs: dict[str, object]): - if not all(k.startswith("teeplot_") for k in teeplot_kwargs): - raise ValueError( - "The `teewrap` decorator only accepts teeplot_* keyword arguments" - ) - - def teewrap( **teeplot_kwargs: object, ): @@ -398,7 +391,10 @@ def teewrap( the same across function calls. For printing attributes on a per-call basis, see `teeplot_outinclude` in `teeplot.tee`. """ - validate_teewrap_kwargs(teeplot_kwargs) + if not all(k.startswith("teeplot_") for k in teeplot_kwargs): + raise ValueError( + "The `teewrap` decorator only accepts teeplot_* keyword arguments" + ) def decorator(f: typing.Callable): @functools.wraps(f)