factory-bot 0.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,13 @@
1
+ # Packages
2
+ *.egg
3
+ *.egg-info
4
+ build
5
+ dist
6
+ eggs
7
+ sdist
8
+ *.pyc
9
+ .coverage
10
+ .pytest_cache
11
+ .venv
12
+ .claude/worktrees
13
+ .ruff_cache
@@ -0,0 +1,7 @@
1
+ # Changelog
2
+
3
+ ## Unreleased
4
+
5
+ ## 0.0.1 - 2026-10-18
6
+
7
+ - Define factories with `@factory(Model)` and build them with `build` / `attributes_for`: static, lazy (dependencies injected by parameter name) and `sequence` attributes, traits, and a shared seedable `fake`. Faker is now a dependency.
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2024 Daniel Pepper
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,60 @@
1
+ Metadata-Version: 2.5
2
+ Name: factory_bot
3
+ Version: 0.0.1
4
+ Summary: Test data factories with the ergonomics of Ruby's factory_bot
5
+ Project-URL: Homepage, https://github.com/dpep/py_factory_bot
6
+ Project-URL: Issues, https://github.com/dpep/py_factory_bot/issues
7
+ Project-URL: Changelog, https://github.com/dpep/py_factory_bot/blob/main/CHANGELOG.md
8
+ Author: Daniel Pepper
9
+ License-Expression: MIT
10
+ License-File: LICENSE.txt
11
+ Keywords: factories,factory_bot,fixtures,testing
12
+ Classifier: Development Status :: 2 - Pre-Alpha
13
+ Classifier: Framework :: Pytest
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Topic :: Software Development :: Testing
17
+ Classifier: Typing :: Typed
18
+ Requires-Python: >=3.11
19
+ Requires-Dist: faker
20
+ Provides-Extra: django
21
+ Requires-Dist: django; extra == 'django'
22
+ Provides-Extra: sqlalchemy
23
+ Requires-Dist: sqlalchemy>=2; extra == 'sqlalchemy'
24
+ Description-Content-Type: text/markdown
25
+
26
+ factory_bot
27
+ ======
28
+ Test data factories with the ergonomics of Ruby's [factory_bot](https://github.com/thoughtbot/factory_bot).
29
+
30
+ Early development: the API is still settling.
31
+
32
+
33
+ ### Install
34
+ ```pip install factory_bot```
35
+
36
+
37
+ ### Usage
38
+ ```python
39
+ from factory_bot import build, factory, fake, sequence
40
+
41
+ @factory(User)
42
+ def user(f):
43
+ f.first_name = fake.first_name # callables run per build
44
+ f.email = lambda first_name: f"{first_name}@example.com".lower() # dependencies by name
45
+ f.username = sequence(lambda n: f"user{n}")
46
+ f.admin = False
47
+
48
+ @f.trait
49
+ def admin(f):
50
+ f.admin = True
51
+
52
+ build("user")
53
+ # User(first_name='Dennis', email='dennis@example.com', username='user1', admin=False)
54
+
55
+ build("user", "admin", first_name="Jane")
56
+ # User(first_name='Jane', email='jane@example.com', username='user2', admin=True)
57
+ ```
58
+
59
+ ----
60
+ [![installs](https://img.shields.io/pypi/dm/factory_bot?label=installs)](https://pypi.org/project/factory_bot)
@@ -0,0 +1,35 @@
1
+ factory_bot
2
+ ======
3
+ Test data factories with the ergonomics of Ruby's [factory_bot](https://github.com/thoughtbot/factory_bot).
4
+
5
+ Early development: the API is still settling.
6
+
7
+
8
+ ### Install
9
+ ```pip install factory_bot```
10
+
11
+
12
+ ### Usage
13
+ ```python
14
+ from factory_bot import build, factory, fake, sequence
15
+
16
+ @factory(User)
17
+ def user(f):
18
+ f.first_name = fake.first_name # callables run per build
19
+ f.email = lambda first_name: f"{first_name}@example.com".lower() # dependencies by name
20
+ f.username = sequence(lambda n: f"user{n}")
21
+ f.admin = False
22
+
23
+ @f.trait
24
+ def admin(f):
25
+ f.admin = True
26
+
27
+ build("user")
28
+ # User(first_name='Dennis', email='dennis@example.com', username='user1', admin=False)
29
+
30
+ build("user", "admin", first_name="Jane")
31
+ # User(first_name='Jane', email='jane@example.com', username='user2', admin=True)
32
+ ```
33
+
34
+ ----
35
+ [![installs](https://img.shields.io/pypi/dm/factory_bot?label=installs)](https://pypi.org/project/factory_bot)
@@ -0,0 +1,59 @@
1
+ [build-system]
2
+ requires = ["hatchling >= 1.27"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "factory_bot"
7
+ dynamic = ["version"]
8
+ description = "Test data factories with the ergonomics of Ruby's factory_bot"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE.txt"]
12
+ authors = [{ name = "Daniel Pepper" }]
13
+ requires-python = ">= 3.11"
14
+ dependencies = ["faker"]
15
+ keywords = ["testing", "factories", "fixtures", "factory_bot"]
16
+ classifiers = [
17
+ "Development Status :: 2 - Pre-Alpha",
18
+ "Framework :: Pytest",
19
+ "Intended Audience :: Developers",
20
+ "Programming Language :: Python :: 3",
21
+ "Topic :: Software Development :: Testing",
22
+ "Typing :: Typed",
23
+ ]
24
+
25
+ [project.optional-dependencies]
26
+ django = ["django"]
27
+ sqlalchemy = ["sqlalchemy >= 2"]
28
+
29
+ [project.urls]
30
+ Homepage = "https://github.com/dpep/py_factory_bot"
31
+ Issues = "https://github.com/dpep/py_factory_bot/issues"
32
+ Changelog = "https://github.com/dpep/py_factory_bot/blob/main/CHANGELOG.md"
33
+
34
+ [dependency-groups]
35
+ dev = [
36
+ "pytest >= 8.2",
37
+ "ruff",
38
+ "django",
39
+ "sqlalchemy >= 2",
40
+ ]
41
+
42
+ [tool.hatch.version]
43
+ path = "src/factory_bot/__init__.py"
44
+
45
+ # Allowlist so untracked local files never ship.
46
+ [tool.hatch.build.targets.sdist]
47
+ only-include = ["src", "tests", "CHANGELOG.md", "LICENSE.txt", "README.md", "pyproject.toml"]
48
+
49
+ [tool.pytest.ini_options]
50
+ testpaths = ["tests"]
51
+ pythonpath = ["tests"]
52
+ addopts = ["--import-mode=importlib", "--strict-markers"]
53
+ xfail_strict = true
54
+
55
+ [tool.ruff]
56
+ target-version = "py311"
57
+
58
+ [tool.ruff.lint]
59
+ extend-select = ["I", "UP", "B"]
@@ -0,0 +1,25 @@
1
+ from ._core import (
2
+ FactoryError,
3
+ attributes_for,
4
+ build,
5
+ clear_factories,
6
+ factory,
7
+ reset_sequences,
8
+ sequence,
9
+ )
10
+ from ._fake import fake, seed
11
+
12
+ __author__ = "dpepper"
13
+ __version__ = "0.0.1"
14
+
15
+ __all__ = [
16
+ "FactoryError",
17
+ "attributes_for",
18
+ "build",
19
+ "clear_factories",
20
+ "factory",
21
+ "fake",
22
+ "reset_sequences",
23
+ "seed",
24
+ "sequence",
25
+ ]
@@ -0,0 +1,210 @@
1
+ import copy
2
+ import difflib
3
+ import inspect
4
+ import itertools
5
+ import os
6
+ import weakref
7
+
8
+ _registry: dict[str, "_Factory"] = {}
9
+ _sequences: weakref.WeakSet["_Sequence"] = weakref.WeakSet()
10
+
11
+
12
+ class FactoryError(Exception):
13
+ pass
14
+
15
+
16
+ def factory(model):
17
+ """Register the decorated function as a factory for `model`, e.g. @factory(User)."""
18
+ if _is_body(model):
19
+ raise FactoryError(
20
+ f"@factory needs a model: write @factory(User) or @factory(dict) above def {model.__name__}"
21
+ )
22
+
23
+ def register(body):
24
+ name = body.__name__
25
+ if not callable(model):
26
+ raise FactoryError(f"Factory {name!r}: model {model!r} is not callable")
27
+ if name in _registry:
28
+ raise FactoryError(
29
+ f"Factory {name!r} is already defined{_where(_registry[name].body)}"
30
+ )
31
+ _registry[name] = _Factory(name, model, body)
32
+ return body
33
+
34
+ return register
35
+
36
+
37
+ def build(name, /, *traits, **overrides):
38
+ fac = _lookup(name)
39
+ return fac.model(**fac.attributes_for(traits, overrides))
40
+
41
+
42
+ def attributes_for(name, /, *traits, **overrides):
43
+ return _lookup(name).attributes_for(traits, overrides)
44
+
45
+
46
+ def sequence(fn):
47
+ """A value built from a counter that starts at 1, e.g. sequence(lambda n: f"user{n}")."""
48
+ try:
49
+ inspect.signature(fn).bind(1)
50
+ except TypeError:
51
+ raise FactoryError(
52
+ f'sequence() takes a function of the counter, e.g. sequence(lambda n: f"user{{n}}"), not {fn!r}'
53
+ ) from None
54
+ except ValueError: # no inspectable signature; trust it
55
+ pass
56
+ return _Sequence(fn)
57
+
58
+
59
+ def clear_factories():
60
+ _registry.clear()
61
+
62
+
63
+ def reset_sequences():
64
+ for seq in _sequences:
65
+ seq.reset()
66
+
67
+
68
+ class _Factory:
69
+ def __init__(self, name, model, body):
70
+ self.name, self.model, self.body = name, model, body
71
+ self.attributes = {}
72
+ self.traits = {}
73
+ body(_Definition(self, self.attributes))
74
+
75
+ def attributes_for(self, traits, overrides):
76
+ return _Evaluator(self.name, self.definitions(traits), overrides).attributes()
77
+
78
+ def definitions(self, traits):
79
+ definitions = dict(self.attributes)
80
+ for trait in traits:
81
+ if trait not in self.traits:
82
+ raise FactoryError(
83
+ f"Factory {self.name!r} has no trait {trait!r}.{_suggest(trait, self.traits)}"
84
+ f" Traits: {', '.join(self.traits) or '(none)'}"
85
+ )
86
+ definitions |= self.traits[trait]
87
+ return definitions
88
+
89
+
90
+ class _Sequence:
91
+ def __init__(self, fn):
92
+ self.fn = fn
93
+ self.reset()
94
+ _sequences.add(self)
95
+
96
+ def reset(self):
97
+ self.counter = itertools.count(1)
98
+
99
+ def next(self):
100
+ return self.fn(next(self.counter))
101
+
102
+
103
+ class _Definition:
104
+ """The `f` in `def user(f):`. Assigning to it declares an attribute."""
105
+
106
+ def __init__(self, factory, attributes):
107
+ object.__setattr__(self, "_factory", factory)
108
+ object.__setattr__(self, "_attributes", attributes)
109
+
110
+ def __setattr__(self, name, value):
111
+ self._attributes[name] = value
112
+
113
+ def trait(self, body):
114
+ # Run the body now, once: re-running it per build would restart its sequences.
115
+ attributes = {}
116
+ body(_Definition(self._factory, attributes))
117
+ self._factory.traits[body.__name__] = attributes
118
+ return body
119
+
120
+ def __getattr__(self, name):
121
+ raise FactoryError(
122
+ f"Factory {self._factory.name!r}: can't read f.{name} in a definition."
123
+ f" To use another attribute, take it as a parameter: lambda {name}: ..."
124
+ )
125
+
126
+
127
+ class _Evaluator:
128
+ """Resolves one build's attributes, each at most once, in dependency order."""
129
+
130
+ def __init__(self, factory_name, definitions, overrides):
131
+ self.factory_name = factory_name
132
+ self.definitions = definitions
133
+ self.values = dict(overrides) # overrides are plain values, never called
134
+ self.resolving = []
135
+
136
+ def attributes(self):
137
+ names = (self.definitions | self.values).keys() # declaration order
138
+ return {name: self.get(name) for name in names}
139
+
140
+ def get(self, name, needed_by=None):
141
+ if name in self.values:
142
+ return self.values[name]
143
+ definitions = self.definitions
144
+ if name not in definitions:
145
+ raise FactoryError(
146
+ f"Factory {self.factory_name!r}: {needed_by!r} depends on {name!r}, which is not defined."
147
+ f"{_suggest(name, definitions)}{_where(definitions[needed_by])}"
148
+ )
149
+ if name in self.resolving:
150
+ cycle = " -> ".join([*self.resolving[self.resolving.index(name) :], name])
151
+ raise FactoryError(
152
+ f"Factory {self.factory_name!r}: circular dependency: {cycle}"
153
+ )
154
+ self.resolving.append(name)
155
+ try:
156
+ self.values[name] = self.evaluate(name, definitions[name])
157
+ finally:
158
+ self.resolving.pop()
159
+ return self.values[name]
160
+
161
+ def evaluate(self, name, value):
162
+ if isinstance(value, _Sequence):
163
+ return value.next()
164
+ if callable(value):
165
+ return value(
166
+ **{dep: self.get(dep, needed_by=name) for dep in _dependencies(value)}
167
+ )
168
+ if isinstance(value, (list, dict, set)):
169
+ return copy.copy(value)
170
+ return value
171
+
172
+
173
+ def _dependencies(fn):
174
+ # Only required params: fake.paragraph(nb_sentences=3) must not be injected.
175
+ try:
176
+ params = inspect.signature(fn).parameters.values()
177
+ except (TypeError, ValueError): # some builtins (dict, str) have no signature
178
+ return []
179
+ keyword = (inspect.Parameter.POSITIONAL_OR_KEYWORD, inspect.Parameter.KEYWORD_ONLY)
180
+ return [p.name for p in params if p.kind in keyword and p.default is p.empty]
181
+
182
+
183
+ def _lookup(name):
184
+ if name not in _registry:
185
+ raise FactoryError(f"No factory named {name!r}.{_suggest(name, _registry)}")
186
+ return _registry[name]
187
+
188
+
189
+ def _is_body(model):
190
+ # Bare @factory hands us the body itself: a function of exactly one required argument.
191
+ if not inspect.isfunction(model):
192
+ return False
193
+ params = list(inspect.signature(model).parameters.values())
194
+ return (
195
+ len(params) == 1
196
+ and params[0].kind is params[0].POSITIONAL_OR_KEYWORD
197
+ and params[0].default is params[0].empty
198
+ )
199
+
200
+
201
+ def _suggest(name, options):
202
+ close = difflib.get_close_matches(name, list(options), n=1)
203
+ return f" Did you mean {close[0]!r}?" if close else ""
204
+
205
+
206
+ def _where(fn):
207
+ code = getattr(fn, "__code__", None)
208
+ return (
209
+ f" ({os.path.basename(code.co_filename)}:{code.co_firstlineno})" if code else ""
210
+ )
@@ -0,0 +1,8 @@
1
+ from faker import Faker
2
+
3
+ fake = Faker()
4
+
5
+
6
+ def seed(value):
7
+ """Seed `fake` so a test's generated data is reproducible."""
8
+ fake.seed_instance(value)
File without changes
@@ -0,0 +1,9 @@
1
+ import pytest
2
+
3
+ from factory_bot import clear_factories, reset_sequences
4
+
5
+
6
+ @pytest.fixture(autouse=True)
7
+ def clean_registry():
8
+ clear_factories()
9
+ reset_sequences()
@@ -0,0 +1,11 @@
1
+ from dataclasses import dataclass, field
2
+
3
+
4
+ @dataclass
5
+ class User:
6
+ first_name: str = ""
7
+ last_name: str = ""
8
+ email: str | None = None
9
+ username: str = ""
10
+ admin: bool = False
11
+ tags: list = field(default_factory=list)
@@ -0,0 +1,23 @@
1
+ from support.models import User
2
+
3
+ from factory_bot import attributes_for, factory, sequence
4
+
5
+
6
+ def test_attributes_for_returns_the_resolved_attributes_without_building():
7
+ @factory(User)
8
+ def user(f):
9
+ f.first_name = "John"
10
+ f.email = lambda first_name: f"{first_name}@example.com"
11
+ f.username = sequence(lambda n: f"user{n}")
12
+ f.admin = False
13
+
14
+ @f.trait
15
+ def admin(f):
16
+ f.admin = True
17
+
18
+ assert attributes_for("user", "admin", first_name="Ann") == {
19
+ "first_name": "Ann",
20
+ "email": "Ann@example.com",
21
+ "username": "user1",
22
+ "admin": True,
23
+ }
@@ -0,0 +1,105 @@
1
+ import pytest
2
+ from support.models import User
3
+
4
+ from factory_bot import FactoryError, build, factory
5
+
6
+
7
+ def test_builds_the_model_from_static_attributes():
8
+ @factory(User)
9
+ def user(f):
10
+ f.first_name = "John"
11
+ f.last_name = "Doe"
12
+
13
+ assert build("user") == User(first_name="John", last_name="Doe")
14
+
15
+
16
+ def test_any_callable_taking_kwargs_is_a_model():
17
+ @factory(dict)
18
+ def point(f):
19
+ f.x = 1
20
+ f.y = 2
21
+
22
+ assert build("point") == {"x": 1, "y": 2}
23
+
24
+
25
+ def test_a_plain_function_is_a_model_too():
26
+ def make_point(**kwargs):
27
+ return kwargs
28
+
29
+ @factory(make_point)
30
+ def point(f):
31
+ f.x = 1
32
+
33
+ assert build("point") == {"x": 1}
34
+
35
+
36
+ def test_overrides_win_and_may_add_attributes():
37
+ @factory(dict)
38
+ def point(f):
39
+ f.x = 1
40
+
41
+ assert build("point", x=5, z=3) == {"x": 5, "z": 3}
42
+
43
+
44
+ def test_an_attribute_called_name_can_be_overridden():
45
+ @factory(dict)
46
+ def person(f):
47
+ f.name = "Ann"
48
+
49
+ assert build("person", name="Bea") == {"name": "Bea"}
50
+
51
+
52
+ def test_mutable_values_are_not_shared_between_builds():
53
+ @factory(User)
54
+ def user(f):
55
+ f.tags = []
56
+
57
+ first, second = build("user"), build("user")
58
+ first.tags.append("x")
59
+ assert second.tags == []
60
+
61
+
62
+ def test_unknown_factory_suggests_the_closest_name():
63
+ @factory(User)
64
+ def user(f):
65
+ pass
66
+
67
+ with pytest.raises(
68
+ FactoryError, match=r"^No factory named 'usr'\. Did you mean 'user'\?$"
69
+ ):
70
+ build("usr")
71
+
72
+
73
+ def test_bare_decorator_names_the_fix():
74
+ with pytest.raises(
75
+ FactoryError, match=r"@factory needs a model.*@factory\(User\).*def user"
76
+ ):
77
+
78
+ @factory
79
+ def user(f):
80
+ pass
81
+
82
+
83
+ def test_model_must_be_callable():
84
+ with pytest.raises(
85
+ FactoryError, match=r"Factory 'user': model 'User' is not callable"
86
+ ):
87
+
88
+ @factory("User")
89
+ def user(f):
90
+ pass
91
+
92
+
93
+ def test_duplicate_factory_names_are_rejected():
94
+ @factory(User)
95
+ def user(f):
96
+ pass
97
+
98
+ with pytest.raises(
99
+ FactoryError,
100
+ match=r"^Factory 'user' is already defined \(test_build\.py:\d+\)$",
101
+ ):
102
+
103
+ @factory(User)
104
+ def user(f):
105
+ pass
@@ -0,0 +1,25 @@
1
+ from faker import Faker
2
+
3
+ from factory_bot import build, factory, fake, seed
4
+
5
+
6
+ def test_fake_is_a_shared_faker_whose_methods_are_lazy_values():
7
+ @factory(dict)
8
+ def user(f):
9
+ f.first_name = fake.first_name
10
+ f.bio = fake.paragraph # its parameters all have defaults, so none are injected
11
+
12
+ assert isinstance(fake, Faker)
13
+ users = [build("user") for _ in range(5)]
14
+ assert len({u["first_name"] + u["bio"] for u in users}) > 1
15
+
16
+
17
+ def test_seed_makes_fake_values_reproducible():
18
+ @factory(dict)
19
+ def user(f):
20
+ f.first_name = fake.first_name
21
+
22
+ seed(1234)
23
+ first = [build("user") for _ in range(3)]
24
+ seed(1234)
25
+ assert [build("user") for _ in range(3)] == first
@@ -0,0 +1,106 @@
1
+ import itertools
2
+ import re
3
+
4
+ import pytest
5
+
6
+ from factory_bot import FactoryError, build, factory
7
+
8
+
9
+ def test_callables_are_called_on_every_build():
10
+ counter = itertools.count(1)
11
+
12
+ @factory(dict)
13
+ def ticket(f):
14
+ f.number = lambda: next(counter)
15
+
16
+ assert [build("ticket")["number"] for _ in range(3)] == [1, 2, 3]
17
+
18
+
19
+ def test_required_parameters_are_injected_by_name():
20
+ @factory(dict)
21
+ def user(f):
22
+ f.email = lambda first_name, last_name: f"{first_name}.{last_name}@example.com"
23
+ f.first_name = "Ann"
24
+ f.last_name = "Lee"
25
+
26
+ assert build("user")["email"] == "Ann.Lee@example.com"
27
+
28
+
29
+ def test_dependencies_see_overrides():
30
+ @factory(dict)
31
+ def user(f):
32
+ f.first_name = "Ann"
33
+ f.email = lambda first_name: f"{first_name}@example.com"
34
+
35
+ assert build("user", first_name="Bea")["email"] == "Bea@example.com"
36
+
37
+
38
+ def test_parameters_with_defaults_are_left_alone():
39
+ @factory(dict)
40
+ def post(f):
41
+ f.domain = "wrong.example"
42
+ f.body = lambda nb_sentences=3, domain=None: (nb_sentences, domain)
43
+
44
+ assert build("post")["body"] == (3, None)
45
+
46
+
47
+ def test_a_callable_without_an_inspectable_signature_has_no_dependencies():
48
+ @factory(dict)
49
+ def user(f):
50
+ f.meta = dict
51
+
52
+ first, second = build("user"), build("user")
53
+ assert first["meta"] == {}
54
+ assert first["meta"] is not second["meta"]
55
+
56
+
57
+ def test_overrides_are_never_called():
58
+ @factory(dict)
59
+ def job(f):
60
+ f.run = lambda: "default"
61
+
62
+ def callback():
63
+ pass
64
+
65
+ assert build("job", run=callback)["run"] is callback
66
+
67
+
68
+ def test_an_undefined_dependency_names_the_attribute_and_suggests_a_fix():
69
+ @factory(dict)
70
+ def broken(f):
71
+ f.first_name = "x"
72
+ f.email = lambda frist_name: frist_name
73
+
74
+ message = (
75
+ "Factory 'broken': 'email' depends on 'frist_name', which is not defined."
76
+ " Did you mean 'first_name'?"
77
+ )
78
+ with pytest.raises(
79
+ FactoryError, match=rf"^{re.escape(message)} \(test_lazy\.py:\d+\)$"
80
+ ):
81
+ build("broken")
82
+
83
+
84
+ def test_a_circular_dependency_shows_the_cycle():
85
+ @factory(dict)
86
+ def cyclic(f):
87
+ f.a = lambda b: b
88
+ f.b = lambda c: c
89
+ f.c = lambda a: a
90
+
91
+ with pytest.raises(
92
+ FactoryError, match=r"^Factory 'cyclic': circular dependency: a -> b -> c -> a$"
93
+ ):
94
+ build("cyclic")
95
+
96
+
97
+ def test_reading_an_attribute_in_a_definition_points_to_injection():
98
+ with pytest.raises(
99
+ FactoryError,
100
+ match=r"Factory 'user': can't read f\.first_name.*lambda first_name:",
101
+ ):
102
+
103
+ @factory(dict)
104
+ def user(f):
105
+ f.first_name = "Ann"
106
+ f.email = f.first_name + "@example.com"
@@ -0,0 +1,5 @@
1
+ import factory_bot
2
+
3
+
4
+ def test_version():
5
+ assert factory_bot.__version__
@@ -0,0 +1,48 @@
1
+ import pytest
2
+
3
+ from factory_bot import FactoryError, build, factory, reset_sequences, sequence
4
+
5
+
6
+ def test_a_sequence_counts_up_from_one_per_build():
7
+ @factory(dict)
8
+ def user(f):
9
+ f.username = sequence(lambda n: f"user{n}")
10
+
11
+ assert [build("user")["username"] for _ in range(3)] == ["user1", "user2", "user3"]
12
+
13
+
14
+ def test_each_sequence_keeps_its_own_count():
15
+ @factory(dict)
16
+ def user(f):
17
+ f.username = sequence(lambda n: f"user{n}")
18
+ f.code = sequence(lambda n: n * 10)
19
+
20
+ build("user")
21
+ assert build("user") == {"username": "user2", "code": 20}
22
+
23
+
24
+ def test_an_override_does_not_advance_the_sequence():
25
+ @factory(dict)
26
+ def user(f):
27
+ f.username = sequence(lambda n: f"user{n}")
28
+
29
+ build("user", username="custom")
30
+ assert build("user")["username"] == "user1"
31
+
32
+
33
+ def test_reset_sequences_starts_every_count_over():
34
+ @factory(dict)
35
+ def user(f):
36
+ f.username = sequence(lambda n: f"user{n}")
37
+
38
+ build("user")
39
+ reset_sequences()
40
+ assert build("user")["username"] == "user1"
41
+
42
+
43
+ def test_a_sequence_needs_a_function_of_the_counter():
44
+ with pytest.raises(
45
+ FactoryError,
46
+ match=r"sequence\(\) takes a function of the counter, e\.g\. sequence\(lambda n:",
47
+ ):
48
+ sequence("user")
@@ -0,0 +1,73 @@
1
+ import re
2
+
3
+ import pytest
4
+ from support.models import User
5
+
6
+ from factory_bot import FactoryError, build, factory, sequence
7
+
8
+
9
+ @pytest.fixture
10
+ def user_factory():
11
+ @factory(User)
12
+ def user(f):
13
+ f.first_name = "John"
14
+ f.email = lambda first_name: f"{first_name}@example.com".lower()
15
+ f.admin = False
16
+
17
+ @f.trait
18
+ def admin(f):
19
+ f.admin = True
20
+ f.first_name = "Root"
21
+
22
+ @f.trait
23
+ def anonymous(f):
24
+ f.first_name = "Anon"
25
+ f.email = None
26
+
27
+
28
+ def test_a_trait_overrides_the_factory_and_feeds_dependencies(user_factory):
29
+ assert build("user", "admin") == User(
30
+ first_name="Root", email="root@example.com", admin=True
31
+ )
32
+
33
+
34
+ def test_traits_apply_in_order_and_later_wins(user_factory):
35
+ assert build("user", "admin", "anonymous").first_name == "Anon"
36
+ assert build("user", "anonymous", "admin").first_name == "Root"
37
+
38
+
39
+ def test_overrides_win_over_traits(user_factory):
40
+ assert build("user", "admin", admin=False, first_name="Jane").admin is False
41
+
42
+
43
+ def test_a_trait_body_runs_once_so_its_sequence_keeps_counting():
44
+ @factory(dict)
45
+ def user(f):
46
+ f.username = sequence(lambda n: f"user{n}")
47
+
48
+ @f.trait
49
+ def admin(f):
50
+ f.username = sequence(lambda n: f"admin{n}")
51
+
52
+ assert [build("user", "admin")["username"] for _ in range(3)] == [
53
+ "admin1",
54
+ "admin2",
55
+ "admin3",
56
+ ]
57
+
58
+
59
+ def test_an_unknown_trait_suggests_the_closest_and_lists_them_all(user_factory):
60
+ message = "Factory 'user' has no trait 'admn'. Did you mean 'admin'? Traits: admin, anonymous"
61
+ with pytest.raises(FactoryError, match=rf"^{re.escape(message)}$"):
62
+ build("user", "admn")
63
+
64
+
65
+ def test_a_factory_without_traits_says_so():
66
+ @factory(User)
67
+ def user(f):
68
+ pass
69
+
70
+ with pytest.raises(
71
+ FactoryError, match=r"^Factory 'user' has no trait 'admin'\. Traits: \(none\)$"
72
+ ):
73
+ build("user", "admin")