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.
- factory_bot-0.0.1/.gitignore +13 -0
- factory_bot-0.0.1/CHANGELOG.md +7 -0
- factory_bot-0.0.1/LICENSE.txt +21 -0
- factory_bot-0.0.1/PKG-INFO +60 -0
- factory_bot-0.0.1/README.md +35 -0
- factory_bot-0.0.1/pyproject.toml +59 -0
- factory_bot-0.0.1/src/factory_bot/__init__.py +25 -0
- factory_bot-0.0.1/src/factory_bot/_core.py +210 -0
- factory_bot-0.0.1/src/factory_bot/_fake.py +8 -0
- factory_bot-0.0.1/src/factory_bot/py.typed +0 -0
- factory_bot-0.0.1/tests/conftest.py +9 -0
- factory_bot-0.0.1/tests/support/models.py +11 -0
- factory_bot-0.0.1/tests/test_attributes_for.py +23 -0
- factory_bot-0.0.1/tests/test_build.py +105 -0
- factory_bot-0.0.1/tests/test_fake.py +25 -0
- factory_bot-0.0.1/tests/test_lazy.py +106 -0
- factory_bot-0.0.1/tests/test_package.py +5 -0
- factory_bot-0.0.1/tests/test_sequences.py +48 -0
- factory_bot-0.0.1/tests/test_traits.py +73 -0
|
@@ -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
|
+
[](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
|
+
[](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
|
+
)
|
|
File without changes
|
|
@@ -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,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")
|