From 197d1f259524c6df438bcb04d514fa476e068e1f Mon Sep 17 00:00:00 2001 From: santhreal <64453045+santhreal@users.noreply.github.com> Date: Fri, 17 Jul 2026 22:12:50 -0700 Subject: [PATCH 1/8] fix: return empty flat map when container appstruct is null Mapping/Sequence/Tuple flatten assumed a real container when a missing=drop child was absent from the appstruct, so SchemaNode.flatten raised TypeError/AttributeError on colander.null (issue #299). --- src/colander/__init__.py | 6 +++++ tests/test_colander.py | 51 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+) 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()) From 940c24d7fe7ca9b3d055c0209a98ad9add7cb5e9 Mon Sep 17 00:00:00 2001 From: santhreal <64453045+santhreal@users.noreply.github.com> Date: Sat, 18 Jul 2026 16:33:19 -0700 Subject: [PATCH 2/8] ci: ignore flake8-bugbear B042 for Invalid kwargs API. --- .flake8 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.flake8 b/.flake8 index a0e1fe3..082b52b 100644 --- a/.flake8 +++ b/.flake8 @@ -1,6 +1,6 @@ [flake8] ignore = - # E731: do not assign a lambda expression, use a def + # E731: do not assign a lambda expression, use a def,B042 E731 # W503: line break before binary operator (flake8 is not PEP8 compliant) W503 From 4fa6a39d822c4ff1c8eadc156f3c4084d494b19f Mon Sep 17 00:00:00 2001 From: santhreal <64453045+santhreal@users.noreply.github.com> Date: Sat, 18 Jul 2026 16:33:28 -0700 Subject: [PATCH 3/8] ci: ignore B042 on its own flake8 ignore line. --- .flake8 | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.flake8 b/.flake8 index 082b52b..f6922fd 100644 --- a/.flake8 +++ b/.flake8 @@ -1,10 +1,12 @@ [flake8] ignore = - # E731: do not assign a lambda expression, use a def,B042 + # E731: do not assign a lambda expression, use a def E731 # W503: line break before binary operator (flake8 is not PEP8 compliant) 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 From 341b931176042d65ed8306d4824cd6d8cb017f9b Mon Sep 17 00:00:00 2001 From: santhreal <64453045+santhreal@users.noreply.github.com> Date: Sat, 18 Jul 2026 16:36:16 -0700 Subject: [PATCH 4/8] docs: use importlib.metadata instead of pkg_resources. --- docs/conf.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 From a89210f60d63bd26dfa44050505cfc484495a73d Mon Sep 17 00:00:00 2001 From: santhreal <64453045+santhreal@users.noreply.github.com> Date: Sat, 18 Jul 2026 16:36:22 -0700 Subject: [PATCH 5/8] ci: drop Python 3.7 and replace removed macos-11 runners. --- .github/workflows/ci-tests.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml index c8fffea..39738ca 100644 --- a/.github/workflows/ci-tests.yml +++ b/.github/workflows/ci-tests.yml @@ -15,7 +15,6 @@ jobs: strategy: matrix: py: - - "3.7" - "3.8" - "3.9" - "3.10" @@ -25,8 +24,7 @@ jobs: os: - "ubuntu-latest" - "windows-latest" - # later versions no longer support Python < 3.10. - - "macos-11" + - "macos-latest" architecture: - x64 - x86 @@ -34,7 +32,7 @@ jobs: # Linux and macOS don't have x86 python - os: "ubuntu-latest" architecture: x86 - - os: "macos-11" + - os: "macos-latest" architecture: x86 name: "Python: ${{ matrix.py }}-${{ matrix.architecture }} on ${{ matrix.os }}" runs-on: ${{ matrix.os }} From 9a42a36aa4cb8fbacbc8743b4de48aa5a15614bc Mon Sep 17 00:00:00 2001 From: santhreal <64453045+santhreal@users.noreply.github.com> Date: Sat, 18 Jul 2026 16:40:33 -0700 Subject: [PATCH 6/8] ci: disable fail-fast so one runner flake cannot cancel the matrix. --- .github/workflows/ci-tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml index 39738ca..e4915b7 100644 --- a/.github/workflows/ci-tests.yml +++ b/.github/workflows/ci-tests.yml @@ -13,6 +13,7 @@ on: jobs: test: strategy: + fail-fast: false matrix: py: - "3.8" From bc225926af2f5da3578109ba5b57a3af88f7fc0a Mon Sep 17 00:00:00 2001 From: santhreal <64453045+santhreal@users.noreply.github.com> Date: Sat, 18 Jul 2026 16:43:27 -0700 Subject: [PATCH 7/8] ci: run macOS jobs on macos-13 for x64 Python macos-latest is arm64; setup-python x64 builds abort under Rosetta/gettext. --- .github/workflows/ci-tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml index e4915b7..49c1e7d 100644 --- a/.github/workflows/ci-tests.yml +++ b/.github/workflows/ci-tests.yml @@ -25,7 +25,7 @@ jobs: os: - "ubuntu-latest" - "windows-latest" - - "macos-latest" + - "macos-13" architecture: - x64 - x86 @@ -33,7 +33,7 @@ jobs: # Linux and macOS don't have x86 python - os: "ubuntu-latest" architecture: x86 - - os: "macos-latest" + - os: "macos-13" architecture: x86 name: "Python: ${{ matrix.py }}-${{ matrix.architecture }} on ${{ matrix.os }}" runs-on: ${{ matrix.os }} From 2f313458cee5dc7d7c17a1d01483587e8fb565a5 Mon Sep 17 00:00:00 2001 From: santhreal <64453045+santhreal@users.noreply.github.com> Date: Sat, 18 Jul 2026 16:44:46 -0700 Subject: [PATCH 8/8] ci: drop Windows PyPy 3.8 x86 from the matrix setup-python has no pypy-3.8 x86 builds on Windows. --- .github/workflows/ci-tests.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml index 49c1e7d..955a6e0 100644 --- a/.github/workflows/ci-tests.yml +++ b/.github/workflows/ci-tests.yml @@ -35,6 +35,10 @@ jobs: architecture: x86 - 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 }} steps: