diff --git a/.github/actions/setup-uv/action.yml b/.github/actions/setup-uv/action.yml index a4f3f6d..5b2734e 100644 --- a/.github/actions/setup-uv/action.yml +++ b/.github/actions/setup-uv/action.yml @@ -3,8 +3,9 @@ runs: using: 'composite' steps: - name: Install uv - uses: astral-sh/setup-uv@v3 + uses: astral-sh/setup-uv@v4 with: - version: "0.5.1" + version: "0.5.10" enable-cache: true cache-dependency-glob: "**/pyproject.toml" + python-version: ${{ matrix.python-version }} diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 31b799e..43433d0 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -12,14 +12,13 @@ jobs: strategy: fail-fast: false matrix: - python-version: [3.9, "3.10", "3.11", "3.12", "3.13"] + python-version: ["3.10", "3.11", "3.12", "3.13"] steps: - uses: actions/checkout@v4 - name: Setup uv + # Installs Python based on ${{ matrix.python-version }} uses: ./.github/actions/setup-uv - - name: Set up Python ${{ matrix.python-version }} - run: uv python install ${{ matrix.python-version }} - name: Lint check run: make lint - name: Unit tests diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5d5f10e..bb82585 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.6.9 + rev: v0.8.3 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] @@ -13,7 +13,7 @@ repos: - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.12.0 + rev: v1.13.0 hooks: - id: mypy args: [--strict] diff --git a/coqpit/coqpit.py b/coqpit/coqpit.py index 60bd5b0..9d8866c 100644 --- a/coqpit/coqpit.py +++ b/coqpit/coqpit.py @@ -6,23 +6,16 @@ import contextlib import json import operator -import sys import typing -from collections.abc import ItemsView, Iterable, Iterator, MutableMapping +from collections.abc import Callable, ItemsView, Iterable, Iterator, MutableMapping from dataclasses import MISSING as _MISSING from dataclasses import Field, asdict, dataclass, fields, is_dataclass, replace from pathlib import Path from pprint import pprint -from types import GenericAlias -from typing import TYPE_CHECKING, Any, Callable, Generic, Literal, TypeVar, Union, overload +from types import GenericAlias, UnionType +from typing import TYPE_CHECKING, Any, Generic, Literal, TypeAlias, TypeGuard, TypeVar, Union, overload -from typing_extensions import Self, TypeAlias, TypeGuard, TypeIs - -# TODO: Available from Python 3.10 -if sys.version_info >= (3, 10): - from types import UnionType -else: - UnionType: TypeAlias = Union +from typing_extensions import Self, TypeIs if TYPE_CHECKING: # pragma: no cover import os @@ -38,7 +31,7 @@ class _NoDefault(Generic[_T]): pass -NoDefaultVar: TypeAlias = Union[_NoDefault[_T], _T] +NoDefaultVar: TypeAlias = _NoDefault[_T] | _T no_default: NoDefaultVar[Any] = _NoDefault() FieldType: TypeAlias = Union[str, type, "UnionType"] @@ -90,10 +83,7 @@ def _is_union(field_type: FieldType) -> TypeIs[UnionType]: bool: True if input type is `Union` """ origin = typing.get_origin(field_type) - is_union = origin is Union - if sys.version_info >= (3, 10): - is_union = is_union or origin is UnionType - return is_union + return origin is Union or origin is UnionType def _is_union_and_not_simple_optional(field_type: FieldType) -> TypeGuard[UnionType]: @@ -262,13 +252,13 @@ def _deserialize_primitive_types( Returns: Union[int, float, str, bool]: deserialized value. """ - if isinstance(x, (str, bool)): + if isinstance(x, str | bool): return x - if isinstance(x, (int, float)): + if isinstance(x, int | float): base_type = _drop_none_type(field_type) if base_type is not float and base_type is not int and base_type is not str and base_type is not bool: raise TypeError - base_type = typing.cast(type[Union[int, float, str, bool]], base_type) + base_type = typing.cast(type[int | float | str | bool], base_type) if x == float("inf") or x == float("-inf"): # if value type is inf return regardless. return x @@ -315,7 +305,7 @@ def _deserialize(x: Any, field_type: FieldType) -> Any: CoqpitType: TypeAlias = MutableMapping[str, "CoqpitNestedValue"] CoqpitNestedValue: TypeAlias = Union["CoqpitValue", CoqpitType] -CoqpitValue: TypeAlias = Union[str, int, float, bool, None] +CoqpitValue: TypeAlias = str | int | float | bool | None # TODO: It should be possible to get rid of the next 3 `type: ignore`. At diff --git a/pyproject.toml b/pyproject.toml index 479f7eb..b457ea5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,10 +4,10 @@ build-backend = "hatchling.build" [project] name = "coqpit-config" -version = "0.1.1" +version = "0.1.2" description = "Simple (maybe too simple), light-weight config management through python data-classes." readme = "README.md" -requires-python = ">=3.9" +requires-python = ">=3.10" license = {text = "MIT"} authors = [ {name = "Eren Gölge", email = "egolge@coqui.ai"} @@ -18,7 +18,6 @@ maintainers = [ classifiers = [ "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", @@ -36,10 +35,10 @@ dependencies = [ [dependency-groups] dev = [ "coverage>=7", - "mypy>=1.12.0", - "pre-commit>=3", + "mypy>=1.13.0", + "pre-commit>=4", "pytest>=8", - "ruff==0.6.9", + "ruff==0.8.3", ] [project.urls] @@ -59,7 +58,6 @@ exclude = [ packages = ["coqpit"] [tool.ruff] -target-version = "py39" line-length = 120 lint.select = ["ALL"] lint.ignore = [ diff --git a/tests/test_init_from_dict.py b/tests/test_init_from_dict.py index 1e0a735..67ebf90 100644 --- a/tests/test_init_from_dict.py +++ b/tests/test_init_from_dict.py @@ -1,5 +1,4 @@ from dataclasses import dataclass, field -from typing import Optional import pytest @@ -8,8 +7,8 @@ @dataclass class Person(Coqpit): - name: Optional[str] = None - age: Optional[int] = None + name: str | None = None + age: int | None = None @dataclass diff --git a/tests/test_merge_configs.py b/tests/test_merge_configs.py index f5e1ae4..b5c7673 100644 --- a/tests/test_merge_configs.py +++ b/tests/test_merge_configs.py @@ -1,5 +1,4 @@ from dataclasses import dataclass -from typing import Optional from coqpit.coqpit import Coqpit @@ -7,7 +6,7 @@ @dataclass class CoqpitA(Coqpit): val_a: int = 10 - val_b: Optional[int] = None + val_b: int | None = None val_c: str = "Coqpit is great!" val_same: float = 10.21 @@ -23,7 +22,7 @@ class CoqpitB(Coqpit): @dataclass class Reference(Coqpit): val_a: int = 10 - val_b: Optional[int] = None + val_b: int | None = None val_c: str = "Coqpit is great!" val_e: int = 257 val_f: float = -10.21 diff --git a/tests/test_nested_configs.py b/tests/test_nested_configs.py index 4117fe6..5707462 100644 --- a/tests/test_nested_configs.py +++ b/tests/test_nested_configs.py @@ -1,6 +1,5 @@ from dataclasses import asdict, dataclass, field from pathlib import Path -from typing import Optional, Union from coqpit import Coqpit, check_argument @@ -8,7 +7,7 @@ @dataclass class SimpleConfig(Coqpit): val_a: int = 10 - val_b: Optional[int] = None + val_b: int | None = None val_c: str = "Coqpit is great!" def check_values(self) -> None: @@ -22,11 +21,11 @@ def check_values(self) -> None: @dataclass class NestedConfig(Coqpit): val_d: int = 10 - val_e: Optional[int] = None + val_e: int | None = None val_f: str = "Coqpit is great!" - sc_list: Optional[list[SimpleConfig]] = None + sc_list: list[SimpleConfig] | None = None sc: SimpleConfig = field(default_factory=lambda: SimpleConfig()) - union_var: Union[list[SimpleConfig], SimpleConfig] = field(default_factory=lambda: [SimpleConfig(), SimpleConfig()]) + union_var: list[SimpleConfig] | SimpleConfig = field(default_factory=lambda: [SimpleConfig(), SimpleConfig()]) def check_values(self) -> None: """Check config fields""" diff --git a/tests/test_parse_argparse.py b/tests/test_parse_argparse.py index ec7d137..c0f843b 100644 --- a/tests/test_parse_argparse.py +++ b/tests/test_parse_argparse.py @@ -1,18 +1,17 @@ from dataclasses import asdict, dataclass, field -from typing import Optional from coqpit.coqpit import Coqpit, check_argument @dataclass class SimplerConfig(Coqpit): - val_a: Optional[int] = field(default=None, metadata={"help": "this is val_a"}) + val_a: int | None = field(default=None, metadata={"help": "this is val_a"}) @dataclass class SimpleConfig(Coqpit): val_a: int = field(default=10, metadata={"help": "this is val_a of SimpleConfig"}) - val_b: Optional[int] = field(default=None, metadata={"help": "this is val_b"}) + val_b: int | None = field(default=None, metadata={"help": "this is val_b"}) val_c: str = "Coqpit is great!" val_dict: dict[str, int] = field(default_factory=lambda: {"val_a": 100, "val_b": 200, "val_c": 300}) mylist_with_default: list[SimplerConfig] = field( @@ -21,8 +20,8 @@ class SimpleConfig(Coqpit): ) int_list: list[int] = field(default_factory=lambda: [1, 2, 3], metadata={"help": "int"}) str_list: list[str] = field(default_factory=lambda: ["veni", "vidi", "vici"], metadata={"help": "str"}) - empty_int_list: Optional[list[int]] = field(default=None, metadata={"help": "int list without default value"}) - empty_str_list: Optional[list[str]] = field(default=None, metadata={"help": "str list without default value"}) + empty_int_list: list[int] | None = field(default=None, metadata={"help": "int list without default value"}) + empty_str_list: list[str] | None = field(default=None, metadata={"help": "str list without default value"}) list_with_default_factory: list[str] = field( default_factory=list, metadata={"help": "str list with default factory"}, @@ -140,13 +139,13 @@ def test_argparse_with_required_field() -> None: def test_init_argparse_list_and_nested() -> None: @dataclass class SimplerConfig2(Coqpit): - val_a: Optional[int] = field(default=None, metadata={"help": "this is val_a"}) + val_a: int | None = field(default=None, metadata={"help": "this is val_a"}) @dataclass class SimpleConfig2(Coqpit): val_req: str # required field val_a: int = field(default=10, metadata={"help": "this is val_a of SimpleConfig2"}) - val_b: Optional[int] = field(default=None, metadata={"help": "this is val_b"}) + val_b: int | None = field(default=None, metadata={"help": "this is val_b"}) nested_config: SimplerConfig2 = field(default_factory=lambda: SimplerConfig2()) mylist_with_default: list[SimplerConfig2] = field( default_factory=lambda: [SimplerConfig2(val_a=100), SimplerConfig2(val_a=999)], diff --git a/tests/test_parse_known_argparse.py b/tests/test_parse_known_argparse.py index a1b3218..5ca5142 100644 --- a/tests/test_parse_known_argparse.py +++ b/tests/test_parse_known_argparse.py @@ -1,18 +1,17 @@ from dataclasses import asdict, dataclass, field -from typing import Optional from coqpit.coqpit import Coqpit, check_argument @dataclass class SimplerConfig(Coqpit): - val_a: Optional[int] = field(default=None, metadata={"help": "this is val_a"}) + val_a: int | None = field(default=None, metadata={"help": "this is val_a"}) @dataclass class SimpleConfig(Coqpit): val_a: int = field(default=10, metadata={"help": "this is val_a of SimpleConfig"}) - val_b: Optional[int] = field(default=None, metadata={"help": "this is val_b"}) + val_b: int | None = field(default=None, metadata={"help": "this is val_b"}) val_c: str = "Coqpit is great!" mylist_with_default: list[SimplerConfig] = field( default_factory=lambda: [SimplerConfig(val_a=100), SimplerConfig(val_a=999)], diff --git a/tests/test_relaxed_parse_known_argparse.py b/tests/test_relaxed_parse_known_argparse.py index 9d096b2..0365fa7 100644 --- a/tests/test_relaxed_parse_known_argparse.py +++ b/tests/test_relaxed_parse_known_argparse.py @@ -1,5 +1,5 @@ from dataclasses import asdict, dataclass, field -from typing import Any, Optional, Union +from typing import Any from coqpit.coqpit import Coqpit, check_argument @@ -7,9 +7,9 @@ @dataclass class SimpleConfig(Coqpit): val_a: int = field(default=10, metadata={"help": "this is val_a of SimpleConfig"}) - val_b: Optional[int] = field(default=None, metadata={"help": "this is val_b"}) - val_c: Optional[Union[int, str]] = None - val_d: Optional[list[list[Any]]] = None + val_b: int | None = field(default=None, metadata={"help": "this is val_b"}) + val_c: int | str | None = None + val_d: list[list[Any]] | None = None def check_values(self) -> None: """Check config fields""" diff --git a/tests/test_serialization.py b/tests/test_serialization.py index 9cd7d2b..4470c60 100644 --- a/tests/test_serialization.py +++ b/tests/test_serialization.py @@ -1,6 +1,5 @@ from dataclasses import dataclass, field from pathlib import Path -from typing import Optional, Union import pytest @@ -9,23 +8,23 @@ @dataclass class Person(Coqpit): - name: Optional[str] = None - age: Optional[int] = None + name: str | None = None + age: int | None = None @dataclass class Group(Coqpit): - name: Optional[str] = None - size: Optional[int] = None - path: Optional[Path] = None + name: str | None = None + size: int | None = None + path: Path | None = None people: list[Person] = field(default_factory=list) - some_dict: dict[str, Optional[int]] = field(default_factory=dict) + some_dict: dict[str, int | None] = field(default_factory=dict) @dataclass class Reference(Coqpit): - name: Optional[str] = "Coqpit" - size: Optional[int] = 3 + name: str | None = "Coqpit" + size: int | None = 3 path: Path = Path("a/b") people: list[Person] = field( default_factory=lambda: [ @@ -34,7 +33,7 @@ class Reference(Coqpit): Person(name="Ceren", age=15), ], ) - some_dict: dict[str, Optional[int]] = field(default_factory=lambda: {"a": 1, "b": 2, "c": None}) + some_dict: dict[str, int | None] = field(default_factory=lambda: {"a": 1, "b": 2, "c": None}) def test_serialization() -> None: @@ -70,8 +69,6 @@ def test_deserialize_list() -> None: assert _deserialize_list([1, 2, 3], list[str]) == ["1", "2", "3"] -# TODO: `type: ignore` can probably be removed when switching to Python 3.10 -# Union syntax (e.g. str | int) def test_deserialize_primitive_type() -> None: cases = ( (True, bool, True), @@ -83,21 +80,21 @@ def test_deserialize_primitive_type() -> None: (3, str, "3"), (3.0, str, "3.0"), (3, bool, True), - ("a", Union[str, None], "a"), - ("3", Union[str, None], "3"), - (3, Union[int, None], 3), - (3, Union[float, None], 3.0), - (None, Union[str, None], None), - (None, Union[int, None], None), - (None, Union[float, None], None), - (None, Union[str, None], None), + ("a", str | None, "a"), + ("3", str | None, "3"), + (3, int | None, 3), + (3, float | None, 3.0), + (None, str | None, None), + (None, int | None, None), + (None, float | None, None), + (None, str | None, None), (float("inf"), float, float("inf")), (float("inf"), int, float("inf")), (float("-inf"), float, float("-inf")), (float("-inf"), int, float("-inf")), ) for value, field_type, expected in cases: - assert _deserialize_primitive_types(value, field_type) == expected # type: ignore[arg-type] + assert _deserialize_primitive_types(value, field_type) == expected with pytest.raises(TypeError): _deserialize_primitive_types(3, Coqpit) diff --git a/tests/test_simple_config.py b/tests/test_simple_config.py index cc2d17a..f74e7ec 100644 --- a/tests/test_simple_config.py +++ b/tests/test_simple_config.py @@ -1,6 +1,6 @@ from dataclasses import asdict, dataclass, field from pathlib import Path -from typing import Any, Optional, Union +from typing import Any from coqpit.coqpit import MISSING, Coqpit, check_argument @@ -8,7 +8,7 @@ @dataclass class SimpleConfig(Coqpit): val_a: int = 10 - val_b: Optional[int] = None + val_b: int | None = None val_d: float = 10.21 val_c: str = "Coqpit is great!" vol_e: bool = True @@ -19,7 +19,7 @@ class SimpleConfig(Coqpit): val_dict: dict[str, Any] = field(default_factory=lambda: {"val_aa": 10, "val_ss": "This is in a dict."}) # list of list val_listoflist: list[list[int]] = field(default_factory=lambda: [[1, 2], [3, 4]]) - val_listofunion: list[list[Union[str, int, bool]]] = field( + val_listofunion: list[list[str | int | bool]] = field( default_factory=lambda: [[1, 3], [1, "Hi!"], [True, False]], ) diff --git a/tests/test_utils.py b/tests/test_utils.py index 7d7de34..6e44a11 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,44 +1,39 @@ -from typing import Union - from coqpit.coqpit import _is_optional_field, _is_union, _is_union_and_not_simple_optional -# TODO: `type: ignore` can probably be removed when switching to Python 3.10 -# Union syntax (e.g. str | int) - def test_is_union() -> None: cases = ( - (Union[str, int], True), - (Union[str, None], True), + (str | int, True), + (str | None, True), (int, False), (list[int], False), - (list[Union[str, int]], False), + (list[str | int], False), ) for item, expected in cases: - assert _is_union(item) == expected # type: ignore[arg-type] + assert _is_union(item) == expected def test_is_union_and_not_simple_optional() -> None: cases = ( - (Union[str, int], True), - (Union[str, None], False), - (Union[list[int], None], False), + (str | int, True), + (str | None, False), + (list[int] | None, False), (int, False), (list[int], False), - (list[Union[str, int]], False), + (list[str | int], False), ) for item, expected in cases: - assert _is_union_and_not_simple_optional(item) == expected # type: ignore[arg-type] + assert _is_union_and_not_simple_optional(item) == expected def test_is_optional_field() -> None: cases = ( - (Union[str, int], False), - (Union[str, None], True), - (Union[list[int], None], True), + (str | int, False), + (str | None, True), + (list[int] | None, True), (int, False), (list[int], False), - (list[Union[str, int]], False), + (list[str | int], False), ) for item, expected in cases: - assert _is_optional_field(item) == expected # type: ignore[arg-type] + assert _is_optional_field(item) == expected