From 6964d0dd7a23f9fa57647b402762755ab64e0e72 Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Sun, 12 May 2024 13:05:03 +0100 Subject: [PATCH 01/15] feat: Type hints: Adds `ChartType` and guard `is_chart_type` I'm working on a library that uses `altair`, and looking to make it an optional dependency. During this process, I thought `ChartType` and `is_chart_type` might be a good fit in `altair` itself. I don't believe any existing `altair` code would directly benefit from these additions - but you could likely reduce some `isinstance` checks with [type narrowing](https://typing.readthedocs.io/en/latest/spec/narrowing.html) functions like `is_chart_type`. For example `altair/vegalite/v5/api.py` has **63** occurrences of `isinstance`. --- per [CONTRIBUTING.md](https://github.com/vega/altair/blob/main/CONTRIBUTING.md) > In particular, we welcome companion efforts from other visualization libraries to render the Vega-Lite specifications output by Altair. We see this portion of the effort as much bigger than Altair itself Towards this aim, it could be beneficial to provide `typing` constructs/tools for downstream use. Regarding my specfic use case, the below [Protocol](https://typing.readthedocs.io/en/latest/spec/protocol.html#runtime-checkable-decorator-and-narrowing-types-by-isinstance) - I believe - describes the concept of a `_/Chart`, for the purposes of writing to file: ```py from typing import Any, Protocol, runtime_checkable @runtime_checkable class AltairChart(Protocol): data: Any def copy(self, *args: Any, **kwargs: Any) -> Any: ... def save(self, *args: Any, **kwargs: Any) -> None: ... def to_dict(self, *args: Any, **kwargs: Any) -> dict[Any, Any]: ... def to_html(self, *args: Any, **kwargs: Any) -> str: ... ``` Due to the number of symbols in `altair` and heavy use of mixins, coming to this conclusion can be a stumbling block. However, exporting things like `ChartType`, `is_chart_type`, `AltairChart` could be a way to expose these shared behaviours - without requiring any changes to the core API. --- altair/vegalite/v5/api.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/altair/vegalite/v5/api.py b/altair/vegalite/v5/api.py index 93bd295b3..6f71f1382 100644 --- a/altair/vegalite/v5/api.py +++ b/altair/vegalite/v5/api.py @@ -29,6 +29,10 @@ from ...utils.core import DataFrameLike from ...utils.data import DataType +if sys.version_info >= (3, 13): + from typing import TypeIs +else: + from typing_extensions import TypeIs if sys.version_info >= (3, 11): from typing import Self else: @@ -4089,3 +4093,23 @@ def graticule(**kwds): def sphere() -> core.SphereGenerator: """Sphere generator.""" return core.SphereGenerator(sphere=True) + + +ChartType = Union[ + Chart, RepeatChart, ConcatChart, HConcatChart, VConcatChart, FacetChart, LayerChart +] + + +def is_chart_type(obj: Any) -> TypeIs[ChartType]: + return isinstance( + obj, + ( + Chart, + RepeatChart, + ConcatChart, + HConcatChart, + VConcatChart, + FacetChart, + LayerChart, + ), + ) From 96bf09231fbec74203c4f494d179dde5b063886e Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Sun, 12 May 2024 13:05:49 +0100 Subject: [PATCH 02/15] fix: Type hints: Add missing `pathlib.Path` in `save` method annotation `TopLevelMixin.save` calls a function accepting `pathlib` objects, but the two didn't share the same `Union` for `fp`. When using `pyright`, the following warning is presented: > Argument of type "Path" cannot be assigned to parameter "fp" of type "str | IO[Unknown]" in function "save" > Type "Path" is incompatible with type "str | IO[Unknown]" > "Path" is incompatible with "str" > "Path" is incompatible with "IO[Unknown]" This fix avoids the need to use type ignores on the public facing API, as a user. --- altair/vegalite/v5/api.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/altair/vegalite/v5/api.py b/altair/vegalite/v5/api.py index 6f71f1382..6257953f9 100644 --- a/altair/vegalite/v5/api.py +++ b/altair/vegalite/v5/api.py @@ -8,6 +8,7 @@ from toolz.curried import pipe as _pipe import itertools import sys +import pathlib from typing import cast, List, Optional, Any, Iterable, Union, Literal, IO # Have to rename it here as else it overlaps with schema.core.Type and schema.core.Dict @@ -1141,7 +1142,7 @@ def open_editor(self, *, fullscreen: bool = False) -> None: def save( self, - fp: Union[str, IO], + fp: Union[str, pathlib.Path, IO], format: Optional[Literal["json", "html", "png", "svg", "pdf"]] = None, override_data_transformer: bool = True, scale_factor: float = 1.0, From 9c770a5fb8da2ed987f96b56017dde4d8a475394 Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Sun, 12 May 2024 13:25:10 +0100 Subject: [PATCH 03/15] refactor: Simplify `save.py` path handling By normalizing `fp` in `save`, it simplifies the signatures of two other functions and removes the need for `str` specific suffix handling. --- altair/utils/save.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/altair/utils/save.py b/altair/utils/save.py index 1784cc43e..7b5ba2df5 100644 --- a/altair/utils/save.py +++ b/altair/utils/save.py @@ -9,14 +9,14 @@ def write_file_or_filename( - fp: Union[str, pathlib.PurePath, IO], + fp: Union[pathlib.Path, IO], content: Union[str, bytes], mode: str = "w", encoding: Optional[str] = None, ) -> None: """Write content to fp, whether fp is a string, a pathlib Path or a file-like object""" - if isinstance(fp, str) or isinstance(fp, pathlib.PurePath): + if isinstance(fp, pathlib.Path): with open(file=fp, mode=mode, encoding=encoding) as f: f.write(content) else: @@ -24,13 +24,11 @@ def write_file_or_filename( def set_inspect_format_argument( - format: Optional[str], fp: Union[str, pathlib.PurePath, IO], inline: bool + format: Optional[str], fp: Union[pathlib.Path, IO], inline: bool ) -> str: """Inspect the format argument in the save function""" if format is None: - if isinstance(fp, str): - format = fp.split(".")[-1] - elif isinstance(fp, pathlib.PurePath): + if isinstance(fp, pathlib.Path): format = fp.suffix.lstrip(".") else: raise ValueError( @@ -70,7 +68,7 @@ def set_inspect_mode_argument( def save( chart, - fp: Union[str, pathlib.PurePath, IO], + fp: Union[str, pathlib.Path, IO], vega_version: Optional[str], vegaembed_version: Optional[str], format: Optional[Literal["json", "html", "png", "svg", "pdf"]] = None, @@ -130,6 +128,7 @@ def save( """ if json_kwds is None: json_kwds = {} + fp = pathlib.Path(fp) if isinstance(fp, str) else fp # type: ignore[assignment] format = set_inspect_format_argument(format, fp, inline) # type: ignore[assignment] From 99b4f81d456b2b05c97919e751152dcec1cbf00b Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Sun, 12 May 2024 13:40:15 +0100 Subject: [PATCH 04/15] refactor: Replace duplicated `save` get encoding with a single expression Although there are branches it isn't needed, I think this is easier to read and maintain. Very low priority change, just something I noticed while reading. --- altair/utils/save.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/altair/utils/save.py b/altair/utils/save.py index 7b5ba2df5..9fbb2b292 100644 --- a/altair/utils/save.py +++ b/altair/utils/save.py @@ -129,6 +129,7 @@ def save( if json_kwds is None: json_kwds = {} fp = pathlib.Path(fp) if isinstance(fp, str) else fp # type: ignore[assignment] + encoding = kwargs.get("encoding", "utf-8") format = set_inspect_format_argument(format, fp, inline) # type: ignore[assignment] @@ -141,9 +142,7 @@ def perform_save(): if format == "json": json_spec = json.dumps(spec, **json_kwds) - write_file_or_filename( - fp, json_spec, mode="w", encoding=kwargs.get("encoding", "utf-8") - ) + write_file_or_filename(fp, json_spec, mode="w", encoding=encoding) elif format == "html": if inline: kwargs["template"] = "inline" @@ -159,12 +158,9 @@ def perform_save(): **kwargs, ) write_file_or_filename( - fp, - mimebundle["text/html"], - mode="w", - encoding=kwargs.get("encoding", "utf-8"), + fp, mimebundle["text/html"], mode="w", encoding=encoding ) - elif format in ["png", "svg", "pdf", "vega"]: + elif format in {"png", "svg", "pdf", "vega"}: mimebundle = spec_to_mimebundle( spec=spec, format=format, @@ -183,7 +179,6 @@ def perform_save(): elif format == "pdf": write_file_or_filename(fp, mimebundle["application/pdf"], mode="wb") else: - encoding = kwargs.get("encoding", "utf-8") write_file_or_filename( fp, mimebundle["image/svg+xml"], mode="w", encoding=encoding ) From 55cddd57f305a714e15a86d3c44450ecc49efcf4 Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Sun, 12 May 2024 13:48:42 +0100 Subject: [PATCH 05/15] docs: Add docstring for `is_chart_type` --- altair/vegalite/v5/api.py | 1 + 1 file changed, 1 insertion(+) diff --git a/altair/vegalite/v5/api.py b/altair/vegalite/v5/api.py index 6257953f9..027962c41 100644 --- a/altair/vegalite/v5/api.py +++ b/altair/vegalite/v5/api.py @@ -4102,6 +4102,7 @@ def sphere() -> core.SphereGenerator: def is_chart_type(obj: Any) -> TypeIs[ChartType]: + """Return `True` if the object is basic or compound `Chart`.""" return isinstance( obj, ( From a97f760051f80c9227b49bcbbc8ab31d14d60863 Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Mon, 13 May 2024 14:55:30 +0100 Subject: [PATCH 06/15] fix: Removed unused `type: ignore` comment --- altair/utils/save.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/altair/utils/save.py b/altair/utils/save.py index 9fbb2b292..4374f8c45 100644 --- a/altair/utils/save.py +++ b/altair/utils/save.py @@ -128,7 +128,7 @@ def save( """ if json_kwds is None: json_kwds = {} - fp = pathlib.Path(fp) if isinstance(fp, str) else fp # type: ignore[assignment] + fp = pathlib.Path(fp) if isinstance(fp, str) else fp encoding = kwargs.get("encoding", "utf-8") format = set_inspect_format_argument(format, fp, inline) # type: ignore[assignment] From 32010bbf46ff81521d087d160cd9d8151d906147 Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Mon, 13 May 2024 15:50:05 +0100 Subject: [PATCH 07/15] build: Mark `TypeIs` as not a relevant attribute --- tools/update_init_file.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tools/update_init_file.py b/tools/update_init_file.py index 4f342f8ef..e4fa65a86 100644 --- a/tools/update_init_file.py +++ b/tools/update_init_file.py @@ -21,6 +21,10 @@ cast, ) +if sys.version_info >= (3, 13): + from typing import TypeIs +else: + from typing_extensions import TypeIs if sys.version_info >= (3, 11): from typing import Self else: @@ -97,6 +101,7 @@ def _is_relevant_attribute(attr_name: str) -> bool: or attr is Protocol or attr is Sequence or attr is IO + or attr is TypeIs or attr_name == "TypingDict" or attr_name == "TypingGenerator" or attr_name == "ValueOrDatum" From 14bd0e6844f46527b2ff25e23a61e1de70a0348f Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Mon, 13 May 2024 15:57:48 +0100 Subject: [PATCH 08/15] build: Make new additions to `api` private Believe these should be public, but it seems like the issue at build-time is that I'd be extending the public API. --- altair/vegalite/v5/api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/altair/vegalite/v5/api.py b/altair/vegalite/v5/api.py index 027962c41..b7e197932 100644 --- a/altair/vegalite/v5/api.py +++ b/altair/vegalite/v5/api.py @@ -4096,12 +4096,12 @@ def sphere() -> core.SphereGenerator: return core.SphereGenerator(sphere=True) -ChartType = Union[ +_ChartType = Union[ Chart, RepeatChart, ConcatChart, HConcatChart, VConcatChart, FacetChart, LayerChart ] -def is_chart_type(obj: Any) -> TypeIs[ChartType]: +def _is_chart_type(obj: Any) -> TypeIs[_ChartType]: """Return `True` if the object is basic or compound `Chart`.""" return isinstance( obj, From 8dd2cded5a96954d23e6304eddead7bcd56e570d Mon Sep 17 00:00:00 2001 From: Stefan Binder Date: Mon, 20 May 2024 15:43:33 +0200 Subject: [PATCH 09/15] Bump version of typing_extensions so it includes TypeIs --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index fdc9704f8..9dad9bd74 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,7 @@ build-backend = "hatchling.build" name = "altair" authors = [{ name = "Vega-Altair Contributors" }] dependencies = [ - "typing_extensions>=4.0.1; python_version<\"3.11\"", + "typing_extensions>=4.10.0; python_version<\"3.13\"", "jinja2", # If you update the minimum required jsonschema version, also update it in build.yml "jsonschema>=3.0", From a4de6d1feb6f3f17c7f6d4c7738eb008ff33d17e Mon Sep 17 00:00:00 2001 From: Stefan Binder Date: Mon, 20 May 2024 15:47:05 +0200 Subject: [PATCH 10/15] Make ChartType and is_chart_type public. Use typing.get_args to reduce duplication of list of charts --- altair/__init__.py | 2 ++ altair/vegalite/v5/api.py | 25 ++++++++++--------------- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/altair/__init__.py b/altair/__init__.py index 1c5b755f6..34455e223 100644 --- a/altair/__init__.py +++ b/altair/__init__.py @@ -55,6 +55,7 @@ "Categorical", "Chart", "ChartDataType", + "ChartType", "Color", "ColorDatum", "ColorDef", @@ -579,6 +580,7 @@ "expr", "graticule", "hconcat", + "is_chart_type", "jupyter", "layer", "limit_rows", diff --git a/altair/vegalite/v5/api.py b/altair/vegalite/v5/api.py index b7e197932..b10f8b1b6 100644 --- a/altair/vegalite/v5/api.py +++ b/altair/vegalite/v5/api.py @@ -9,6 +9,7 @@ import itertools import sys import pathlib +import typing from typing import cast, List, Optional, Any, Iterable, Union, Literal, IO # Have to rename it here as else it overlaps with schema.core.Type and schema.core.Dict @@ -236,9 +237,9 @@ def to_dict(self) -> TypingDict[str, Union[str, dict]]: return {"expr": self.name} elif self.param_type == "selection": return { - "param": self.name.to_dict() - if hasattr(self.name, "to_dict") - else self.name + "param": ( + self.name.to_dict() if hasattr(self.name, "to_dict") else self.name + ) } else: raise ValueError(f"Unrecognized parameter type: {self.param_type}") @@ -4096,22 +4097,16 @@ def sphere() -> core.SphereGenerator: return core.SphereGenerator(sphere=True) -_ChartType = Union[ +ChartType = Union[ Chart, RepeatChart, ConcatChart, HConcatChart, VConcatChart, FacetChart, LayerChart ] -def _is_chart_type(obj: Any) -> TypeIs[_ChartType]: - """Return `True` if the object is basic or compound `Chart`.""" +def is_chart_type(obj: Any) -> TypeIs[ChartType]: + """Return `True` if the object is an Altair chart. This can be a basic chart + but also a repeat, concat, or facet chart. + """ return isinstance( obj, - ( - Chart, - RepeatChart, - ConcatChart, - HConcatChart, - VConcatChart, - FacetChart, - LayerChart, - ), + typing.get_args(ChartType), ) From f12aea0d0c9c26187129e9eaa85df0cd9acde5f6 Mon Sep 17 00:00:00 2001 From: Stefan Binder Date: Mon, 20 May 2024 15:49:09 +0200 Subject: [PATCH 11/15] Update API docs --- doc/user_guide/api.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/user_guide/api.rst b/doc/user_guide/api.rst index 99aa5f69b..08013023f 100644 --- a/doc/user_guide/api.rst +++ b/doc/user_guide/api.rst @@ -152,6 +152,7 @@ API Functions condition graticule hconcat + is_chart_type layer param repeat From 9b8f6a8878f2a2e8fcd2eae5d10184349bfa59d1 Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Tue, 21 May 2024 09:23:49 +0100 Subject: [PATCH 12/15] refactor: Adds ruff `SIM101` and apply fixes As [requested](https://github.com/vega/altair/pull/3420/files/a4de6d1feb6f3f17c7f6d4c7738eb008ff33d17e#r1606780541). Seems to only occur in a few places, but all were autofix-able. --- altair/utils/_transformed_data.py | 4 +--- altair/utils/schemapi.py | 2 +- pyproject.toml | 3 +++ tools/schemapi/schemapi.py | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/altair/utils/_transformed_data.py b/altair/utils/_transformed_data.py index 1d886eb5e..9ed28f475 100644 --- a/altair/utils/_transformed_data.py +++ b/altair/utils/_transformed_data.py @@ -180,9 +180,7 @@ def name_views( List of the names of the charts and subcharts """ exclude = set(exclude) if exclude is not None else set() - if isinstance(chart, _chart_class_mapping[Chart]) or isinstance( - chart, _chart_class_mapping[FacetChart] - ): + if isinstance(chart, (_chart_class_mapping[Chart], _chart_class_mapping[FacetChart])): if chart.name not in exclude: if chart.name in (None, Undefined): # Add name since none is specified diff --git a/altair/utils/schemapi.py b/altair/utils/schemapi.py index 9325b3a70..10b2597e6 100644 --- a/altair/utils/schemapi.py +++ b/altair/utils/schemapi.py @@ -1193,7 +1193,7 @@ def _freeze(val): return frozenset((k, _freeze(v)) for k, v in val.items()) elif isinstance(val, set): return frozenset(map(_freeze, val)) - elif isinstance(val, list) or isinstance(val, tuple): + elif isinstance(val, (list, tuple)): return tuple(map(_freeze, val)) else: return val diff --git a/pyproject.toml b/pyproject.toml index 9dad9bd74..487cc26d9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -164,6 +164,7 @@ exclude = [ ] [tool.ruff.lint] +extend-safe-fixes=["SIM101"] select = [ # flake8-bugbear "B", @@ -177,6 +178,8 @@ select = [ "F", # flake8-tidy-imports "TID", + # flake8-simplify + "SIM101" ] ignore = [ # Whitespace before ':' diff --git a/tools/schemapi/schemapi.py b/tools/schemapi/schemapi.py index 29da970ec..ce1268c04 100644 --- a/tools/schemapi/schemapi.py +++ b/tools/schemapi/schemapi.py @@ -1191,7 +1191,7 @@ def _freeze(val): return frozenset((k, _freeze(v)) for k, v in val.items()) elif isinstance(val, set): return frozenset(map(_freeze, val)) - elif isinstance(val, list) or isinstance(val, tuple): + elif isinstance(val, (list, tuple)): return tuple(map(_freeze, val)) else: return val From 19010ba3d839461a741085b8a3e1db4378a37032 Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Tue, 21 May 2024 10:44:03 +0100 Subject: [PATCH 13/15] revert: Change `set` back to `list` for `format` As [requested](https://github.com/vega/altair/pull/3420/commits/99b4f81d456b2b05c97919e751152dcec1cbf00b#r1606785466) --- altair/utils/save.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/altair/utils/save.py b/altair/utils/save.py index 4374f8c45..61e4ec34a 100644 --- a/altair/utils/save.py +++ b/altair/utils/save.py @@ -160,7 +160,7 @@ def perform_save(): write_file_or_filename( fp, mimebundle["text/html"], mode="w", encoding=encoding ) - elif format in {"png", "svg", "pdf", "vega"}: + elif format in ["png", "svg", "pdf", "vega"]: mimebundle = spec_to_mimebundle( spec=spec, format=format, From 082cd601b4ff126c688a3e5341d799f1c3518100 Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Tue, 21 May 2024 10:48:54 +0100 Subject: [PATCH 14/15] style: Re-run ruff format --- altair/utils/_transformed_data.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/altair/utils/_transformed_data.py b/altair/utils/_transformed_data.py index 9ed28f475..7a616b9c5 100644 --- a/altair/utils/_transformed_data.py +++ b/altair/utils/_transformed_data.py @@ -180,7 +180,9 @@ def name_views( List of the names of the charts and subcharts """ exclude = set(exclude) if exclude is not None else set() - if isinstance(chart, (_chart_class_mapping[Chart], _chart_class_mapping[FacetChart])): + if isinstance( + chart, (_chart_class_mapping[Chart], _chart_class_mapping[FacetChart]) + ): if chart.name not in exclude: if chart.name in (None, Undefined): # Add name since none is specified From fad765b92a5e984b3c20a313cf10e6feca6c83c0 Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Thu, 23 May 2024 10:47:14 +0100 Subject: [PATCH 15/15] revert: Add `str` support back for "private" `save` functions An earlier commit assumed a minor optimization would be possible, by normalizing a `Union` early and reuse the narrowed type in "private" functions. @binste [comment](https://github.com/vega/altair/pull/3420/files#r1610440552) --- altair/utils/save.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/altair/utils/save.py b/altair/utils/save.py index 7bc9593aa..609486bc6 100644 --- a/altair/utils/save.py +++ b/altair/utils/save.py @@ -10,14 +10,14 @@ def write_file_or_filename( - fp: Union[pathlib.Path, IO], + fp: Union[str, pathlib.Path, IO], content: Union[str, bytes], mode: str = "w", encoding: Optional[str] = None, ) -> None: """Write content to fp, whether fp is a string, a pathlib Path or a file-like object""" - if isinstance(fp, pathlib.Path): + if isinstance(fp, (str, pathlib.Path)): with open(file=fp, mode=mode, encoding=encoding) as f: f.write(content) else: @@ -25,12 +25,12 @@ def write_file_or_filename( def set_inspect_format_argument( - format: Optional[str], fp: Union[pathlib.Path, IO], inline: bool + format: Optional[str], fp: Union[str, pathlib.Path, IO], inline: bool ) -> str: """Inspect the format argument in the save function""" if format is None: - if isinstance(fp, pathlib.Path): - format = fp.suffix.lstrip(".") + if isinstance(fp, (str, pathlib.Path)): + format = pathlib.Path(fp).suffix.lstrip(".") else: raise ValueError( "must specify file format: " @@ -138,9 +138,7 @@ def save( if json_kwds is None: json_kwds = {} - fp = pathlib.Path(fp) if isinstance(fp, str) else fp encoding = kwargs.get("encoding", "utf-8") - format = set_inspect_format_argument(format, fp, inline) # type: ignore[assignment] def perform_save():