Skip to content

Commit

Permalink
fix: have type_is_optional detect Py3.10 'type | None' syntax
Browse files Browse the repository at this point in the history
  • Loading branch information
Jeremy Silver committed Apr 15, 2024
1 parent 0865d3b commit 06d1fb1
Show file tree
Hide file tree
Showing 6 changed files with 40 additions and 14 deletions.
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ repos:
hooks:
- id: vermin-all
# specify your target version here, OR in a Vermin config file as usual:
args: ['-t=3.8-', '--no-tips', '--violations', '--exclude', 'enum.StrEnum', '.']
args: ['-t=3.8-', '--no-tips', '--violations', '--exclude', 'enum.StrEnum', '--exclude', 'types.UnionType', '.']
- repo: https://github.com/commitizen-tools/commitizen
rev: v3.20.0
hooks:
Expand Down
21 changes: 18 additions & 3 deletions TODO.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,30 @@
# TODO

## v0.4.0
## v0.3.1

- `ConfigDataclass`: rename `configure` to `as_config`?
- Github Actions for automated testing
- Coverage badge
- Special branch prefix to not run pipeline
- Badges (shields.io)
- Version
- Coverage
- CI status
- Read the Docs
- PyPI version
- Auto-publish when new tag is pushed (see: https://pypi.org/manage/project/fancy-dataclass/settings/publishing/)
- Require tag to match version?
- `DictConfig` subclass of `Config` (unstructured configs loaded from JSON/TOML)
- Do "hatch version" and check if it's a prefix of "git describe --tags" or matches "git describe --tags --abbrev=0"
- PyPI Links
- Changelog
- Release
- CHANGELOG update
- pre-push hook to ensure it contains an entry for the latest tag
- Tag new version

## v0.4.0

- `DictConfig` subclass of `Config` (unstructured configs loaded from JSON/TOML)

## v0.4.1

- documentation
Expand Down Expand Up @@ -51,6 +65,7 @@
- PEP 712 (`converter` argument for dataclass fields)
- Allow `Annotated` as an alternative to `field.metadata`
- Esp. with `Doc`: this could auto-populate JSON schema, argparse description
- Tiered configs?
- Improve `mkdocs` documentation
- Auto-generate per-field descriptions via PEP 727?
- Auto-generate dataclass field docs?
Expand Down
6 changes: 1 addition & 5 deletions fancy_dataclass/dict.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from typing_extensions import Self, _AnnotatedAlias

from fancy_dataclass.mixin import DataclassMixin, DataclassMixinSettings, FieldSettings
from fancy_dataclass.utils import TypeConversionError, _flatten_dataclass, check_dataclass, fully_qualified_class_name, issubclass_safe, obj_class_name, safe_dict_insert, type_is_optional
from fancy_dataclass.utils import TypeConversionError, _flatten_dataclass, check_dataclass, fully_qualified_class_name, issubclass_safe, obj_class_name, safe_dict_insert


if TYPE_CHECKING:
Expand Down Expand Up @@ -250,10 +250,6 @@ def err() -> TypeConversionError:
return tuple(convert_val(subtype, elt) for elt in val)
return tuple(convert_val(subtype, elt) for (subtype, elt) in zip(args, val))
elif origin_type == Union:
if type_is_optional(tp):
assert len(args) == 2
assert args[1] is type(None)
args = args[::-1] # check None first
for subtype in args:
try:
# NB: will resolve to the first valid type in the Union
Expand Down
6 changes: 5 additions & 1 deletion fancy_dataclass/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import importlib
from pathlib import Path
import re
import types
from typing import TYPE_CHECKING, Any, BinaryIO, Callable, Dict, ForwardRef, Generic, Iterable, Iterator, List, Optional, Sequence, Set, TextIO, Tuple, Type, TypeVar, Union, get_args, get_origin, get_type_hints

from typing_extensions import TypeGuard
Expand Down Expand Up @@ -55,7 +56,10 @@ def type_is_optional(tp: type) -> bool:
True if the type is Optional"""
origin_type = get_origin(tp)
args = get_args(tp)
return (origin_type == Union) and (len(args) == 2) and (type(None) in args)
union_types: List[Any] = [Union]
if hasattr(types, 'UnionType'): # Python >= 3.10
union_types.append(types.UnionType)
return (origin_type in union_types) and (type(None) in args)

def safe_dict_insert(d: Dict[Any, Any], key: str, val: Any) -> None:
"""Inserts a (key, value) pair into a dict, if the key is not already present.
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ build-backend = "hatchling.build"
[project]
name = "fancy-dataclass"
dynamic = ["version"]
description = "Spiff up your dataclasses with additional features."
description = "Spiff up your dataclasses with extra features."
readme = "docs/README.md"
requires-python = ">=3.8"
license = "MIT"
Expand Down Expand Up @@ -62,7 +62,7 @@ dependencies = [

[tool.hatch.envs.lint.scripts]
run-ruff = "ruff check"
run-vermin = "vermin {args:-t=3.8- --eval-annotations --no-tips --violations --exclude 'tests.test_serializable.StrEnum' fancy_dataclass}"
run-vermin = "vermin {args:-t=3.8- --eval-annotations --no-tips --violations --exclude enum.StrEnum --exclude types.UnionType .}"
run-mypy = "mypy --install-types --non-interactive {args:fancy_dataclass tests}"
run-loc-summary = "./summarize.sh"
all = ["run-ruff", "run-vermin", "run-mypy", "run-loc-summary"]
Expand Down
15 changes: 13 additions & 2 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from dataclasses import dataclass, is_dataclass
import sys
from typing import Any, ClassVar, Dict, List, Optional, Sequence, Union

import pytest
Expand All @@ -9,16 +10,26 @@

@pytest.mark.parametrize(['tp', 'is_optional'], [
(int, False),
(type(None), False),
(Optional[int], True),
(Optional[Optional[int]], True),
(Union[int, Optional[float]], False),
(Union[Optional[float], int], False),
(Union[int, Optional[float]], True),
(Union[Optional[float], int], True),
(Optional[type(None)], False), # Optional[NoneType] -> NoneType
(List[Optional[int]], False),
])
def test_type_is_optional(tp, is_optional):
assert type_is_optional(tp) == is_optional

@pytest.mark.skipif(sys.version_info[:2] < (3, 10), reason='Py3.10 union type')
def test_type_is_optional_py39():
assert type_is_optional(int | None)
assert type_is_optional(None | int)
assert type_is_optional(None | int | str)
assert type_is_optional(int | str | None)
assert type_is_optional(Optional[int | str])
assert type_is_optional(Optional[int] | str)

@pytest.mark.parametrize(['obj', 'tp', 'output'], [
(1, int, True),
('a', int, False),
Expand Down

0 comments on commit 06d1fb1

Please sign in to comment.