Skip to content

Commit

Permalink
3.9 initial support
Browse files Browse the repository at this point in the history
  • Loading branch information
Tinche committed Sep 1, 2020
1 parent 686ab87 commit eb454d4
Show file tree
Hide file tree
Showing 9 changed files with 117 additions and 70 deletions.
6 changes: 3 additions & 3 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,19 @@ language: python

matrix:
include:
- python: "3.6"
env: TOXENV=py36
- python: "3.7"
env: TOXENV=py37
- python: "3.8"
env: TOXENV=py38
dist: xenial
sudo: true
- python: "3.9-dev"
env: TOXENV=py39

# Meta
- python: "3.8"
env: TOXENV=lint
- python: "3.6"
- python: "3.8"
env: TOXENV=docs

before_install:
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@
"License :: OSI Approved :: MIT License",
"Natural Language :: English",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
],
)
79 changes: 52 additions & 27 deletions src/cattr/_compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@
from typing import (
Dict,
FrozenSet,
List,
Mapping,
MutableMapping,
MutableSequence,
MutableSet,
Sequence,
Set,
Tuple,
)

Expand All @@ -15,8 +18,14 @@
is_py38 = version_info[:2] == (3, 8)


def is_tuple(type):
return type is Tuple or (
type.__class__ is _GenericAlias and issubclass(type.__origin__, Tuple)
)


if is_py37 or is_py38:
from typing import List, Union, _GenericAlias
from typing import Union, _GenericAlias

def is_union_type(obj):
return (
Expand All @@ -32,20 +41,14 @@ def is_sequence(type):
and issubclass(type.__origin__, Sequence)
)

def is_frozenset(type):
return type.__class__ is _GenericAlias and issubclass(
type.__origin__, FrozenSet
)

def is_mutable_set(type):
return type.__class__ is _GenericAlias and issubclass(
type.__origin__, MutableSet
)

def is_tuple(type):
return type is Tuple or (
type.__class__ is _GenericAlias
and issubclass(type.__origin__, Tuple)
def is_frozenset(type):
return type.__class__ is _GenericAlias and issubclass(
type.__origin__, FrozenSet
)

def is_mapping(type):
Expand All @@ -72,29 +75,51 @@ def is_bare(type):


else:
from typing import _Union
# 3.9+
from typing import (
Union,
_GenericAlias,
_SpecialGenericAlias,
_UnionGenericAlias,
)

def is_union_type(obj):
"""Return true if the object is a union. """
return isinstance(obj, _Union)

def is_frozenset(type):
return issubclass(type, FrozenSet)

def is_mutable_set(type):
return issubclass(type, MutableSet)
return (
obj is Union
or isinstance(obj, _UnionGenericAlias)
and obj.__origin__ is Union
)

def is_sequence(type):
is_string = issubclass(type, str)
return issubclass(type, Sequence) and not is_string
return (
type is List
or type is Sequence
or type is MutableSequence
or (
type.__class__ is _GenericAlias
and issubclass(type.__origin__, Sequence)
)
)

def is_tuple(type):
return issubclass(type, Tuple)
def is_mutable_set(type):
return type in (Set, MutableSet) or (
type.__class__ is _GenericAlias
and issubclass(type.__origin__, MutableSet)
)

def is_mapping(type):
return issubclass(type, Mapping)
def is_frozenset(type):
return type is FrozenSet or (
type.__class__ is _GenericAlias
and issubclass(type.__origin__, FrozenSet)
)

def is_bare(type):
return not type.__args__
return isinstance(type, _SpecialGenericAlias) or (
not hasattr(type, "__origin__") and not hasattr(type, "__args__")
)

is_bare_frozenset = is_bare
def is_mapping(type):
return type in (Mapping, Dict, MutableMapping) or (
type.__class__ is _GenericAlias
and issubclass(type.__origin__, Mapping)
)
12 changes: 9 additions & 3 deletions src/cattr/converters.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,10 @@ def __init__(
# Strings are sequences.
self._structure_func.register_cls_list(
[
(str, self._structure_call,),
(
str,
self._structure_call,
),
(bytes, self._structure_call),
(int, self._structure_call),
(float, self._structure_call),
Expand Down Expand Up @@ -389,9 +392,12 @@ def _structure_union(self, obj, union):
cl = self._dis_func_cache(union)(obj)
return self._structure_func.dispatch(cl)(obj, cl)

def _structure_tuple(self, obj, tup):
def _structure_tuple(self, obj, tup: Type[T]):
"""Deal with converting to a tuple."""
tup_params = tup.__args__
if tup is Tuple:
tup_params = None
else:
tup_params = tup.__args__
has_ellipsis = tup_params and tup_params[-1] is Ellipsis
if tup_params is None or (has_ellipsis and tup_params[0] is Any):
# Just a Tuple. (No generic information.)
Expand Down
7 changes: 3 additions & 4 deletions tests/metadata/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Tests for metadata functionality."""
from collections import OrderedDict
from typing import Any, Dict, List

import attr
from attr import NOTHING
Expand All @@ -15,8 +16,6 @@
text,
tuples,
)
from typing import Any, Dict, List
from cattr._compat import unicode

from .. import gen_attr_names, make_class

Expand Down Expand Up @@ -100,7 +99,7 @@ def str_typed_attrs(draw, defaults=None):
default = NOTHING
if defaults is True or (defaults is None and draw(booleans())):
default = draw(text())
return (attr.ib(type=unicode, default=default), text())
return (attr.ib(type=str, default=default), text())


@composite
Expand All @@ -125,7 +124,7 @@ def dict_typed_attrs(draw, defaults=None):
val_strat = dictionaries(keys=text(), values=integers())
if defaults is True or (defaults is None and draw(booleans())):
default = draw(val_strat)
return (attr.ib(type=Dict[unicode, int], default=default), val_strat)
return (attr.ib(type=Dict[str, int], default=default), val_strat)


def just_class(tup):
Expand Down
4 changes: 3 additions & 1 deletion tests/metadata/test_roundtrips.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,9 @@ def test_nested_roundtrip(cls_and_vals, strat):
assert inst == converter.structure(converter.unstructure(inst), cl)


@settings(suppress_health_check=[HealthCheck.filter_too_much])
@settings(
suppress_health_check=[HealthCheck.filter_too_much, HealthCheck.too_slow]
)
@given(
simple_typed_classes(defaults=False),
simple_typed_classes(defaults=False),
Expand Down
25 changes: 19 additions & 6 deletions tests/test_dict_generation.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,17 @@ def test_nodefs_generated_unstructuring_cl(converter, cl_and_vals):
else:
assert attr.name in res
else:
if val == attr.default.factory():
assert attr.name not in res
# The default is a factory, but might take self.
if attr.default.takes_self:
if val == attr.default.factory(cl):
assert attr.name not in res
else:
assert attr.name in res
else:
assert attr.name in res
if val == attr.default.factory():
assert attr.name not in res
else:
assert attr.name in res


@given(nested_classes | simple_classes())
Expand Down Expand Up @@ -126,7 +133,13 @@ def test_individual_overrides(cl_and_vals):
else:
assert attr.name in res
else:
if val == attr.default.factory():
assert attr.name not in res
if attr.default.takes_self:
if val == attr.default.factory(inst):
assert attr.name not in res
else:
assert attr.name in res
else:
assert attr.name in res
if val == attr.default.factory():
assert attr.name not in res
else:
assert attr.name in res
Loading

0 comments on commit eb454d4

Please sign in to comment.