Skip to content

Commit

Permalink
Add Meta option to include non init-ed fields (#246)
Browse files Browse the repository at this point in the history
* fix bitrot in tests

* Add Meta option to include non init-ed fields

Updates #60

* ensure fields are always compared in a deterministic order

* maxdiff none

* update pypy

* skip failing test on python3.6

* Revert "skip failing test on python3.6"

This reverts commit e8e29b9.

* fixup! fix bitrot in tests

We do care about the order of Union.union_fields.

This partially reverts commit cbad82b.

* Ignore union ordering

---------

Co-authored-by: lovasoa <pere.jobs@gmail.com>
Co-authored-by: Jeff Dairiki <dairiki@dairiki.org>
  • Loading branch information
3 people authored Sep 16, 2023
1 parent 06f0660 commit 5f38b93
Show file tree
Hide file tree
Showing 5 changed files with 60 additions and 7 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/python-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
fail-fast: false
matrix:
os: ["ubuntu-latest"]
python_version: ["3.7", "3.8", "3.9", "3.10", "3.11", "pypy3.9"]
python_version: ["3.7", "3.8", "3.9", "3.10", "3.11", "pypy3.10"]
include:
- os: "ubuntu-20.04"
python_version: "3.6"
Expand Down
22 changes: 21 additions & 1 deletion marshmallow_dataclass/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,9 @@ def class_schema(
>>> person
Person(name='Anonymous', friends=[Person(name='Roger Boucher', friends=[])])
Marking dataclass fields as non-initialized (``init=False``), by default, will result in those
fields from being exluded in the schema. To override this behaviour, set the ``Meta`` option
``include_non_init=True``.
>>> @dataclasses.dataclass()
... class C:
... important: int = dataclasses.field(init=True, default=0)
Expand All @@ -310,6 +313,20 @@ def class_schema(
>>> c
C(important=9, unimportant=0)
>>> @dataclasses.dataclass()
... class C:
... class Meta:
... include_non_init = True
... important: int = dataclasses.field(init=True, default=0)
... unimportant: int = dataclasses.field(init=False, default=0)
...
>>> c = class_schema(C)().load({
... "important": 9, # This field will be imported
... "unimportant": 9 # This field will be imported
... }, unknown=marshmallow.EXCLUDE)
>>> c
C(important=9, unimportant=9)
>>> @dataclasses.dataclass
... class Website:
... url:str = dataclasses.field(metadata = {
Expand Down Expand Up @@ -408,6 +425,9 @@ def _internal_class_schema(
if hasattr(v, "__marshmallow_hook__") or k in MEMBERS_WHITELIST
}

# Determine whether we should include non-init fields
include_non_init = getattr(getattr(clazz, "Meta", None), "include_non_init", False)

# Update the schema members to contain marshmallow fields instead of dataclass fields
type_hints = get_type_hints(
clazz, localns=clazz_frame.f_locals if clazz_frame else None
Expand All @@ -424,7 +444,7 @@ def _internal_class_schema(
),
)
for field in fields
if field.init
if field.init or include_non_init
)

schema_class = type(clazz.__name__, (_base_schema(clazz, base_schema),), attributes)
Expand Down
23 changes: 23 additions & 0 deletions tests/test_class_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,29 @@ class Second:
{"first": {"second": {"first": None}}},
)

def test_init_fields(self):
@dataclasses.dataclass
class NoMeta:
no_init: str = dataclasses.field(init=False)

@dataclasses.dataclass
class NoInit:
class Meta:
pass

no_init: str = dataclasses.field(init=False)

@dataclasses.dataclass
class Init:
class Meta:
include_non_init = True

no_init: str = dataclasses.field(init=False)

self.assertNotIn("no_init", class_schema(NoMeta)().fields)
self.assertNotIn("no_init", class_schema(NoInit)().fields)
self.assertIn("no_init", class_schema(Init)().fields)


if __name__ == "__main__":
unittest.main()
19 changes: 14 additions & 5 deletions tests/test_field_for_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,24 @@


class TestFieldForSchema(unittest.TestCase):
maxDiff = None

def assertFieldsEqual(self, a: fields.Field, b: fields.Field):
self.assertEqual(a.__class__, b.__class__, "field class")

def canonical(k, v):
if k == "union_fields":
# See https://github.com/lovasoa/marshmallow_dataclass/pull/246#issuecomment-1722291806
return k, sorted(map(repr, v))
elif inspect.isclass(v):
return k, f"{v!r} ({v.__mro__!r})"
else:
return k, repr(v)

def attrs(x):
return {
k: f"{v!r} ({v.__mro__!r})" if inspect.isclass(v) else repr(v)
for k, v in x.__dict__.items()
if not k.startswith("_")
}
return sorted(
canonical(k, v) for k, v in vars(x).items() if not k.startswith("_")
)

self.assertEqual(attrs(a), attrs(b))

Expand Down
1 change: 1 addition & 0 deletions tests/test_mypy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
follow_imports = silent
plugins = marshmallow_dataclass.mypy
show_error_codes = true
python_version = 3.6
env:
- PYTHONPATH=.
main: |
Expand Down

0 comments on commit 5f38b93

Please sign in to comment.