diff --git a/HISTORY.rst b/HISTORY.rst index 9670bae..2b0d534 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -5,6 +5,8 @@ History v0.2.0 (TBC) ............ +* **breaking change**: ``values()`` on a model is now a method not a property, + takes ``include`` and ``exclude`` arguments * allow annotation only fields to support mypy * add pretty ``to_string(pretty=True)`` method for models diff --git a/docs/examples/exotic.py b/docs/examples/exotic.py index d2ff6de..8f60818 100644 --- a/docs/examples/exotic.py +++ b/docs/examples/exotic.py @@ -37,7 +37,7 @@ m = Model( email_address='Samuel Colvin ', email_and_name='Samuel Colvin ', ) -print(m.values) +print(m.values()) """ { 'cos_function': , diff --git a/docs/examples/recursive.py b/docs/examples/recursive.py index 1770cb7..5982657 100644 --- a/docs/examples/recursive.py +++ b/docs/examples/recursive.py @@ -17,5 +17,5 @@ class Spam(BaseModel): m = Spam(foo={'count': 4}, bars=[{'apple': 'x1'}, {'apple': 'x2'}]) print(m) # > Spam foo= bars=[, ] -print(m.values) +print(m.values()) # {'foo': {'count': 4, 'size': None}, 'bars': [{'apple': 'x1', 'banana': 'y'}, {'apple': 'x2', 'banana': 'y'}]} diff --git a/pydantic/main.py b/pydantic/main.py index 88d5894..5504a8a 100644 --- a/pydantic/main.py +++ b/pydantic/main.py @@ -1,5 +1,6 @@ from collections import OrderedDict from types import FunctionType +from typing import Any, Dict, Set from .exceptions import Error, Extra, Missing, ValidationError from .fields import Field @@ -96,7 +97,7 @@ class BaseModel(metaclass=MetaModel): Config = BaseConfig def __init__(self, **values): - self.__values__ = {} + self.__values__ = OrderedDict() self.__errors__ = OrderedDict() self._process_values(values) @@ -111,9 +112,16 @@ class BaseModel(metaclass=MetaModel): setattr(self, name, value) self.__values__[name] = value - @property - def values(self): - return dict(self) + def values(self, *, include: Set[str]=None, exclude: Set[str]=set()) -> Dict[str, Any]: + """ + Get a dict of the values processed by the model, optionally specifying which fields to include or exclude. + + This is NOT equivalent to the values() method on a dict. + """ + return { + k: v for k, v in self + if k not in exclude and (not include or k in include) + } @property def fields(self): @@ -175,7 +183,7 @@ class BaseModel(metaclass=MetaModel): @classmethod def _get_value(cls, v): if isinstance(v, BaseModel): - return v.values + return v.values() elif isinstance(v, list): return [cls._get_value(v_) for v_ in v] elif isinstance(v, dict): @@ -194,9 +202,9 @@ class BaseModel(metaclass=MetaModel): def __eq__(self, other): if isinstance(other, BaseModel): - return self.values == other.values + return self.values() == other.values() else: - return self.values == other + return self.values() == other def __repr__(self): return f'<{self}>' diff --git a/tests/test_complex.py b/tests/test_complex.py index 9fbd6f6..82458c6 100644 --- a/tests/test_complex.py +++ b/tests/test_complex.py @@ -180,7 +180,7 @@ class DictModel(BaseModel): def test_dict_values(): - assert DictModel(v={'foo': 1}).values == {'v': {'foo': 1}} + assert DictModel(v={'foo': 1}).values() == {'v': {'foo': 1}} @pytest.mark.parametrize('value,result', [ @@ -269,7 +269,7 @@ def test_recursive_list(): assert "]>" == repr(m) assert m.v[0].name == 'testing' assert m.v[0].count == 4 - assert m.values == {'v': [{'count': 4, 'name': 'testing'}]} + assert m.values() == {'v': [{'count': 4, 'name': 'testing'}]} with pytest.raises(ValidationError) as exc_info: MasterListModel(v=['x']) @@ -356,9 +356,9 @@ def test_str_enum(): def test_any_dict(): class Model(BaseModel): v: Dict[int, Any] = ... - assert Model(v={1: 'foobar'}).values == {'v': {1: 'foobar'}} - assert Model(v={123: 456}).values == {'v': {123: 456}} - assert Model(v={2: [1, 2, 3]}).values == {'v': {2: [1, 2, 3]}} + assert Model(v={1: 'foobar'}).values() == {'v': {1: 'foobar'}} + assert Model(v={123: 456}).values() == {'v': {123: 456}} + assert Model(v={2: [1, 2, 3]}).values() == {'v': {2: [1, 2, 3]}} def test_infer_alias(): @@ -402,3 +402,26 @@ def test_annotation_config(): assert list(Model.__fields__.keys()) == ['b', 'a'] assert [f.alias for f in Model.__fields__.values()] == ['b', 'foobar'] assert Model(foobar='123').a == 123.0 + + +def test_success_values_include(): + class Model(BaseModel): + a: int = 1 + b: int = 2 + c: int = 3 + + m = Model() + assert m.values() == {'a': 1, 'b': 2, 'c': 3} + assert m.values(include={'a'}) == {'a': 1} + assert m.values(exclude={'a'}) == {'b': 2, 'c': 3} + assert m.values(include={'a', 'b'}, exclude={'a'}) == {'b': 2} + + +def test_values_order(): + class Model(BaseModel): + a: int = 1 + b: int = 2 + c: int = 3 + + m = Model(c=30, b=20, a=10) + assert list(m) == [('a', 10), ('b', 20), ('c', 30)] diff --git a/tests/test_main.py b/tests/test_main.py index 61f39ad..b51fd93 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -244,19 +244,19 @@ def test_allow_extra(): class Config: allow_extra = True - assert Model(a='10.2', b=12).values == {'a': 10.2, 'b': 12} + assert Model(a='10.2', b=12).values() == {'a': 10.2, 'b': 12} def test_set_attr(): m = UltraSimpleModel(a=10.2) - assert m.values == {'a': 10.2, 'b': 10} + assert m.values() == {'a': 10.2, 'b': 10} m.setattr('b', 20) - assert m.values == {'a': 10.2, 'b': 20} + assert m.values() == {'a': 10.2, 'b': 20} def test_set_attr_invalid(): m = UltraSimpleModel(a=10.2) - assert m.values == {'a': 10.2, 'b': 10} + assert m.values() == {'a': 10.2, 'b': 10} with pytest.raises(ValueError) as exc_info: m.setattr('c', 20) assert '"UltraSimpleModel" object has no field "c"' in str(exc_info) @@ -280,9 +280,9 @@ def test_alias(): } assert Model().a == 'foobar' - assert Model().values == {'a': 'foobar'} + assert Model().values() == {'a': 'foobar'} assert Model(_a='different').a == 'different' - assert Model(_a='different').values == {'a': 'different'} + assert Model(_a='different').values() == {'a': 'different'} def test_field_order(): @@ -303,7 +303,7 @@ def test_required(): b: int = 10 m = Model(a=10.2) - assert m.values == dict(a=10.2, b=10) + assert m.values() == dict(a=10.2, b=10) with pytest.raises(ValidationError) as exc_info: Model() diff --git a/tests/test_types.py b/tests/test_types.py index 8b5fee4..f535e8e 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -148,7 +148,7 @@ def test_default_validators(field, value, result): with pytest.raises(ValidationError): CheckModel(**kwargs) else: - assert CheckModel(**kwargs).values[field] == result + assert CheckModel(**kwargs).values()[field] == result class DatetimeModel(BaseModel): @@ -348,7 +348,7 @@ def test_tuple(): m = ListDictTupleModel(d=(1, 2, '3')) assert m.a is None assert m.d == (1, 2, '3') - assert m.values == {'a': None, 'b': None, 'c': None, 'd': (1, 2, '3')} + assert m.values() == {'a': None, 'b': None, 'c': None, 'd': (1, 2, '3')} assert ListDictTupleModel(d='xyz').d == ('x', 'y', 'z') assert ListDictTupleModel(d=(i**2 for i in range(5))).d == (0, 1, 4, 9, 16) with pytest.raises(ValidationError) as exc_info: @@ -376,5 +376,5 @@ def test_set(): m = SetModel(v=[1, 2, 3]) assert m.v == {1, 2, 3} - assert m.values == {'v': {1, 2, 3}} + assert m.values() == {'v': {1, 2, 3}} assert SetModel(v={'a', 'b', 'c'}).v == {'a', 'b', 'c'}