Skip to content

Commit 27bd0cc

Browse files
author
rodrigo.nogueira
committed
Add MISSING sentinel to exclude keys from DictFactory
This adds a MISSING sentinel value that can be used with DictFactory to exclude specific keys from the generated dictionary. Example usage: class MyFactory(factory.DictFactory): name = factory.Faker('name') email = factory.MISSING # This key will be excluded The MISSING sentinel: - Is a singleton with a falsy boolean value - Can be used with Transformer declarations - Can be conditionally returned from LazyFunction - Can be overridden at call time to include the key
1 parent ae9f2f4 commit 27bd0cc

5 files changed

Lines changed: 128 additions & 1 deletion

File tree

factory/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
from .enums import BUILD_STRATEGY, CREATE_STRATEGY, STUB_STRATEGY
3434
from .errors import FactoryError
3535
from .faker import Faker
36+
from .utils import MISSING
3637
from .helpers import (
3738
build,
3839
build_batch,

factory/base.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -697,6 +697,12 @@ def _build(cls, model_class, *args, **kwargs):
697697
def _create(cls, model_class, *args, **kwargs):
698698
return cls._build(model_class, *args, **kwargs)
699699

700+
@classmethod
701+
def _adjust_kwargs(cls, **kwargs):
702+
from .utils import MISSING
703+
return {k: v for k, v in kwargs.items() if v is not MISSING}
704+
705+
700706

701707
class DictFactory(BaseDictFactory):
702708
class Meta:

factory/declarations.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,11 +157,14 @@ def evaluate_pre(self, instance, step, overrides):
157157
step=step,
158158
overrides=overrides,
159159
)
160-
if bypass_transform:
160+
161+
from .utils import MISSING
162+
if bypass_transform or value is MISSING:
161163
return value
162164
return self.transform(value)
163165

164166

167+
165168
class _UNSPECIFIED:
166169
pass
167170

factory/utils.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,30 @@
55
import importlib
66

77

8+
class _MissingSentinel:
9+
"""Sentinel value to mark dictionary items that should be excluded.
10+
11+
When used as a value in DictFactory, the corresponding key will be
12+
completely omitted from the generated dictionary.
13+
"""
14+
15+
_instance = None
16+
17+
def __new__(cls):
18+
if cls._instance is None:
19+
cls._instance = super().__new__(cls)
20+
return cls._instance
21+
22+
def __repr__(self) -> str:
23+
return "<MISSING>"
24+
25+
def __bool__(self) -> bool:
26+
return False
27+
28+
29+
MISSING = _MissingSentinel()
30+
31+
832
def import_object(module_name, attribute_name):
933
"""Import an object from its absolute path.
1034

tests/test_missing.py

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
# Copyright: See the LICENSE file.
2+
3+
import unittest
4+
5+
import factory
6+
from factory import MISSING
7+
8+
9+
class MissingSentinelTestCase(unittest.TestCase):
10+
"""Tests for the MISSING sentinel object."""
11+
12+
def test_missing_basic(self):
13+
"""Test basic MISSING exclusion from DictFactory."""
14+
15+
class MyDictFactory(factory.DictFactory):
16+
name = "John"
17+
email = MISSING
18+
phone = "123-456"
19+
20+
result = MyDictFactory()
21+
self.assertEqual(result, {"name": "John", "phone": "123-456"})
22+
self.assertNotIn("email", result)
23+
24+
def test_missing_override(self):
25+
"""Test MISSING can be overridden per-instance."""
26+
27+
class MyDictFactory(factory.DictFactory):
28+
name = "John"
29+
email = "default@example.com"
30+
31+
result = MyDictFactory(email=MISSING)
32+
self.assertEqual(result, {"name": "John"})
33+
self.assertNotIn("email", result)
34+
35+
def test_missing_all(self):
36+
"""Test all fields can be MISSING."""
37+
38+
class MyDictFactory(factory.DictFactory):
39+
field1 = MISSING
40+
field2 = MISSING
41+
42+
result = MyDictFactory()
43+
self.assertEqual(result, {})
44+
45+
def test_missing_with_lazy_function(self):
46+
"""Test MISSING works with declarations."""
47+
48+
class MyDictFactory(factory.DictFactory):
49+
name = factory.LazyFunction(lambda: "Generated")
50+
skipped = MISSING
51+
value = 42
52+
53+
result = MyDictFactory()
54+
self.assertEqual(result["name"], "Generated")
55+
self.assertEqual(result["value"], 42)
56+
self.assertNotIn("skipped", result)
57+
58+
def test_missing_singleton(self):
59+
"""Test MISSING is a singleton."""
60+
another_missing = factory.MISSING
61+
self.assertIs(MISSING, another_missing)
62+
63+
def test_missing_bool(self):
64+
"""Test MISSING is falsy."""
65+
self.assertFalse(MISSING)
66+
self.assertFalse(bool(MISSING))
67+
68+
def test_missing_repr(self):
69+
"""Test MISSING has readable repr."""
70+
self.assertEqual(repr(MISSING), "<MISSING>")
71+
72+
def test_missing_with_transformer(self):
73+
"""Test MISSING works with Transformer declarations."""
74+
75+
class MyDictFactory(factory.DictFactory):
76+
timeout = factory.Transformer(30.0, transform=int)
77+
78+
result = MyDictFactory()
79+
self.assertEqual(result, {"timeout": 30})
80+
81+
result = MyDictFactory(timeout=MISSING)
82+
self.assertEqual(result, {})
83+
self.assertNotIn("timeout", result)
84+
85+
def test_missing_transformer_default(self):
86+
"""Test MISSING as Transformer default excludes without error."""
87+
88+
class MyDictFactory(factory.DictFactory):
89+
timeout = factory.Transformer(MISSING, transform=int)
90+
91+
result = MyDictFactory()
92+
self.assertEqual(result, {})
93+
self.assertNotIn("timeout", result)

0 commit comments

Comments
 (0)