diff --git a/.flake8 b/.flake8 index a0e1fe3..f6922fd 100644 --- a/.flake8 +++ b/.flake8 @@ -6,5 +6,7 @@ ignore = W503 # W504: line break after binary operator (flake8 is not PEP8 compliant) W504 + # B042: Invalid.__init__ documents and uses kwargs (msg/value); public API. + B042 show-source = True max-line-length = 79 diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml index c8fffea..955a6e0 100644 --- a/.github/workflows/ci-tests.yml +++ b/.github/workflows/ci-tests.yml @@ -13,9 +13,9 @@ on: jobs: test: strategy: + fail-fast: false matrix: py: - - "3.7" - "3.8" - "3.9" - "3.10" @@ -25,8 +25,7 @@ jobs: os: - "ubuntu-latest" - "windows-latest" - # later versions no longer support Python < 3.10. - - "macos-11" + - "macos-13" architecture: - x64 - x86 @@ -34,7 +33,11 @@ jobs: # Linux and macOS don't have x86 python - os: "ubuntu-latest" architecture: x86 - - os: "macos-11" + - os: "macos-13" + architecture: x86 + # setup-python has no Windows PyPy 3.8 x86 builds + - os: "windows-latest" + py: "pypy-3.8" architecture: x86 name: "Python: ${{ matrix.py }}-${{ matrix.architecture }} on ${{ matrix.os }}" runs-on: ${{ matrix.os }} diff --git a/docs/conf.py b/docs/conf.py index bda52d2..bad61fc 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -13,7 +13,6 @@ # out serve to show the default value. import sys, os, datetime -import pkg_resources import pylons_sphinx_themes # General configuration @@ -41,7 +40,8 @@ # other places throughout the built documents. # # The short X.Y version. -version = pkg_resources.get_distribution('colander').version +from importlib.metadata import version as _get_version +version = _get_version('colander') # The full version, including alpha/beta/rc tags. release = version diff --git a/src/colander/__init__.py b/src/colander/__init__.py index 06efc1f..ca3eed9 100644 --- a/src/colander/__init__.py +++ b/src/colander/__init__.py @@ -957,6 +957,8 @@ def callback(subnode, subcstruct): def flatten(self, node, appstruct, prefix='', listitem=False): result = {} + if appstruct is null: + return result if listitem: selfprefix = prefix else: @@ -1096,6 +1098,8 @@ def callback(subnode, subval): def flatten(self, node, appstruct, prefix='', listitem=False): result = {} + if appstruct is null: + return result if listitem: selfprefix = prefix else: @@ -1362,6 +1366,8 @@ def callback(subnode, subcstruct): def flatten(self, node, appstruct, prefix='', listitem=False): result = {} + if appstruct is null: + return result if listitem: selfprefix = prefix else: diff --git a/tests/test_colander.py b/tests/test_colander.py index 8520868..351406e 100644 --- a/tests/test_colander.py +++ b/tests/test_colander.py @@ -1124,6 +1124,14 @@ def test_flatten(self): result = typ.flatten(node, {'a': 1, 'b': 2}) self.assertEqual(result, {'node.appstruct': 2}) + def test_flatten_null(self): + from colander import null + + node = DummySchemaNode(None, name='node') + node.children = [DummySchemaNode(DummyType(), name='a')] + typ = self._makeOne() + self.assertEqual(typ.flatten(node, null), {}) + def test_flatten_listitem(self): node = DummySchemaNode(None, name='node') int1 = DummyType() @@ -1371,6 +1379,17 @@ def test_flatten(self): result = typ.flatten(node, (1, 2)) self.assertEqual(result, {'node.appstruct': 2}) + def test_flatten_null(self): + from colander import null + + node = DummySchemaNode(None, name='node') + node.children = [ + DummySchemaNode(DummyType(), name='a'), + DummySchemaNode(DummyType(), name='b'), + ] + typ = self._makeOne() + self.assertEqual(typ.flatten(node, null), {}) + def test_flatten_listitem(self): node = DummySchemaNode(None, name='node') int1 = DummyType() @@ -1732,6 +1751,14 @@ def test_flatten(self): result = typ.flatten(node, [1, 2]) self.assertEqual(result, {'node.0': 1, 'node.1': 2}) + def test_flatten_null(self): + from colander import null + + node = DummySchemaNode(None, name='node') + node.children = [DummySchemaNode(DummyType(), name='foo')] + typ = self._makeOne() + self.assertEqual(typ.flatten(node, null), {}) + def test_flatten_with_integer(self): from colander import Integer @@ -4080,6 +4107,30 @@ class MySchema(colander.Schema): result = node.deserialize(expected) self.assertEqual(result, expected) + def test_flatten_after_deserialize_drop_containers(self): + # missing=drop omits the key from the appstruct; flatten must not + # raise when Mapping walks that absent child as colander.null. + class Seq(colander.SequenceSchema): + item = colander.SchemaNode(colander.String()) + + class Tup(colander.TupleSchema): + x = colander.SchemaNode(colander.Int()) + y = colander.SchemaNode(colander.Int()) + + class Inner(colander.MappingSchema): + z = colander.SchemaNode(colander.String()) + + class MySchema(colander.Schema): + title = colander.SchemaNode(colander.String()) + items = Seq(missing=colander.drop) + point = Tup(missing=colander.drop) + inner = Inner(missing=colander.drop) + + node = MySchema() + appstruct = node.deserialize({'title': 't'}) + self.assertEqual(appstruct, {'title': 't'}) + self.assertEqual(node.flatten(appstruct), {'title': 't'}) + def test_serialize_drop_default(self): class MySchema(colander.Schema): a = colander.SchemaNode(colander.String())