diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cf12c04f3..698ece657 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -23,7 +23,7 @@ jobs: - name: Install uv uses: astral-sh/setup-uv@v7 - # Install dependencies from pyproject.toml (latest versions for all matrix jobs) + # Install dependencies from pyproject.toml (latest versions for all matrix jobs) - name: Install dependencies run: uv pip install -e ".[dev, all]" @@ -32,7 +32,7 @@ jobs: if: ${{ matrix.required-dependencies == 'minimum' }} run: | # Keep in sync with versions in `pyproject.toml` - uv pip install jsonschema==3.0 narwhals==1.27.1 typing_extensions==4.12.0 + uv pip install jsonschema==3.0 narwhals==1.27.1 typing_extensions==4.12.0 setuptools==81.0.0 - name: Maybe uninstall optional dependencies # We uninstall pyarrow and vegafusion for one job to test that we have not # accidentally introduced a hard dependency on these libraries. diff --git a/altair/vegalite/v6/api.py b/altair/vegalite/v6/api.py index f755dd7a0..83bf71e2a 100644 --- a/altair/vegalite/v6/api.py +++ b/altair/vegalite/v6/api.py @@ -133,7 +133,6 @@ InlineDataset, IntervalSelectionConfig, JoinAggregateFieldDef, - LayerRepeatMapping, LookupSelection, NamedData, ParameterName, @@ -155,6 +154,8 @@ WindowFieldDef, ) +from .schema.core import LayerRepeatMapping + __all__ = [ "TOPLEVEL_ONLY_KEYS", "Bin", @@ -212,7 +213,7 @@ # ------------------------------------------------------------------------ # Data Utilities -def _dataset_name(values: dict[str, Any] | list | InlineDataset) -> str: +def _dataset_name(values: dict[str, Any] | list[str] | InlineDataset) -> str: """ Generate a unique hash of the data. @@ -344,7 +345,7 @@ def __init__( row: Optional[str | FacetFieldDef | Row] = Undefined, **kwargs: Any, ) -> None: - super().__init__(column=column, row=row, **kwargs) # type: ignore[arg-type] + super().__init__(column=column, row=row, **kwargs) # type: ignore def to_dict(self, *args: Any, **kwargs: Any) -> dict[str, Any]: copy = self.copy(deep=False) @@ -695,13 +696,13 @@ def _condition_to_selection( if isinstance(if_false, str): if_false = utils.parse_shorthand(if_false) if_false.update(kwargs) - selection = _Conditional(condition=cond_mutable, **if_false) # type: ignore[typeddict-item] + selection = _Conditional(condition=cond_mutable, **if_false) # type: ignore else: raise TypeError(if_false) return selection -class _ConditionExtra(TypedDict, closed=True, total=False): # type: ignore[call-arg] +class _ConditionExtra(TypedDict, closed=True, total=False): # type: ignore # https://peps.python.org/pep-0728/ # Likely a Field predicate empty: Optional[bool] @@ -727,7 +728,7 @@ class _ConditionExtra(TypedDict, closed=True, total=False): # type: ignore[call """ -class _ConditionClosed(TypedDict, closed=True, total=False): # type: ignore[call-arg] +class _ConditionClosed(TypedDict, closed=True, total=False): # type: ignore # https://peps.python.org/pep-0728/ # Parameter {"param", "value", "empty"} # Predicate {"test", "value"} @@ -776,7 +777,7 @@ class _Conditional(TypedDict, t.Generic[_C], total=False): """ -class _Value(TypedDict, closed=True, total=False): # type: ignore[call-arg] +class _Value(TypedDict, closed=True, total=False): # type: ignore # https://peps.python.org/pep-0728/ value: Required[Any] __extra_items__: Any @@ -920,7 +921,7 @@ def _parse_otherwise( selection: SchemaBase | _Conditional[Any] if isinstance(statement, SchemaBase): selection = statement.copy() - conditions.update(**kwds) # type: ignore[call-arg] + conditions.update(**kwds) # type: ignore selection.condition = conditions["condition"] else: if not isinstance(statement, Mapping): @@ -1399,7 +1400,7 @@ def when( def value(value: Any, **kwargs: Any) -> _Value: """Specify a value for use in an encoding.""" - return _Value(value=value, **kwargs) # type: ignore[typeddict-item] + return _Value(value=value, **kwargs) # type: ignore def _make_param_obj( @@ -1407,7 +1408,7 @@ def _make_param_obj( value: Optional[Any], bind: Optional[Binding], expr: Optional[str | Expr | Expression], - kwds: dict, + kwds: dict[str, Any], ) -> tuple[ VariableParameter | TopLevelSelectionParameter | SelectionParameter, Literal["variable", "selection"], @@ -1500,7 +1501,7 @@ def param( parameter.name = str(name) if parameter.param is not Undefined: param_obj = t.cast( - "Union[VariableParameter, TopLevelSelectionParameter, SelectionParameter]", + "VariableParameter | TopLevelSelectionParameter | SelectionParameter", parameter.param, ) param_obj.name = str(name) @@ -1511,7 +1512,7 @@ def param( parameter._name_is_hashed = True if parameter.param is not Undefined: param_obj = t.cast( - "Union[VariableParameter, TopLevelSelectionParameter, SelectionParameter]", + "VariableParameter | TopLevelSelectionParameter | SelectionParameter", parameter.param, ) param_obj.name = hash_name @@ -2219,8 +2220,8 @@ def to_html( self, base_url: str = "https://cdn.jsdelivr.net/npm", output_div: str = "vis", - embed_options: dict | None = None, - json_kwds: dict | None = None, + embed_options: dict[str, Any] | None = None, + json_kwds: dict[str, Any] | None = None, fullhtml: bool = True, requirejs: bool = False, inline: bool = False, @@ -3793,7 +3794,7 @@ def transform_window( # Display-related methods - def _repr_mimebundle_(self, *args, **kwds) -> MimeBundleType | None: # type:ignore[return] # noqa: ANN002, ANN003 + def _repr_mimebundle_(self, *args: Any, **kwds: Any) -> MimeBundleType | None: # type:ignore """Return a MIME bundle for display in Jupyter frontends.""" # Catch errors explicitly to get around issues in Jupyter frontend # see https://github.com/ipython/ipython/issues/11038 @@ -3810,7 +3811,7 @@ def display( self, renderer: Optional[Literal["canvas", "svg"]] = Undefined, theme: Optional[str] = Undefined, - actions: Optional[bool | dict] = Undefined, + actions: Optional[bool | dict[str, Any]] = Undefined, **kwargs: Any, ) -> None: """ @@ -4060,8 +4061,12 @@ def __init__( data: Optional[ChartDataType] = Undefined, encoding: Optional[FacetedEncoding] = Undefined, mark: Optional[AnyMark | Mark_T | CompositeMark_T] = Undefined, - width: Optional[float | dict | Step | Literal["container"]] = Undefined, - height: Optional[float | dict | Step | Literal["container"]] = Undefined, + width: Optional[ + float | dict[str, Any] | Step | Literal["container"] + ] = Undefined, + height: Optional[ + float | dict[str, Any] | Step | Literal["container"] + ] = Undefined, **kwargs: Any, ) -> None: super().__init__( @@ -4163,7 +4168,12 @@ def to_dict( - *Technical*: ``ignore`` will **not** be passed to child :meth:`.to_dict()`. """ context = context or {} - kwds: Map = {"validate": validate, "format": format, "ignore": ignore, "context": context} # fmt: skip + kwds: Map = { + "validate": validate, + "format": format, + "ignore": ignore, + "context": context, + } if self.data is Undefined and "data" not in context: # No data specified here or in parent: inject empty data # for easier specification of datum encodings. @@ -4321,26 +4331,26 @@ def __init__( self, repeat: Optional[list[str] | LayerRepeatMapping | RepeatMapping] = Undefined, spec: Optional[ChartType] = Undefined, - align: Optional[dict | SchemaBase | LayoutAlign_T] = Undefined, - autosize: Optional[dict | SchemaBase | AutosizeType_T] = Undefined, + align: Optional[dict[str, Any] | SchemaBase | LayoutAlign_T] = Undefined, + autosize: Optional[dict[str, Any] | SchemaBase | AutosizeType_T] = Undefined, background: Optional[ - str | dict | Parameter | SchemaBase | ColorName_T + str | dict[str, Any] | Parameter | SchemaBase | ColorName_T ] = Undefined, bounds: Optional[Literal["full", "flush"]] = Undefined, - center: Optional[bool | dict | SchemaBase] = Undefined, + center: Optional[bool | dict[str, Any] | SchemaBase] = Undefined, columns: Optional[int] = Undefined, - config: Optional[dict | SchemaBase] = Undefined, + config: Optional[dict[str, Any] | SchemaBase] = Undefined, data: Optional[ChartDataType] = Undefined, - datasets: Optional[dict | SchemaBase] = Undefined, + datasets: Optional[dict[str, Any] | SchemaBase] = Undefined, description: Optional[str] = Undefined, name: Optional[str] = Undefined, - padding: Optional[dict | float | Parameter | SchemaBase] = Undefined, + padding: Optional[dict[str, Any] | float | Parameter | SchemaBase] = Undefined, params: Optional[Sequence[_Parameter]] = Undefined, - resolve: Optional[dict | SchemaBase] = Undefined, - spacing: Optional[dict | float | SchemaBase] = Undefined, - title: Optional[str | dict | SchemaBase | Sequence[str]] = Undefined, - transform: Optional[Sequence[dict | SchemaBase]] = Undefined, - usermeta: Optional[dict | SchemaBase] = Undefined, + resolve: Optional[dict[str, Any] | SchemaBase] = Undefined, + spacing: Optional[dict[str, Any] | float | SchemaBase] = Undefined, + title: Optional[str | dict[str, Any] | SchemaBase | Sequence[str]] = Undefined, + transform: Optional[Sequence[dict[str, Any] | SchemaBase]] = Undefined, + usermeta: Optional[dict[str, Any] | SchemaBase] = Undefined, **kwds: Any, ) -> None: tp_name = type(self).__name__ @@ -4917,7 +4927,7 @@ def __init__( self, data: Optional[ChartDataType] = Undefined, spec: Optional[ChartType] = Undefined, - facet: Optional[dict | SchemaBase] = Undefined, + facet: Optional[dict[str, Any] | SchemaBase] = Undefined, params: Optional[Sequence[_Parameter]] = Undefined, **kwargs: Any, ) -> None: @@ -5199,13 +5209,13 @@ def _combine_subchart_params( # noqa: C901 def _get_repeat_strings( repeat: list[str] | LayerRepeatMapping | RepeatMapping, -) -> list[str] | list: +) -> list[str]: if isinstance(repeat, list): return repeat - elif isinstance(repeat, core.LayerRepeatMapping): + + klist = ["row", "column"] # RepeatMapping + if isinstance(repeat, LayerRepeatMapping): klist = ["row", "column", "layer"] - elif isinstance(repeat, core.RepeatMapping): - klist = ["row", "column"] rclist = [k for k in klist if repeat[k] is not Undefined] rcstrings = [[f"{k}_{v}" for v in repeat[k]] for k in rclist] retstr: list[str] = ["".join(s) for s in itertools.product(*rcstrings)] diff --git a/pyproject.toml b/pyproject.toml index fd61af12f..b84f939e7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,6 +6,7 @@ # - ruff # - pytest # - mypy +# - pyright (experimental) [build-system] requires = ["hatchling"] @@ -34,7 +35,7 @@ keywords = [ ] requires-python = ">=3.9" dynamic = ["version"] -license = {file = "LICENSE"} +license = { file = "LICENSE" } classifiers = [ "Development Status :: 5 - Production/Stable", "Environment :: Console", @@ -70,7 +71,7 @@ all = [ dev = [ "hatch>=1.13.0", "ruff>=0.9.5", - "duckdb>=1.0; python_version<\"3.14\"", + "duckdb>=1.0; python_version<'3.14'", "ipython", "ipykernel", "pandas>=1.1.3", @@ -83,7 +84,7 @@ dev = [ "pandas-stubs", "types-jsonschema", "types-setuptools", - "geopandas>=0.14.3; python_version<\"3.14\"", + "geopandas>=0.14.3; python_version<'3.14'", # Pin <1.38 until string_view/Utf8View breakage fixed (narwhals#3450, polars#26435) "polars>=0.20.3,<1.38", "taskipy>=1.14.1", @@ -101,20 +102,20 @@ doc = [ "sphinx_copybutton", "sphinx-design", "scipy", - "scipy-stubs; python_version>=\"3.10\"", + "scipy-stubs; python_version>='3.10'", ] + [dependency-groups] geospatial = [ "geopandas>=0.14.3", - "polars-st>=0.1.0a27 ; python_full_version >= '3.10'", + "polars-st>=0.1.0a27 ; python_full_version>='3.10'", ] - [tool.altair.vega] # Minimum/exact versions, for projects under the `vega` organization -vega-datasets = "v3.2.1" # https://github.com/vega/vega-datasets -vega-embed = "v7" # https://github.com/vega/vega-embed -vega-lite = "v6.1.0" # https://github.com/vega/vega-lite +vega-datasets = "v3.2.1" # https://github.com/vega/vega-datasets +vega-embed = "v7" # https://github.com/vega/vega-embed +vega-lite = "v6.1.0" # https://github.com/vega/vega-lite [tool.hatch] build = { include = ["/altair"], artifacts = ["altair/jupyter/js/index.js"] } @@ -144,7 +145,8 @@ line-length = 88 target-version = "py39" [tool.ruff.lint] -extend-safe-fixes = [ # https://docs.astral.sh/ruff/settings/#lint_extend-safe-fixes +extend-safe-fixes = [ + # https://docs.astral.sh/ruff/settings/#lint_extend-safe-fixes "ANN204", # missing-return-type-special-method "C405", # unnecessary-literal-set "C419", # unnecessary-comprehension-in-call @@ -162,7 +164,8 @@ extend-safe-fixes = [ # https://docs.astral.sh/ruff/settings/#lint_extend-safe-f "W291", # trailing-whitespace "W293", # blank line contains whitespace ] -extend-select = [ # https://docs.astral.sh/ruff/preview/#using-rules-that-are-in-preview +extend-select = [ + # https://docs.astral.sh/ruff/preview/#using-rules-that-are-in-preview "FURB", # refurb # "PLC2801", # unnecessary-dunder-call "PLR1733", # unnecessary-dict-index-lookup @@ -190,7 +193,7 @@ ignore = [ "W505", # doc-line-too-long ] mccabe.max-complexity = 10 -preview = false # https://docs.astral.sh/ruff/preview/ +preview = false # https://docs.astral.sh/ruff/preview/ pydocstyle.convention = "numpy" # https://docs.astral.sh/ruff/settings/#lintpydocstyle select = [ "ANN", # flake8-annotations @@ -282,7 +285,6 @@ test-datasets = "task ruff-fix && pytest tests -k test_datasets -m \"not no_xdis test-min = "task ruff-check && task type-check && hatch test --python 3.9" test-all = "task ruff-check && task type-check && hatch test --all" - generate-schema-wrapper = "mypy tools && python tools/generate_schema_wrapper.py && task test" update-init-file = "python tools/update_init_file.py && task ruff-fix" @@ -301,14 +303,17 @@ clean = "python -c \"import tools;tools.fs.rm('dist')\"" build = "task clean && uv build" publish = "task build && uv publish" -nightly-build = "python tools/generate_nightly_version.py --update && task build" +nightly-build = "python tools/generate_nightly_version.py --update && task build" nightly-release = "task nightly-build && echo 'Package built, ready for GitHub release'" [tool.pytest.ini_options] # Pytest does not need to search these folders for test functions. # They contain examples which are being executed by the # test_examples tests. -norecursedirs = ["tests/examples_arguments_syntax", "tests/examples_methods_syntax"] +norecursedirs = [ + "tests/examples_arguments_syntax", + "tests/examples_methods_syntax" +] addopts = [ "--numprocesses=logical", "--doctest-modules", @@ -353,22 +358,22 @@ module = [ disable_error_code = ["annotation-unchecked"] [tool.pyright] -enableExperimentalFeatures=true -extraPaths=["./tools"] -pythonPlatform="All" -pythonVersion="3.9" -reportTypedDictNotRequiredAccess="none" -reportIncompatibleMethodOverride="none" -reportUnusedExpression="none" -reportUnsupportedDunderAll="none" -include=[ +enableExperimentalFeatures = true +extraPaths = ["./tools"] +pythonPlatform = "All" +pythonVersion = "3.9" +reportTypedDictNotRequiredAccess = "none" +reportIncompatibleMethodOverride = "none" +reportUnusedExpression = "none" +reportUnsupportedDunderAll = "none" +include = [ "./altair/**/*.py", "./doc/*.py", - "./tests/**/*.py", - "./tools/**/*.py", + "./tests/**/*.py", + "./tools/**/*.py", "./sphinxext/**/*.py", ] -ignore=[ +ignore = [ "./altair/vegalite/v6/schema/channels.py", # 716 warns "./altair/vegalite/v6/schema/mixins.py", # 1001 warns "./altair/jupyter/", # Mostly untyped diff --git a/tests/vegalite/v6/schema/test_channels.py b/tests/vegalite/v6/schema/test_channels.py index fa46d9f85..06f463667 100644 --- a/tests/vegalite/v6/schema/test_channels.py +++ b/tests/vegalite/v6/schema/test_channels.py @@ -53,7 +53,7 @@ def test_channels_typing() -> None: assert angle.sort(dates_mixed).to_dict() # NOTE: Triggering static and runtime errors - invariant_sequence = angle.sort([*nums, *dates]) # type: ignore[arg-type] # pyright: ignore[reportCallIssue] + invariant_sequence = angle.sort([*nums, *dates]) # type: ignore with pytest.raises(SchemaValidationError): invariant_sequence.to_dict()