Skip to content

Commit 3f96b74

Browse files
authored
refactor: Rename and move is_undefined, OneOrSeq (#3491)
* refactor: Rename and move `is_undefined` Planned in: https://github.com/vega/altair/pull/3480/files/419a4e944f231026322e2c4f137e7a3eb94735e8#r1679197993 - `api._is_undefined` -> `schemapi.is_undefined` - Added to `utils.__all__` * refactor: Rename and move `OneOrSeq` Planned in #3427 (comment) Will allow for more reuse
1 parent c29afaa commit 3f96b74

File tree

7 files changed

+89
-53
lines changed

7 files changed

+89
-53
lines changed

altair/utils/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from .html import spec_to_html
1414
from .plugin_registry import PluginRegistry
1515
from .deprecation import AltairDeprecationWarning, deprecated, deprecated_warn
16-
from .schemapi import Undefined, Optional
16+
from .schemapi import Undefined, Optional, is_undefined
1717

1818

1919
__all__ = (
@@ -28,6 +28,7 @@
2828
"display_traceback",
2929
"infer_encoding_types",
3030
"infer_vegalite_type_for_pandas",
31+
"is_undefined",
3132
"parse_shorthand",
3233
"sanitize_narwhals_dataframe",
3334
"sanitize_pandas_dataframe",

altair/utils/schemapi.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -787,6 +787,19 @@ def func_2(
787787
"""
788788

789789

790+
def is_undefined(obj: Any) -> TypeIs[UndefinedType]:
791+
"""Type-safe singleton check for `UndefinedType`.
792+
793+
Notes
794+
-----
795+
- Using `obj is Undefined` does not narrow from `UndefinedType` in a union.
796+
- Due to the assumption that other `UndefinedType`'s could exist.
797+
- Current [typing spec advises](https://typing.readthedocs.io/en/latest/spec/concepts.html#support-for-singleton-types-in-unions) using an `Enum`.
798+
- Otherwise, requires an explicit guard to inform the type checker.
799+
"""
800+
return obj is Undefined
801+
802+
790803
class SchemaBase:
791804
"""Base class for schema wrappers.
792805

altair/vegalite/v5/api.py

Lines changed: 9 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
Union,
1515
TYPE_CHECKING,
1616
TypeVar,
17-
Sequence,
1817
Protocol,
1918
)
2019
from typing_extensions import TypeAlias
@@ -45,10 +44,6 @@
4544
from typing import TypedDict
4645
else:
4746
from typing_extensions import TypedDict
48-
if sys.version_info >= (3, 12):
49-
from typing import TypeAliasType
50-
else:
51-
from typing_extensions import TypeAliasType
5247

5348
if TYPE_CHECKING:
5449
from ...utils.core import DataFrameLike
@@ -107,7 +102,6 @@
107102
TopLevelSelectionParameter,
108103
SelectionParameter,
109104
InlineDataset,
110-
UndefinedType,
111105
)
112106
from altair.expr.core import (
113107
BinaryExpression,
@@ -126,26 +120,12 @@
126120
AggregateOp_T,
127121
MultiTimeUnit_T,
128122
SingleTimeUnit_T,
123+
OneOrSeq,
129124
)
130125

131126

132127
ChartDataType: TypeAlias = Optional[Union[DataType, core.Data, str, core.Generator]]
133128
_TSchemaBase = TypeVar("_TSchemaBase", bound=core.SchemaBase)
134-
_T = TypeVar("_T")
135-
_OneOrSeq = TypeAliasType("_OneOrSeq", Union[_T, Sequence[_T]], type_params=(_T,))
136-
"""One of ``_T`` specified type(s), or a `Sequence` of such.
137-
138-
Examples
139-
--------
140-
The parameters ``short``, ``long`` accept the same range of types::
141-
142-
# ruff: noqa: UP006, UP007
143-
144-
def func(
145-
short: _OneOrSeq[str | bool | float],
146-
long: Union[str, bool, float, Sequence[Union[str, bool, float]],
147-
): ...
148-
"""
149129

150130

151131
# ------------------------------------------------------------------------
@@ -181,7 +161,7 @@ def _consolidate_data(data: Any, context: Any) -> Any:
181161
kwds = {}
182162

183163
if isinstance(data, core.InlineData):
184-
if _is_undefined(data.name) and not _is_undefined(data.values):
164+
if utils.is_undefined(data.name) and not utils.is_undefined(data.values):
185165
if isinstance(data.values, core.InlineDataset):
186166
values = data.to_dict()["values"]
187167
else:
@@ -192,7 +172,7 @@ def _consolidate_data(data: Any, context: Any) -> Any:
192172
values = data["values"]
193173
kwds = {k: v for k, v in data.items() if k != "values"}
194174

195-
if not _is_undefined(values):
175+
if not utils.is_undefined(values):
196176
name = _dataset_name(values)
197177
data = core.NamedData(name=name, **kwds)
198178
context.setdefault("datasets", {})[name] = values
@@ -416,7 +396,7 @@ def _from_expr(self, expr) -> SelectionExpression:
416396

417397
def check_fields_and_encodings(parameter: Parameter, field_name: str) -> bool:
418398
param = parameter.param
419-
if _is_undefined(param) or isinstance(param, core.VariableParameter):
399+
if utils.is_undefined(param) or isinstance(param, core.VariableParameter):
420400
return False
421401
for prop in ["fields", "encodings"]:
422402
try:
@@ -485,19 +465,6 @@ def _is_test_predicate(obj: Any) -> TypeIs[_TestPredicateType]:
485465
return isinstance(obj, (str, _expr_core.Expression, core.PredicateComposition))
486466

487467

488-
def _is_undefined(obj: Any) -> TypeIs[UndefinedType]:
489-
"""Type-safe singleton check for `UndefinedType`.
490-
491-
Notes
492-
-----
493-
- Using `obj is Undefined` does not narrow from `UndefinedType` in a union.
494-
- Due to the assumption that other `UndefinedType`'s could exist.
495-
- Current [typing spec advises](https://typing.readthedocs.io/en/latest/spec/concepts.html#support-for-singleton-types-in-unions) using an `Enum`.
496-
- Otherwise, requires an explicit guard to inform the type checker.
497-
"""
498-
return obj is Undefined
499-
500-
501468
def _get_predicate_expr(p: Parameter) -> Optional[str | SchemaBase]:
502469
# https://vega.github.io/vega-lite/docs/predicate.html
503470
return getattr(p.param, "expr", Undefined)
@@ -509,7 +476,7 @@ def _predicate_to_condition(
509476
condition: _ConditionType
510477
if isinstance(predicate, Parameter):
511478
predicate_expr = _get_predicate_expr(predicate)
512-
if predicate.param_type == "selection" or _is_undefined(predicate_expr):
479+
if predicate.param_type == "selection" or utils.is_undefined(predicate_expr):
513480
condition = {"param": predicate.name}
514481
if isinstance(empty, bool):
515482
condition["empty"] = empty
@@ -585,7 +552,7 @@ class _ConditionExtra(TypedDict, closed=True, total=False): # type: ignore[call
585552
param: Parameter | str
586553
test: _TestPredicateType
587554
value: Any
588-
__extra_items__: _StatementType | _OneOrSeq[_LiteralValue]
555+
__extra_items__: _StatementType | OneOrSeq[_LiteralValue]
589556

590557

591558
_Condition: TypeAlias = _ConditionExtra
@@ -696,7 +663,7 @@ def _parse_when(
696663
**constraints: _FieldEqualType,
697664
) -> _ConditionType:
698665
composed: _PredicateType
699-
if _is_undefined(predicate):
666+
if utils.is_undefined(predicate):
700667
if more_predicates or constraints:
701668
composed = _parse_when_compose(more_predicates, constraints)
702669
else:
@@ -1106,7 +1073,7 @@ def param(
11061073
empty_remap = {"none": False, "all": True}
11071074
parameter = Parameter(name)
11081075

1109-
if not _is_undefined(empty):
1076+
if not utils.is_undefined(empty):
11101077
if isinstance(empty, bool) and not isinstance(empty, str):
11111078
parameter.empty = empty
11121079
elif empty in empty_remap:
@@ -1624,7 +1591,7 @@ def to_dict(
16241591

16251592
copy = _top_schema_base(self).copy(deep=False)
16261593
original_data = getattr(copy, "data", Undefined)
1627-
if not _is_undefined(original_data):
1594+
if not utils.is_undefined(original_data):
16281595
try:
16291596
data = _to_eager_narwhals_dataframe(original_data)
16301597
except TypeError:

altair/vegalite/v5/schema/_typing.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44

55
from __future__ import annotations
66

7-
from typing import Any, Literal, Mapping
7+
from typing import Any, Literal, Mapping, Sequence, TypeVar, Union
88

9-
from typing_extensions import TypeAlias
9+
from typing_extensions import TypeAlias, TypeAliasType
1010

1111
__all__ = [
1212
"AggregateOp_T",
@@ -32,6 +32,7 @@
3232
"Mark_T",
3333
"MultiTimeUnit_T",
3434
"NonArgAggregateOp_T",
35+
"OneOrSeq",
3536
"Orient_T",
3637
"Orientation_T",
3738
"ProjectionType_T",
@@ -60,6 +61,22 @@
6061
]
6162

6263

64+
T = TypeVar("T")
65+
OneOrSeq = TypeAliasType("OneOrSeq", Union[T, Sequence[T]], type_params=(T,))
66+
"""One of ``T`` specified type(s), or a `Sequence` of such.
67+
68+
Examples
69+
--------
70+
The parameters ``short``, ``long`` accept the same range of types::
71+
72+
# ruff: noqa: UP006, UP007
73+
74+
def func(
75+
short: OneOrSeq[str | bool | float],
76+
long: Union[str, bool, float, Sequence[Union[str, bool, float]],
77+
): ...
78+
"""
79+
6380
Map: TypeAlias = Mapping[str, Any]
6481
AggregateOp_T: TypeAlias = Literal[
6582
"argmax",

tools/generate_schema_wrapper.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,26 @@ def encode({encode_method_args}) -> Self:
232232
return copy
233233
'''
234234

235+
# NOTE: Not yet reasonable to generalize `TypeAliasType`, `TypeVar`
236+
# Revisit if this starts to become more common
237+
TYPING_EXTRA: Final = '''
238+
T = TypeVar("T")
239+
OneOrSeq = TypeAliasType("OneOrSeq", Union[T, Sequence[T]], type_params=(T,))
240+
"""One of ``T`` specified type(s), or a `Sequence` of such.
241+
242+
Examples
243+
--------
244+
The parameters ``short``, ``long`` accept the same range of types::
245+
246+
# ruff: noqa: UP006, UP007
247+
248+
def func(
249+
short: OneOrSeq[str | bool | float],
250+
long: Union[str, bool, float, Sequence[Union[str, bool, float]],
251+
): ...
252+
"""
253+
'''
254+
235255

236256
class SchemaGenerator(codegen.SchemaGenerator):
237257
schema_class_template = textwrap.dedent(
@@ -815,7 +835,9 @@ def vegalite_main(skip_download: bool = False) -> None:
815835
)
816836
print(msg)
817837
TypeAliasTracer.update_aliases(("Map", "Mapping[str, Any]"))
818-
TypeAliasTracer.write_module(fp_typing, header=HEADER)
838+
TypeAliasTracer.write_module(
839+
fp_typing, "OneOrSeq", header=HEADER, extra=TYPING_EXTRA
840+
)
819841
# Write the pre-generated modules
820842
for fp, contents in files.items():
821843
print(f"Writing\n {schemafile!s}\n ->{fp!s}")

tools/schemapi/schemapi.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -785,6 +785,19 @@ def func_2(
785785
"""
786786

787787

788+
def is_undefined(obj: Any) -> TypeIs[UndefinedType]:
789+
"""Type-safe singleton check for `UndefinedType`.
790+
791+
Notes
792+
-----
793+
- Using `obj is Undefined` does not narrow from `UndefinedType` in a union.
794+
- Due to the assumption that other `UndefinedType`'s could exist.
795+
- Current [typing spec advises](https://typing.readthedocs.io/en/latest/spec/concepts.html#support-for-singleton-types-in-unions) using an `Enum`.
796+
- Otherwise, requires an explicit guard to inform the type checker.
797+
"""
798+
return obj is Undefined
799+
800+
788801
class SchemaBase:
789802
"""Base class for schema wrappers.
790803

tools/schemapi/utils.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,8 @@ def __init__(
7171
self._aliases: dict[str, str] = {}
7272
self._imports: Sequence[str] = (
7373
"from __future__ import annotations\n",
74-
"from typing import Literal, Mapping, Any",
75-
"from typing_extensions import TypeAlias",
74+
"from typing import Any, Literal, Mapping, TypeVar, Sequence, Union",
75+
"from typing_extensions import TypeAlias, TypeAliasType",
7676
)
7777
self._cmd_check: list[str] = ["--fix"]
7878
self._cmd_format: Sequence[str] = ruff_format or ()
@@ -141,28 +141,31 @@ def is_cached(self, tp: str, /) -> bool:
141141
return tp in self._literals_invert or tp in self._literals
142142

143143
def write_module(
144-
self, fp: Path, *extra_imports: str, header: LiteralString
144+
self, fp: Path, *extra_all: str, header: LiteralString, extra: LiteralString
145145
) -> None:
146146
"""Write all collected `TypeAlias`'s to `fp`.
147147
148148
Parameters
149149
----------
150150
fp
151151
Path to new module.
152-
*extra_imports
153-
Follows `self._imports` block.
152+
*extra_all
153+
Any manually spelled types to be exported.
154154
header
155155
`tools.generate_schema_wrapper.HEADER`.
156+
extra
157+
`tools.generate_schema_wrapper.TYPING_EXTRA`.
156158
"""
157159
ruff_format = ["ruff", "format", fp]
158160
if self._cmd_format:
159161
ruff_format.extend(self._cmd_format)
160162
commands = (["ruff", "check", fp, *self._cmd_check], ruff_format)
161-
static = (header, "\n", *self._imports, *extra_imports, "\n\n")
163+
static = (header, "\n", *self._imports, "\n\n")
162164
self.update_aliases(*sorted(self._literals.items(), key=itemgetter(0)))
165+
all_ = [*iter(self._aliases), *extra_all]
163166
it = chain(
164167
static,
165-
[f"__all__ = {list(self._aliases)}", "\n\n"],
168+
[f"__all__ = {all_}", "\n\n", extra],
166169
self.generate_aliases(),
167170
)
168171
fp.write_text("\n".join(it), encoding="utf-8")

0 commit comments

Comments
 (0)