Skip to content

Commit e39c25f

Browse files
committed
Fix typing of LetterCase
Multiple issues prevented the letter_case argument on the dataclass_json decorator to work correctly with strict type checking settings and PyRight: * While config() etc. correctly accept also a callable instead of only an enum member, the dataclass_json decorator did not do this. Since the enum members are mapped to callables, which cannot be used as base types, they cannot be used as argument values of type LetterCase. * All functions in stringcase.py were missing type hints. * The LetterCase enum members need concrete type definitions, so that there arguments are sufficiently constrained and they are compatible with the correct callable type `Callable[[str], str]`. PyRight would otherwise infer `Callable[..., str]`, which is not narrow enough. These changes allow a JSON dataclass with letter_case argument to pass type testing with basedpyright. The code is fully backwards-compatible, as only type information is enhanced.
1 parent dc63902 commit e39c25f

File tree

3 files changed

+19
-16
lines changed

3 files changed

+19
-16
lines changed

dataclasses_json/api.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import json
33
from typing import Any, Callable, Dict, List, Optional, Tuple, Type, TypeVar, Union, overload
44

5-
from dataclasses_json.cfg import config, LetterCase
5+
from dataclasses_json.cfg import config, LetterCase, LetterCaseCallable
66
from dataclasses_json.core import (Json, _ExtendedEncoder, _asdict,
77
_decode_dataclass)
88
from dataclasses_json.mm import (JsonData, SchemaType, build_schema)
@@ -103,16 +103,16 @@ def schema(cls: Type[A],
103103

104104

105105
@overload
106-
def dataclass_json(_cls: None = ..., *, letter_case: Optional[LetterCase] = ...,
106+
def dataclass_json(_cls: None = ..., *, letter_case: Union[LetterCaseCallable, LetterCase, None] = ...,
107107
undefined: Optional[Union[str, Undefined]] = ...) -> Callable[[Type[T]], Type[T]]: ...
108108

109109

110110
@overload
111-
def dataclass_json(_cls: Type[T], *, letter_case: Optional[LetterCase] = ...,
111+
def dataclass_json(_cls: Type[T], *, letter_case: Union[LetterCaseCallable, LetterCase, None] = ...,
112112
undefined: Optional[Union[str, Undefined]] = ...) -> Type[T]: ...
113113

114114

115-
def dataclass_json(_cls: Optional[Type[T]] = None, *, letter_case: Optional[LetterCase] = None,
115+
def dataclass_json(_cls: Optional[Type[T]] = None, *, letter_case: Union[LetterCaseCallable, LetterCase, None] = None,
116116
undefined: Optional[Union[str, Undefined]] = None) -> Union[Callable[[Type[T]], Type[T]], Type[T]]:
117117
"""
118118
Based on the code in the `dataclasses` module to handle optional-parens
@@ -132,7 +132,7 @@ def wrap(cls: Type[T]) -> Type[T]:
132132
return wrap(_cls)
133133

134134

135-
def _process_class(cls: Type[T], letter_case: Optional[LetterCase],
135+
def _process_class(cls: Type[T], letter_case: Union[LetterCaseCallable, LetterCase, None],
136136
undefined: Optional[Union[str, Undefined]]) -> Type[T]:
137137
if letter_case is not None or undefined is not None:
138138
cls.dataclass_json_config = config(letter_case=letter_case, # type: ignore[attr-defined]

dataclasses_json/cfg.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,13 @@ def __init__(self):
4747
global_config = _GlobalConfig()
4848

4949

50+
LetterCaseCallable = Callable[[str], str]
51+
5052
class LetterCase(Enum):
51-
CAMEL = camelcase
52-
KEBAB = spinalcase
53-
SNAKE = snakecase
54-
PASCAL = pascalcase
53+
CAMEL: LetterCaseCallable = camelcase
54+
KEBAB: LetterCaseCallable = spinalcase
55+
SNAKE: LetterCaseCallable = snakecase
56+
PASCAL: LetterCaseCallable = pascalcase
5557

5658

5759
def config(metadata: Optional[dict] = None, *,
@@ -60,7 +62,7 @@ def config(metadata: Optional[dict] = None, *,
6062
encoder: Optional[Callable] = None,
6163
decoder: Optional[Callable] = None,
6264
mm_field: Optional[MarshmallowField] = None,
63-
letter_case: Union[Callable[[str], str], LetterCase, None] = None,
65+
letter_case: Union[LetterCaseCallable, LetterCase, None] = None,
6466
undefined: Optional[Union[str, Undefined]] = None,
6567
field_name: Optional[str] = None,
6668
exclude: Optional[Callable[[T], bool]] = None,

dataclasses_json/stringcase.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,10 @@
2424
# Copyright © 2020 Louis-Philippe Véronneau <[email protected]>
2525

2626
import re
27+
from typing import Literal
2728

2829

29-
def uplowcase(string, case):
30+
def uplowcase(string: str, case: Literal["up", "low"]) -> str:
3031
"""Convert string into upper or lower case.
3132
3233
Args:
@@ -42,7 +43,7 @@ def uplowcase(string, case):
4243
return str(string).lower()
4344

4445

45-
def capitalcase(string):
46+
def capitalcase(string: str) -> str:
4647
"""Convert string into capital case.
4748
First letters will be uppercase.
4849
@@ -60,7 +61,7 @@ def capitalcase(string):
6061
return uplowcase(string[0], 'up') + string[1:]
6162

6263

63-
def camelcase(string):
64+
def camelcase(string: str) -> str:
6465
""" Convert string into camel case.
6566
6667
Args:
@@ -80,7 +81,7 @@ def camelcase(string):
8081
string[1:]))
8182

8283

83-
def snakecase(string):
84+
def snakecase(string: str) -> str:
8485
"""Convert string into snake case.
8586
Join punctuation with underscore
8687
@@ -101,7 +102,7 @@ def snakecase(string):
101102
string[1:]))
102103

103104

104-
def spinalcase(string):
105+
def spinalcase(string: str) -> str:
105106
"""Convert string into spinal case.
106107
Join punctuation with hyphen.
107108
@@ -116,7 +117,7 @@ def spinalcase(string):
116117
return re.sub(r"_", "-", snakecase(string))
117118

118119

119-
def pascalcase(string):
120+
def pascalcase(string: str) -> str:
120121
"""Convert string into pascal case.
121122
122123
Args:

0 commit comments

Comments
 (0)