capper 0.1.1__tar.gz → 0.3.0__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.
- {capper-0.1.1/capper.egg-info → capper-0.3.0}/PKG-INFO +39 -11
- capper-0.1.1/PKG-INFO → capper-0.3.0/README.md +29 -28
- {capper-0.1.1 → capper-0.3.0}/capper/__init__.py +23 -2
- capper-0.3.0/capper/barcode.py +15 -0
- {capper-0.1.1 → capper-0.3.0}/capper/base.py +33 -5
- capper-0.3.0/capper/cli.py +79 -0
- capper-0.3.0/capper/color.py +10 -0
- capper-0.3.0/capper/file.py +21 -0
- capper-0.3.0/capper/misc.py +9 -0
- capper-0.3.0/capper/strategies.py +100 -0
- capper-0.3.0/capper/tests/test_cli.py +41 -0
- capper-0.3.0/capper/tests/test_hypothesis_strategies.py +34 -0
- {capper-0.1.1 → capper-0.3.0}/capper/tests/test_polyfactory_integration.py +8 -6
- {capper-0.1.1 → capper-0.3.0}/capper/tests/test_types.py +80 -8
- capper-0.1.1/README.md → capper-0.3.0/capper.egg-info/PKG-INFO +56 -9
- {capper-0.1.1 → capper-0.3.0}/capper.egg-info/SOURCES.txt +9 -0
- capper-0.3.0/capper.egg-info/entry_points.txt +2 -0
- {capper-0.1.1 → capper-0.3.0}/capper.egg-info/requires.txt +10 -0
- {capper-0.1.1 → capper-0.3.0}/pyproject.toml +30 -2
- {capper-0.1.1 → capper-0.3.0}/capper/commerce.py +0 -0
- {capper-0.1.1 → capper-0.3.0}/capper/date_time.py +0 -0
- {capper-0.1.1 → capper-0.3.0}/capper/examples/user_factory.py +1 -1
- {capper-0.1.1 → capper-0.3.0}/capper/finance.py +0 -0
- {capper-0.1.1 → capper-0.3.0}/capper/geo.py +0 -0
- {capper-0.1.1 → capper-0.3.0}/capper/internet.py +0 -0
- {capper-0.1.1 → capper-0.3.0}/capper/person.py +0 -0
- {capper-0.1.1 → capper-0.3.0}/capper/phone.py +0 -0
- {capper-0.1.1 → capper-0.3.0}/capper/tests/__init__.py +0 -0
- {capper-0.1.1 → capper-0.3.0}/capper/tests/conftest.py +0 -0
- {capper-0.1.1 → capper-0.3.0}/capper/tests/test_docs_examples.py +0 -0
- {capper-0.1.1 → capper-0.3.0}/capper/text.py +0 -0
- {capper-0.1.1 → capper-0.3.0}/capper.egg-info/dependency_links.txt +0 -0
- {capper-0.1.1 → capper-0.3.0}/capper.egg-info/top_level.txt +0 -0
- {capper-0.1.1 → capper-0.3.0}/setup.cfg +0 -0
|
@@ -1,26 +1,34 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: capper
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Semantic, typed wrappers for Faker with automatic Polyfactory integration
|
|
5
5
|
Author-email: Odos Matthews <odosmatthews@gmail.com>
|
|
6
6
|
Project-URL: Documentation, https://github.com/eddiethedean/capper#readme
|
|
7
7
|
Project-URL: Repository, https://github.com/eddiethedean/capper
|
|
8
|
-
Requires-Python: >=3.
|
|
8
|
+
Requires-Python: >=3.10
|
|
9
9
|
Description-Content-Type: text/markdown
|
|
10
10
|
Requires-Dist: Faker>=20.0
|
|
11
11
|
Requires-Dist: Polyfactory>=2.0
|
|
12
12
|
Provides-Extra: pydantic
|
|
13
13
|
Requires-Dist: pydantic>=2.0; extra == "pydantic"
|
|
14
|
+
Provides-Extra: hypothesis
|
|
15
|
+
Requires-Dist: hypothesis>=6.0; extra == "hypothesis"
|
|
14
16
|
Provides-Extra: dev
|
|
15
17
|
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
16
18
|
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
17
19
|
Requires-Dist: pytest-xdist>=3.0; extra == "dev"
|
|
18
20
|
Requires-Dist: pydantic>=2.0; extra == "dev"
|
|
21
|
+
Requires-Dist: ruff>=0.4; extra == "dev"
|
|
22
|
+
Requires-Dist: mypy>=1.0; extra == "dev"
|
|
23
|
+
Requires-Dist: hypothesis>=6.0; extra == "dev"
|
|
24
|
+
Provides-Extra: docs
|
|
25
|
+
Requires-Dist: mkdocs>=1.5; extra == "docs"
|
|
26
|
+
Requires-Dist: mkdocstrings[python]>=0.24; extra == "docs"
|
|
19
27
|
|
|
20
28
|
# Capper
|
|
21
29
|
|
|
22
30
|
[](https://pypi.org/project/capper/)
|
|
23
|
-
[](https://pypi.org/project/capper/)
|
|
24
32
|
[](https://github.com/eddiethedean/capper/actions/workflows/ci.yml)
|
|
25
33
|
[](https://docs.astral.sh/ruff/)
|
|
26
34
|
[](https://mypy-lang.org/)
|
|
@@ -42,11 +50,10 @@ Semantic, typed wrappers for [Faker](https://faker.readthedocs.io/) with automat
|
|
|
42
50
|
pip install capper
|
|
43
51
|
```
|
|
44
52
|
|
|
45
|
-
Requires **Python 3.
|
|
53
|
+
Requires **Python 3.10+**, **Faker >= 20.0**, and **Polyfactory >= 2.0**. Optional extras:
|
|
46
54
|
|
|
47
|
-
|
|
48
|
-
pip install capper[
|
|
49
|
-
```
|
|
55
|
+
- **Pydantic** (for Pydantic models): `pip install capper[pydantic]`
|
|
56
|
+
- **Hypothesis** (for property-based tests with `st.from_type(...)`): `pip install capper[hypothesis]`
|
|
50
57
|
|
|
51
58
|
## Usage
|
|
52
59
|
|
|
@@ -117,6 +124,10 @@ Works automatically. No extra steps. IDE autocompletion.
|
|
|
117
124
|
- **Text**: `Paragraph`, `Sentence`
|
|
118
125
|
- **Phone**: `PhoneNumber`, `CountryCallingCode`
|
|
119
126
|
- **Finance**: `CreditCardNumber`, `CreditCardExpiry`, `CreditCardProvider`
|
|
127
|
+
- **File**: `FilePath`, `FileName`, `FileExtension`
|
|
128
|
+
- **Misc**: `UUID`
|
|
129
|
+
- **Color**: `HexColor`
|
|
130
|
+
- **Barcode**: `EAN13`, `EAN8`
|
|
120
131
|
|
|
121
132
|
Import from the top level: `from capper import Name, Email, Address, ...`
|
|
122
133
|
See [docs/FAKER_PROVIDERS.md](https://github.com/eddiethedean/capper/blob/main/docs/FAKER_PROVIDERS.md) for the Faker provider used by each type.
|
|
@@ -125,9 +136,20 @@ See [docs/FAKER_PROVIDERS.md](https://github.com/eddiethedean/capper/blob/main/d
|
|
|
125
136
|
|
|
126
137
|
**Custom types:** Subclass `FakerType`, set `faker_provider` to the Faker method name (e.g. `"company"`), and optionally `faker_kwargs`. The type auto-registers with Polyfactory when the class is defined.
|
|
127
138
|
|
|
139
|
+
## CLI
|
|
140
|
+
|
|
141
|
+
Generate fake values from the command line:
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
capper generate Name Email --count 5
|
|
145
|
+
capper generate Name Email --count 3 --seed 42
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Use `-n`/`--count` for the number of rows and `-s`/`--seed` for reproducible output. Type names are the same as the Python types (e.g. `Name`, `Email`, `Address`).
|
|
149
|
+
|
|
128
150
|
## Compatibility
|
|
129
151
|
|
|
130
|
-
Capper targets **Faker >= 20.0
|
|
152
|
+
Capper targets **Python 3.10+**, **Faker >= 20.0**, and **Polyfactory >= 2.0**. For version ranges, upgrade guidance, and the deprecation policy, see [Compatibility](docs/compatibility.md).
|
|
131
153
|
|
|
132
154
|
## Development
|
|
133
155
|
|
|
@@ -136,6 +158,8 @@ pip install -e ".[dev]"
|
|
|
136
158
|
pytest capper/tests
|
|
137
159
|
```
|
|
138
160
|
|
|
161
|
+
Lint and type-check: `ruff check .`, `ruff format .`, `mypy capper`.
|
|
162
|
+
|
|
139
163
|
Run tests with coverage: `pytest capper/tests --cov=capper --cov-report=term-missing`.
|
|
140
164
|
|
|
141
165
|
**Reproducibility:** Capper and Polyfactory share the same Faker instance, so one seed controls both capper types and built-in types (`str`, `int`, etc.):
|
|
@@ -159,20 +183,23 @@ UserFactory.seed_random(42)
|
|
|
159
183
|
user2 = UserFactory.build() # same data as user1 if you seed the same before each
|
|
160
184
|
```
|
|
161
185
|
|
|
162
|
-
Use `UserFactory.__random_seed__ = 42` to seed once when the factory class is created, or call `seed(42)` / `UserFactory.seed_random(42)` before each build for identical builds.
|
|
186
|
+
Use `UserFactory.__random_seed__ = 42` to seed once when the factory class is created, or call `seed(42)` / `UserFactory.seed_random(42)` before each build for identical builds. For a custom locale (e.g. German names), use **`use_faker(Faker('de_DE'))`** so both Capper and Polyfactory use the same Faker instance; see [Reproducible data](docs/user_guides/reproducible_data.md#locales-and-custom-faker).
|
|
163
187
|
|
|
164
188
|
## Publishing
|
|
165
189
|
|
|
166
190
|
Releases are built and published to PyPI via [GitHub Actions](https://github.com/eddiethedean/capper/blob/main/.github/workflows/publish.yml). To publish:
|
|
167
191
|
|
|
168
|
-
1.
|
|
169
|
-
2.
|
|
192
|
+
1. Update [CHANGELOG.md](CHANGELOG.md): move Unreleased entries into a new version section and date it.
|
|
193
|
+
2. Add a `PYPI_API_TOKEN` secret (PyPI API token) to the repo.
|
|
194
|
+
3. Create a GitHub release (tag e.g. `v0.3.0`). The workflow runs tests, builds the package, and uploads to PyPI.
|
|
170
195
|
|
|
171
196
|
To build and upload manually: `pip install build twine`, `python -m build`, `twine upload dist/*`.
|
|
172
197
|
|
|
173
198
|
## Documentation
|
|
174
199
|
|
|
175
200
|
- **[Docs index](docs/README.md)** — overview and links to all documentation
|
|
201
|
+
- **[API reference](docs/api.md)** — generated API docs (build with `mkdocs serve`; see [Contributing](CONTRIBUTING.md))
|
|
202
|
+
- **[Contributing](CONTRIBUTING.md)** — dev setup and how to add new types
|
|
176
203
|
- **User guides** (step-by-step, with runnable examples):
|
|
177
204
|
- [Getting started](docs/user_guides/getting_started.md) — install, first model, first factory
|
|
178
205
|
- [Models and factories](docs/user_guides/models_and_factories.md) — Pydantic, dataclasses, batches
|
|
@@ -181,3 +208,4 @@ To build and upload manually: `pip install build twine`, `python -m build`, `twi
|
|
|
181
208
|
- [Package plan](docs/capper_package_plan.md) — design and rationale
|
|
182
209
|
- [Roadmap](docs/ROADMAP.md) — development phases and status
|
|
183
210
|
- [Faker provider mapping](docs/FAKER_PROVIDERS.md) — which Faker method each type uses
|
|
211
|
+
- [Example notebooks](docs/notebooks/README.md) — Jupyter notebooks in `docs/notebooks/`
|
|
@@ -1,26 +1,7 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: capper
|
|
3
|
-
Version: 0.1.1
|
|
4
|
-
Summary: Semantic, typed wrappers for Faker with automatic Polyfactory integration
|
|
5
|
-
Author-email: Odos Matthews <odosmatthews@gmail.com>
|
|
6
|
-
Project-URL: Documentation, https://github.com/eddiethedean/capper#readme
|
|
7
|
-
Project-URL: Repository, https://github.com/eddiethedean/capper
|
|
8
|
-
Requires-Python: >=3.9
|
|
9
|
-
Description-Content-Type: text/markdown
|
|
10
|
-
Requires-Dist: Faker>=20.0
|
|
11
|
-
Requires-Dist: Polyfactory>=2.0
|
|
12
|
-
Provides-Extra: pydantic
|
|
13
|
-
Requires-Dist: pydantic>=2.0; extra == "pydantic"
|
|
14
|
-
Provides-Extra: dev
|
|
15
|
-
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
16
|
-
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
17
|
-
Requires-Dist: pytest-xdist>=3.0; extra == "dev"
|
|
18
|
-
Requires-Dist: pydantic>=2.0; extra == "dev"
|
|
19
|
-
|
|
20
1
|
# Capper
|
|
21
2
|
|
|
22
3
|
[](https://pypi.org/project/capper/)
|
|
23
|
-
[](https://pypi.org/project/capper/)
|
|
24
5
|
[](https://github.com/eddiethedean/capper/actions/workflows/ci.yml)
|
|
25
6
|
[](https://docs.astral.sh/ruff/)
|
|
26
7
|
[](https://mypy-lang.org/)
|
|
@@ -42,11 +23,10 @@ Semantic, typed wrappers for [Faker](https://faker.readthedocs.io/) with automat
|
|
|
42
23
|
pip install capper
|
|
43
24
|
```
|
|
44
25
|
|
|
45
|
-
Requires **Python 3.
|
|
26
|
+
Requires **Python 3.10+**, **Faker >= 20.0**, and **Polyfactory >= 2.0**. Optional extras:
|
|
46
27
|
|
|
47
|
-
|
|
48
|
-
pip install capper[
|
|
49
|
-
```
|
|
28
|
+
- **Pydantic** (for Pydantic models): `pip install capper[pydantic]`
|
|
29
|
+
- **Hypothesis** (for property-based tests with `st.from_type(...)`): `pip install capper[hypothesis]`
|
|
50
30
|
|
|
51
31
|
## Usage
|
|
52
32
|
|
|
@@ -117,6 +97,10 @@ Works automatically. No extra steps. IDE autocompletion.
|
|
|
117
97
|
- **Text**: `Paragraph`, `Sentence`
|
|
118
98
|
- **Phone**: `PhoneNumber`, `CountryCallingCode`
|
|
119
99
|
- **Finance**: `CreditCardNumber`, `CreditCardExpiry`, `CreditCardProvider`
|
|
100
|
+
- **File**: `FilePath`, `FileName`, `FileExtension`
|
|
101
|
+
- **Misc**: `UUID`
|
|
102
|
+
- **Color**: `HexColor`
|
|
103
|
+
- **Barcode**: `EAN13`, `EAN8`
|
|
120
104
|
|
|
121
105
|
Import from the top level: `from capper import Name, Email, Address, ...`
|
|
122
106
|
See [docs/FAKER_PROVIDERS.md](https://github.com/eddiethedean/capper/blob/main/docs/FAKER_PROVIDERS.md) for the Faker provider used by each type.
|
|
@@ -125,9 +109,20 @@ See [docs/FAKER_PROVIDERS.md](https://github.com/eddiethedean/capper/blob/main/d
|
|
|
125
109
|
|
|
126
110
|
**Custom types:** Subclass `FakerType`, set `faker_provider` to the Faker method name (e.g. `"company"`), and optionally `faker_kwargs`. The type auto-registers with Polyfactory when the class is defined.
|
|
127
111
|
|
|
112
|
+
## CLI
|
|
113
|
+
|
|
114
|
+
Generate fake values from the command line:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
capper generate Name Email --count 5
|
|
118
|
+
capper generate Name Email --count 3 --seed 42
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Use `-n`/`--count` for the number of rows and `-s`/`--seed` for reproducible output. Type names are the same as the Python types (e.g. `Name`, `Email`, `Address`).
|
|
122
|
+
|
|
128
123
|
## Compatibility
|
|
129
124
|
|
|
130
|
-
Capper targets **Faker >= 20.0
|
|
125
|
+
Capper targets **Python 3.10+**, **Faker >= 20.0**, and **Polyfactory >= 2.0**. For version ranges, upgrade guidance, and the deprecation policy, see [Compatibility](docs/compatibility.md).
|
|
131
126
|
|
|
132
127
|
## Development
|
|
133
128
|
|
|
@@ -136,6 +131,8 @@ pip install -e ".[dev]"
|
|
|
136
131
|
pytest capper/tests
|
|
137
132
|
```
|
|
138
133
|
|
|
134
|
+
Lint and type-check: `ruff check .`, `ruff format .`, `mypy capper`.
|
|
135
|
+
|
|
139
136
|
Run tests with coverage: `pytest capper/tests --cov=capper --cov-report=term-missing`.
|
|
140
137
|
|
|
141
138
|
**Reproducibility:** Capper and Polyfactory share the same Faker instance, so one seed controls both capper types and built-in types (`str`, `int`, etc.):
|
|
@@ -159,20 +156,23 @@ UserFactory.seed_random(42)
|
|
|
159
156
|
user2 = UserFactory.build() # same data as user1 if you seed the same before each
|
|
160
157
|
```
|
|
161
158
|
|
|
162
|
-
Use `UserFactory.__random_seed__ = 42` to seed once when the factory class is created, or call `seed(42)` / `UserFactory.seed_random(42)` before each build for identical builds.
|
|
159
|
+
Use `UserFactory.__random_seed__ = 42` to seed once when the factory class is created, or call `seed(42)` / `UserFactory.seed_random(42)` before each build for identical builds. For a custom locale (e.g. German names), use **`use_faker(Faker('de_DE'))`** so both Capper and Polyfactory use the same Faker instance; see [Reproducible data](docs/user_guides/reproducible_data.md#locales-and-custom-faker).
|
|
163
160
|
|
|
164
161
|
## Publishing
|
|
165
162
|
|
|
166
163
|
Releases are built and published to PyPI via [GitHub Actions](https://github.com/eddiethedean/capper/blob/main/.github/workflows/publish.yml). To publish:
|
|
167
164
|
|
|
168
|
-
1.
|
|
169
|
-
2.
|
|
165
|
+
1. Update [CHANGELOG.md](CHANGELOG.md): move Unreleased entries into a new version section and date it.
|
|
166
|
+
2. Add a `PYPI_API_TOKEN` secret (PyPI API token) to the repo.
|
|
167
|
+
3. Create a GitHub release (tag e.g. `v0.3.0`). The workflow runs tests, builds the package, and uploads to PyPI.
|
|
170
168
|
|
|
171
169
|
To build and upload manually: `pip install build twine`, `python -m build`, `twine upload dist/*`.
|
|
172
170
|
|
|
173
171
|
## Documentation
|
|
174
172
|
|
|
175
173
|
- **[Docs index](docs/README.md)** — overview and links to all documentation
|
|
174
|
+
- **[API reference](docs/api.md)** — generated API docs (build with `mkdocs serve`; see [Contributing](CONTRIBUTING.md))
|
|
175
|
+
- **[Contributing](CONTRIBUTING.md)** — dev setup and how to add new types
|
|
176
176
|
- **User guides** (step-by-step, with runnable examples):
|
|
177
177
|
- [Getting started](docs/user_guides/getting_started.md) — install, first model, first factory
|
|
178
178
|
- [Models and factories](docs/user_guides/models_and_factories.md) — Pydantic, dataclasses, batches
|
|
@@ -181,3 +181,4 @@ To build and upload manually: `pip install build twine`, `python -m build`, `twi
|
|
|
181
181
|
- [Package plan](docs/capper_package_plan.md) — design and rationale
|
|
182
182
|
- [Roadmap](docs/ROADMAP.md) — development phases and status
|
|
183
183
|
- [Faker provider mapping](docs/FAKER_PROVIDERS.md) — which Faker method each type uses
|
|
184
|
+
- [Example notebooks](docs/notebooks/README.md) — Jupyter notebooks in `docs/notebooks/`
|
|
@@ -2,14 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
Import types (e.g. ``Name``, ``Email``) and use them in Pydantic models, dataclasses,
|
|
4
4
|
or attrs; Polyfactory will generate values via Faker. Use ``seed(n)`` for reproducibility.
|
|
5
|
+
With Hypothesis installed (pip install capper[hypothesis]), import capper.strategies
|
|
6
|
+
and use st.from_type(Name) for property-based tests.
|
|
5
7
|
"""
|
|
6
8
|
|
|
7
|
-
|
|
9
|
+
try:
|
|
10
|
+
from importlib.metadata import version as _version
|
|
11
|
+
|
|
12
|
+
__version__ = _version("capper")
|
|
13
|
+
except Exception: # Package not installed (e.g. dev tree) or metadata missing
|
|
14
|
+
__version__ = "0.3.0"
|
|
15
|
+
|
|
16
|
+
from .barcode import EAN8, EAN13
|
|
17
|
+
from .base import FakerType, faker, seed, use_faker
|
|
18
|
+
from .color import HexColor
|
|
8
19
|
from .commerce import Company, Currency, Price, Product
|
|
9
20
|
from .date_time import Date, DateTime, Time
|
|
21
|
+
from .file import FileExtension, FileName, FilePath
|
|
10
22
|
from .finance import CreditCardExpiry, CreditCardNumber, CreditCardProvider
|
|
11
23
|
from .geo import Address, City, Country
|
|
12
|
-
from .internet import
|
|
24
|
+
from .internet import IP, URL, Email, UserName
|
|
25
|
+
from .misc import UUID
|
|
13
26
|
from .person import FirstName, Job, LastName, Name
|
|
14
27
|
from .phone import CountryCallingCode, PhoneNumber
|
|
15
28
|
from .text import Paragraph, Sentence
|
|
@@ -26,9 +39,15 @@ __all__ = [
|
|
|
26
39
|
"Currency",
|
|
27
40
|
"Date",
|
|
28
41
|
"DateTime",
|
|
42
|
+
"EAN13",
|
|
43
|
+
"EAN8",
|
|
29
44
|
"Email",
|
|
30
45
|
"FakerType",
|
|
46
|
+
"FileExtension",
|
|
47
|
+
"FileName",
|
|
48
|
+
"FilePath",
|
|
31
49
|
"FirstName",
|
|
50
|
+
"HexColor",
|
|
32
51
|
"IP",
|
|
33
52
|
"Job",
|
|
34
53
|
"LastName",
|
|
@@ -40,7 +59,9 @@ __all__ = [
|
|
|
40
59
|
"Sentence",
|
|
41
60
|
"Time",
|
|
42
61
|
"URL",
|
|
62
|
+
"UUID",
|
|
43
63
|
"UserName",
|
|
44
64
|
"faker",
|
|
45
65
|
"seed",
|
|
66
|
+
"use_faker",
|
|
46
67
|
]
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Barcode-related semantic Faker types."""
|
|
2
|
+
|
|
3
|
+
from .base import FakerType
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class EAN13(FakerType):
|
|
7
|
+
"""EAN-13 barcode. Uses Faker provider ``ean13``."""
|
|
8
|
+
|
|
9
|
+
faker_provider = "ean13"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class EAN8(FakerType):
|
|
13
|
+
"""EAN-8 barcode. Uses Faker provider ``ean8``."""
|
|
14
|
+
|
|
15
|
+
faker_provider = "ean8"
|
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
The module-level ``faker`` is attached to Polyfactory's BaseFactory so that one seed
|
|
4
4
|
controls both capper types and built-in types. Use ``seed(n)`` for reproducible data.
|
|
5
|
+
|
|
6
|
+
Note: The shared ``faker`` and ``use_faker()`` are not thread-safe; do not switch
|
|
7
|
+
the global Faker from multiple threads while building models.
|
|
5
8
|
"""
|
|
6
9
|
|
|
7
10
|
from typing import Any
|
|
@@ -24,6 +27,24 @@ def seed(seed_value: int) -> None:
|
|
|
24
27
|
faker.seed_instance(seed_value)
|
|
25
28
|
|
|
26
29
|
|
|
30
|
+
def use_faker(instance: Faker) -> None:
|
|
31
|
+
"""Use a custom Faker instance for both Capper and Polyfactory.
|
|
32
|
+
|
|
33
|
+
Replaces the module-level faker and Polyfactory's BaseFactory.__faker__
|
|
34
|
+
so that one instance (e.g. a locale-specific Faker) is used everywhere.
|
|
35
|
+
Call before building any models.
|
|
36
|
+
|
|
37
|
+
Note: The shared faker is not thread-safe. Do not call use_faker() from
|
|
38
|
+
multiple threads while other threads are building models.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
instance: The Faker instance to use (e.g. Faker('de_DE')).
|
|
42
|
+
"""
|
|
43
|
+
global faker
|
|
44
|
+
faker = instance
|
|
45
|
+
BaseFactory.__faker__ = instance
|
|
46
|
+
|
|
47
|
+
|
|
27
48
|
def _install_pydantic_schema() -> None:
|
|
28
49
|
"""If Pydantic is available, attach __get_pydantic_core_schema__ to FakerType."""
|
|
29
50
|
try:
|
|
@@ -32,9 +53,7 @@ def _install_pydantic_schema() -> None:
|
|
|
32
53
|
except ImportError:
|
|
33
54
|
return
|
|
34
55
|
|
|
35
|
-
def __get_pydantic_core_schema__(
|
|
36
|
-
source_type: Any, handler: GetCoreSchemaHandler
|
|
37
|
-
) -> CoreSchema:
|
|
56
|
+
def __get_pydantic_core_schema__(source_type: Any, handler: GetCoreSchemaHandler) -> CoreSchema:
|
|
38
57
|
"""Validate as str then coerce to the FakerType subclass."""
|
|
39
58
|
return core_schema.no_info_after_validator_function(source_type, handler(str))
|
|
40
59
|
|
|
@@ -42,11 +61,12 @@ def _install_pydantic_schema() -> None:
|
|
|
42
61
|
|
|
43
62
|
|
|
44
63
|
class FakerType(str):
|
|
45
|
-
"""Base class for semantic Faker types; subclasses
|
|
64
|
+
"""Base class for semantic Faker types; subclasses auto-register with Polyfactory.
|
|
46
65
|
|
|
47
66
|
Subclasses must set a non-empty ``faker_provider`` (the Faker method name).
|
|
48
67
|
Optional ``faker_kwargs`` is a dict of keyword arguments passed to that provider
|
|
49
68
|
(e.g. ``faker_kwargs = {"nb_words": 10}`` for ``sentence``).
|
|
69
|
+
When Hypothesis is installed, use ``st.from_type(YourFakerType)`` for property-based tests.
|
|
50
70
|
"""
|
|
51
71
|
|
|
52
72
|
faker_provider: str = ""
|
|
@@ -63,7 +83,15 @@ _install_pydantic_schema()
|
|
|
63
83
|
|
|
64
84
|
def _register(cls: type, provider_name: str) -> None:
|
|
65
85
|
"""Register a FakerType subclass with Polyfactory so factories can generate values."""
|
|
66
|
-
|
|
86
|
+
if not hasattr(faker, provider_name):
|
|
87
|
+
raise AttributeError(
|
|
88
|
+
f"Faker has no provider {provider_name!r} (used by {cls.__name__}). "
|
|
89
|
+
"Check faker_provider on the type."
|
|
90
|
+
)
|
|
91
|
+
provider_fn = getattr(faker, provider_name)
|
|
92
|
+
if not callable(provider_fn):
|
|
93
|
+
raise TypeError(f"Faker.{provider_name} is not callable (used by {cls.__name__}).")
|
|
94
|
+
provider_kwargs = dict(getattr(cls, "faker_kwargs", None) or {})
|
|
67
95
|
|
|
68
96
|
def _provide() -> str:
|
|
69
97
|
value = getattr(faker, provider_name)(**provider_kwargs)
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""CLI for ad-hoc fake data generation."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import sys
|
|
5
|
+
from typing import Type
|
|
6
|
+
|
|
7
|
+
import capper
|
|
8
|
+
|
|
9
|
+
from .base import FakerType, faker, seed
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _type_registry() -> dict[str, Type[FakerType]]:
|
|
13
|
+
"""Map type name -> FakerType subclass (only types with faker_provider)."""
|
|
14
|
+
registry: dict[str, Type[FakerType]] = {}
|
|
15
|
+
for name in getattr(capper, "__all__", []):
|
|
16
|
+
if name in ("FakerType", "faker", "seed", "use_faker"):
|
|
17
|
+
continue
|
|
18
|
+
obj = getattr(capper, name, None)
|
|
19
|
+
provider = getattr(obj, "faker_provider", None)
|
|
20
|
+
if (
|
|
21
|
+
isinstance(obj, type)
|
|
22
|
+
and issubclass(obj, FakerType)
|
|
23
|
+
and isinstance(provider, str)
|
|
24
|
+
and len(provider) > 0
|
|
25
|
+
):
|
|
26
|
+
registry[name] = obj
|
|
27
|
+
return registry
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _generate_one(typ: Type[FakerType]) -> str:
|
|
31
|
+
"""Generate a single value for the given FakerType."""
|
|
32
|
+
provider = getattr(typ, "faker_provider", "")
|
|
33
|
+
kwargs = getattr(typ, "faker_kwargs", None) or {}
|
|
34
|
+
value = getattr(faker, provider)(**kwargs)
|
|
35
|
+
return str(value)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def cmd_generate(args: argparse.Namespace) -> int:
|
|
39
|
+
"""Run generate subcommand."""
|
|
40
|
+
registry = _type_registry()
|
|
41
|
+
types: list[Type[FakerType]] = []
|
|
42
|
+
for name in args.types:
|
|
43
|
+
if name not in registry:
|
|
44
|
+
known = ", ".join(sorted(registry))
|
|
45
|
+
print(f"Unknown type: {name}. Known: {known}", file=sys.stderr)
|
|
46
|
+
return 1
|
|
47
|
+
types.append(registry[name])
|
|
48
|
+
|
|
49
|
+
if args.seed is not None:
|
|
50
|
+
seed(args.seed)
|
|
51
|
+
|
|
52
|
+
count = max(0, args.count)
|
|
53
|
+
for _ in range(count):
|
|
54
|
+
row = [_generate_one(t) for t in types]
|
|
55
|
+
print("\t".join(row))
|
|
56
|
+
return 0
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def main() -> int:
|
|
60
|
+
"""Entry point for the capper CLI."""
|
|
61
|
+
parser = argparse.ArgumentParser(
|
|
62
|
+
prog="capper", description="Capper: fake data via Faker types."
|
|
63
|
+
)
|
|
64
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
65
|
+
|
|
66
|
+
gen = subparsers.add_parser("generate", help="Generate fake values for one or more types.")
|
|
67
|
+
gen.add_argument(
|
|
68
|
+
"types",
|
|
69
|
+
nargs="+",
|
|
70
|
+
metavar="TYPE",
|
|
71
|
+
help="Type names (e.g. Name, Email).",
|
|
72
|
+
)
|
|
73
|
+
gen.add_argument("-n", "--count", type=int, default=1, help="Number of rows (default: 1).")
|
|
74
|
+
gen.add_argument("-s", "--seed", type=int, default=None, help="Seed for reproducible output.")
|
|
75
|
+
gen.set_defaults(func=cmd_generate)
|
|
76
|
+
|
|
77
|
+
args = parser.parse_args()
|
|
78
|
+
result: int = args.func(args)
|
|
79
|
+
return result
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""Color-related semantic Faker types."""
|
|
2
|
+
|
|
3
|
+
from .base import FakerType
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class HexColor(FakerType):
|
|
7
|
+
"""Hex color string (e.g. #ff5500). Uses Faker provider ``color`` with hex format."""
|
|
8
|
+
|
|
9
|
+
faker_provider = "color"
|
|
10
|
+
faker_kwargs = {"color_format": "hex"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""File-related semantic Faker types (path, name, extension)."""
|
|
2
|
+
|
|
3
|
+
from .base import FakerType
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class FilePath(FakerType):
|
|
7
|
+
"""File path. Uses Faker provider ``file_path``."""
|
|
8
|
+
|
|
9
|
+
faker_provider = "file_path"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class FileName(FakerType):
|
|
13
|
+
"""File name with extension. Uses Faker provider ``file_name``."""
|
|
14
|
+
|
|
15
|
+
faker_provider = "file_name"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class FileExtension(FakerType):
|
|
19
|
+
"""File extension. Uses Faker provider ``file_extension``."""
|
|
20
|
+
|
|
21
|
+
faker_provider = "file_extension"
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""Hypothesis strategies for Capper types (optional; requires hypothesis>=6.0).
|
|
2
|
+
|
|
3
|
+
Use ``st.from_type(Name)`` after importing capper and capper.strategies, or call
|
|
4
|
+
``strategies.for_type(Name)`` to get a strategy that generates instances of that type.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Type, TypeVar, cast
|
|
10
|
+
|
|
11
|
+
from hypothesis import strategies as st
|
|
12
|
+
|
|
13
|
+
from .barcode import EAN8, EAN13
|
|
14
|
+
from .base import FakerType, faker
|
|
15
|
+
from .color import HexColor
|
|
16
|
+
from .commerce import Company, Currency, Price, Product
|
|
17
|
+
from .date_time import Date, DateTime, Time
|
|
18
|
+
from .file import FileExtension, FileName, FilePath
|
|
19
|
+
from .finance import CreditCardExpiry, CreditCardNumber, CreditCardProvider
|
|
20
|
+
from .geo import Address, City, Country
|
|
21
|
+
from .internet import IP, URL, Email, UserName
|
|
22
|
+
from .misc import UUID
|
|
23
|
+
from .person import FirstName, Job, LastName, Name
|
|
24
|
+
from .phone import CountryCallingCode, PhoneNumber
|
|
25
|
+
from .text import Paragraph, Sentence
|
|
26
|
+
|
|
27
|
+
T = TypeVar("T", bound=FakerType)
|
|
28
|
+
|
|
29
|
+
# All built-in FakerType subclasses for registration
|
|
30
|
+
_BUILTIN_TYPES: tuple[type[FakerType], ...] = (
|
|
31
|
+
Address,
|
|
32
|
+
City,
|
|
33
|
+
Company,
|
|
34
|
+
Country,
|
|
35
|
+
CountryCallingCode,
|
|
36
|
+
CreditCardExpiry,
|
|
37
|
+
CreditCardNumber,
|
|
38
|
+
CreditCardProvider,
|
|
39
|
+
Currency,
|
|
40
|
+
Date,
|
|
41
|
+
DateTime,
|
|
42
|
+
EAN13,
|
|
43
|
+
EAN8,
|
|
44
|
+
Email,
|
|
45
|
+
FileExtension,
|
|
46
|
+
FileName,
|
|
47
|
+
FilePath,
|
|
48
|
+
FirstName,
|
|
49
|
+
HexColor,
|
|
50
|
+
IP,
|
|
51
|
+
Job,
|
|
52
|
+
LastName,
|
|
53
|
+
Name,
|
|
54
|
+
Paragraph,
|
|
55
|
+
PhoneNumber,
|
|
56
|
+
Price,
|
|
57
|
+
Product,
|
|
58
|
+
Sentence,
|
|
59
|
+
Time,
|
|
60
|
+
URL,
|
|
61
|
+
UUID,
|
|
62
|
+
UserName,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def for_type(cls: Type[T]) -> st.SearchStrategy[T]:
|
|
67
|
+
"""Return a Hypothesis strategy that generates instances of the given FakerType subclass."""
|
|
68
|
+
provider = getattr(cls, "faker_provider", None)
|
|
69
|
+
if not provider:
|
|
70
|
+
raise ValueError(f"{cls.__name__} has no faker_provider")
|
|
71
|
+
if not hasattr(faker, provider):
|
|
72
|
+
raise AttributeError(
|
|
73
|
+
f"Faker has no provider {provider!r} (used by {cls.__name__}). "
|
|
74
|
+
"Check faker_provider on the type."
|
|
75
|
+
)
|
|
76
|
+
if not callable(getattr(faker, provider)):
|
|
77
|
+
raise TypeError(f"Faker.{provider} is not callable (used by {cls.__name__}).")
|
|
78
|
+
kwargs = getattr(cls, "faker_kwargs", None) or {}
|
|
79
|
+
|
|
80
|
+
@st.composite
|
|
81
|
+
def _draw(draw: st.DrawFn) -> T:
|
|
82
|
+
seed_val = draw(st.integers(min_value=0, max_value=2**32 - 1))
|
|
83
|
+
faker.seed_instance(seed_val)
|
|
84
|
+
value = getattr(faker, provider)(**kwargs)
|
|
85
|
+
return cls(str(value))
|
|
86
|
+
|
|
87
|
+
return cast(st.SearchStrategy[T], _draw())
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _register_strategies() -> None:
|
|
91
|
+
"""Register all built-in Capper types with Hypothesis so st.from_type() works."""
|
|
92
|
+
from hypothesis.strategies import register_type_strategy
|
|
93
|
+
|
|
94
|
+
for typ in _BUILTIN_TYPES:
|
|
95
|
+
register_type_strategy(typ, for_type(typ))
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
_register_strategies()
|
|
99
|
+
|
|
100
|
+
__all__ = ["for_type"]
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Tests for the capper CLI (generate subcommand)."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
if sys.version_info >= (3, 10):
|
|
8
|
+
from capper import cli
|
|
9
|
+
else:
|
|
10
|
+
cli = None # type: ignore[assignment]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@pytest.mark.skipif(sys.version_info < (3, 10), reason="capper CLI uses Python 3.10+ syntax")
|
|
14
|
+
def test_cli_generate_exit_zero_and_non_empty_output() -> None:
|
|
15
|
+
"""Invoke generate subcommand; assert exit 0 and non-empty output for a known type."""
|
|
16
|
+
orig_argv = sys.argv
|
|
17
|
+
try:
|
|
18
|
+
sys.argv = ["capper", "generate", "Name", "--count", "1"]
|
|
19
|
+
exit_code = cli.main()
|
|
20
|
+
assert exit_code == 0
|
|
21
|
+
finally:
|
|
22
|
+
sys.argv = orig_argv
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@pytest.mark.skipif(sys.version_info < (3, 10), reason="capper CLI uses Python 3.10+ syntax")
|
|
26
|
+
def test_cli_generate_produces_output(capsys: pytest.CaptureFixture[str]) -> None:
|
|
27
|
+
"""Generate subcommand prints one tab-separated value per row."""
|
|
28
|
+
orig_argv = sys.argv
|
|
29
|
+
try:
|
|
30
|
+
sys.argv = ["capper", "generate", "Name", "Email", "--count", "2"]
|
|
31
|
+
exit_code = cli.main()
|
|
32
|
+
assert exit_code == 0
|
|
33
|
+
out = capsys.readouterr().out
|
|
34
|
+
lines = [line for line in out.strip().splitlines() if line.strip()]
|
|
35
|
+
assert len(lines) == 2
|
|
36
|
+
for line in lines:
|
|
37
|
+
parts = line.split("\t")
|
|
38
|
+
assert len(parts) == 2
|
|
39
|
+
assert len(parts[0]) > 0 and len(parts[1]) > 0
|
|
40
|
+
finally:
|
|
41
|
+
sys.argv = orig_argv
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Tests for Hypothesis strategies (require capper[hypothesis])."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
pytest.importorskip("hypothesis")
|
|
6
|
+
|
|
7
|
+
from hypothesis import given
|
|
8
|
+
from hypothesis import strategies as st
|
|
9
|
+
|
|
10
|
+
import capper.strategies # noqa: F401 - register types with Hypothesis
|
|
11
|
+
from capper import Email, Name
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@given(st.from_type(Name))
|
|
15
|
+
def test_from_type_name_produces_non_empty_string(name: Name) -> None:
|
|
16
|
+
"""st.from_type(Name) yields non-empty Name instances."""
|
|
17
|
+
assert isinstance(name, Name)
|
|
18
|
+
assert isinstance(name, str)
|
|
19
|
+
assert len(name) > 0
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@given(st.from_type(Email))
|
|
23
|
+
def test_from_type_email_contains_at(email: Email) -> None:
|
|
24
|
+
"""st.from_type(Email) yields strings containing '@'."""
|
|
25
|
+
assert isinstance(email, Email)
|
|
26
|
+
assert "@" in email
|
|
27
|
+
assert len(email) > 0
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@given(capper.strategies.for_type(Name))
|
|
31
|
+
def test_for_type_name_produces_name(name: Name) -> None:
|
|
32
|
+
"""strategies.for_type(Name) yields Name instances."""
|
|
33
|
+
assert isinstance(name, Name)
|
|
34
|
+
assert len(name) > 0
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
"""Tests for Polyfactory
|
|
1
|
+
"""Tests for Polyfactory: ModelFactory, DataclassFactory, shared Faker, auto-registration."""
|
|
2
2
|
|
|
3
3
|
from dataclasses import dataclass
|
|
4
4
|
|
|
5
|
-
from pydantic import BaseModel
|
|
6
5
|
from polyfactory.factories import DataclassFactory
|
|
7
6
|
from polyfactory.factories.pydantic_factory import ModelFactory
|
|
7
|
+
from pydantic import BaseModel
|
|
8
8
|
|
|
9
9
|
from capper import Email, Name
|
|
10
10
|
|
|
@@ -50,14 +50,15 @@ def test_seed_random_and_capper_seed_produce_same_value() -> None:
|
|
|
50
50
|
|
|
51
51
|
def test_model_factory_uses_capper_faker() -> None:
|
|
52
52
|
"""Asserts ModelFactory.__faker__ is capper's faker to document the shared instance."""
|
|
53
|
-
import capper
|
|
54
53
|
from polyfactory.factories.pydantic_factory import ModelFactory
|
|
55
54
|
|
|
55
|
+
import capper
|
|
56
|
+
|
|
56
57
|
assert ModelFactory.__faker__ is capper.faker
|
|
57
58
|
|
|
58
59
|
|
|
59
60
|
def test_dataclass_factory_builds_with_capper_types() -> None:
|
|
60
|
-
"""
|
|
61
|
+
"""Dataclass with Name/Email via DataclassFactory; asserts non-empty and valid email."""
|
|
61
62
|
|
|
62
63
|
@dataclass
|
|
63
64
|
class Person:
|
|
@@ -94,11 +95,12 @@ def test_dataclass_factory_batch() -> None:
|
|
|
94
95
|
|
|
95
96
|
|
|
96
97
|
def test_capper_types_auto_registered_with_polyfactory() -> None:
|
|
97
|
-
"""
|
|
98
|
-
from capper import PhoneNumber
|
|
98
|
+
"""Importing capper registers types so Polyfactory can build a model with PhoneNumber."""
|
|
99
99
|
from polyfactory.factories.pydantic_factory import ModelFactory
|
|
100
100
|
from pydantic import BaseModel
|
|
101
101
|
|
|
102
|
+
from capper import PhoneNumber
|
|
103
|
+
|
|
102
104
|
class Contact(BaseModel):
|
|
103
105
|
phone: PhoneNumber
|
|
104
106
|
|
|
@@ -3,6 +3,11 @@
|
|
|
3
3
|
import pytest
|
|
4
4
|
|
|
5
5
|
from capper import (
|
|
6
|
+
EAN8,
|
|
7
|
+
EAN13,
|
|
8
|
+
IP,
|
|
9
|
+
URL,
|
|
10
|
+
UUID,
|
|
6
11
|
Address,
|
|
7
12
|
City,
|
|
8
13
|
Company,
|
|
@@ -15,8 +20,11 @@ from capper import (
|
|
|
15
20
|
Date,
|
|
16
21
|
DateTime,
|
|
17
22
|
Email,
|
|
23
|
+
FileExtension,
|
|
24
|
+
FileName,
|
|
25
|
+
FilePath,
|
|
18
26
|
FirstName,
|
|
19
|
-
|
|
27
|
+
HexColor,
|
|
20
28
|
Job,
|
|
21
29
|
LastName,
|
|
22
30
|
Name,
|
|
@@ -26,7 +34,6 @@ from capper import (
|
|
|
26
34
|
Product,
|
|
27
35
|
Sentence,
|
|
28
36
|
Time,
|
|
29
|
-
URL,
|
|
30
37
|
UserName,
|
|
31
38
|
)
|
|
32
39
|
|
|
@@ -59,25 +66,45 @@ from capper import (
|
|
|
59
66
|
CreditCardNumber,
|
|
60
67
|
CreditCardExpiry,
|
|
61
68
|
CreditCardProvider,
|
|
69
|
+
FilePath,
|
|
70
|
+
FileName,
|
|
71
|
+
FileExtension,
|
|
72
|
+
UUID,
|
|
73
|
+
HexColor,
|
|
74
|
+
EAN13,
|
|
75
|
+
EAN8,
|
|
62
76
|
],
|
|
63
77
|
)
|
|
64
78
|
def test_type_generates_non_empty_string(type_class: type) -> None:
|
|
65
|
-
"""
|
|
79
|
+
"""Each type's Faker provider exists and returns a non-empty value."""
|
|
66
80
|
from faker import Faker
|
|
67
81
|
|
|
68
82
|
faker = Faker()
|
|
69
83
|
provider_name = getattr(type_class, "faker_provider")
|
|
70
|
-
|
|
84
|
+
kwargs = getattr(type_class, "faker_kwargs", None) or {}
|
|
85
|
+
value = getattr(faker, provider_name)(**kwargs)
|
|
71
86
|
assert isinstance(value, (str, type_class))
|
|
72
87
|
assert len(str(value)) > 0
|
|
73
88
|
|
|
74
89
|
|
|
75
90
|
@pytest.mark.parametrize(
|
|
76
91
|
"type_class",
|
|
77
|
-
[
|
|
92
|
+
[
|
|
93
|
+
Name,
|
|
94
|
+
Email,
|
|
95
|
+
PhoneNumber,
|
|
96
|
+
FirstName,
|
|
97
|
+
Address,
|
|
98
|
+
Sentence,
|
|
99
|
+
CreditCardNumber,
|
|
100
|
+
FilePath,
|
|
101
|
+
UUID,
|
|
102
|
+
HexColor,
|
|
103
|
+
EAN13,
|
|
104
|
+
],
|
|
78
105
|
)
|
|
79
106
|
def test_model_factory_builds_capper_type(type_class: type) -> None:
|
|
80
|
-
"""
|
|
107
|
+
"""Pydantic model with one capper type via ModelFactory; asserts non-empty and type."""
|
|
81
108
|
from typing import Any, Type
|
|
82
109
|
|
|
83
110
|
from polyfactory.factories.pydantic_factory import ModelFactory
|
|
@@ -94,7 +121,7 @@ def test_model_factory_builds_capper_type(type_class: type) -> None:
|
|
|
94
121
|
|
|
95
122
|
|
|
96
123
|
def test_faker_kwargs_support() -> None:
|
|
97
|
-
"""
|
|
124
|
+
"""faker_kwargs passed to provider; ModelFactory builds and checks value."""
|
|
98
125
|
from capper.base import FakerType
|
|
99
126
|
|
|
100
127
|
class ShortSentence(FakerType):
|
|
@@ -121,12 +148,22 @@ def test_faker_kwargs_support() -> None:
|
|
|
121
148
|
assert isinstance(instance.text, (str, ShortSentence)) and len(instance.text) > 0
|
|
122
149
|
|
|
123
150
|
|
|
151
|
+
def test_hex_color_format() -> None:
|
|
152
|
+
"""HexColor yields a string starting with #."""
|
|
153
|
+
from faker import Faker
|
|
154
|
+
|
|
155
|
+
faker = Faker()
|
|
156
|
+
value = faker.color(color_format="hex")
|
|
157
|
+
assert isinstance(value, str) and value.startswith("#") and len(value) == 7
|
|
158
|
+
|
|
159
|
+
|
|
124
160
|
def test_seed_reproducibility(seeded_faker: None) -> None:
|
|
125
161
|
"""Builds twice after seeding with same value; asserts identical generated name."""
|
|
126
|
-
from capper import Name, seed
|
|
127
162
|
from polyfactory.factories.pydantic_factory import ModelFactory
|
|
128
163
|
from pydantic import BaseModel
|
|
129
164
|
|
|
165
|
+
from capper import Name, seed
|
|
166
|
+
|
|
130
167
|
class User(BaseModel):
|
|
131
168
|
name: Name
|
|
132
169
|
|
|
@@ -138,3 +175,38 @@ def test_seed_reproducibility(seeded_faker: None) -> None:
|
|
|
138
175
|
seed(42)
|
|
139
176
|
user2 = UserFactory.build()
|
|
140
177
|
assert user1.name == user2.name
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def test_use_faker_switches_global_faker() -> None:
|
|
181
|
+
"""use_faker() switches the global Faker; next factory build uses that instance (e.g. seed)."""
|
|
182
|
+
from faker import Faker
|
|
183
|
+
from polyfactory.factories.base import BaseFactory
|
|
184
|
+
from polyfactory.factories.pydantic_factory import ModelFactory
|
|
185
|
+
from pydantic import BaseModel
|
|
186
|
+
|
|
187
|
+
from capper import Name, use_faker
|
|
188
|
+
from capper import faker as default_faker
|
|
189
|
+
|
|
190
|
+
try:
|
|
191
|
+
custom = Faker()
|
|
192
|
+
custom.seed_instance(123)
|
|
193
|
+
use_faker(custom)
|
|
194
|
+
from capper.base import faker as module_faker
|
|
195
|
+
|
|
196
|
+
assert module_faker is custom
|
|
197
|
+
assert BaseFactory.__faker__ is custom
|
|
198
|
+
|
|
199
|
+
class User(BaseModel):
|
|
200
|
+
name: Name
|
|
201
|
+
|
|
202
|
+
class UserFactory(ModelFactory[User]):
|
|
203
|
+
pass
|
|
204
|
+
|
|
205
|
+
seed_val = 999
|
|
206
|
+
custom.seed_instance(seed_val)
|
|
207
|
+
user1 = UserFactory.build()
|
|
208
|
+
custom.seed_instance(seed_val)
|
|
209
|
+
user2 = UserFactory.build()
|
|
210
|
+
assert user1.name == user2.name
|
|
211
|
+
finally:
|
|
212
|
+
use_faker(default_faker)
|
|
@@ -1,7 +1,34 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: capper
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Semantic, typed wrappers for Faker with automatic Polyfactory integration
|
|
5
|
+
Author-email: Odos Matthews <odosmatthews@gmail.com>
|
|
6
|
+
Project-URL: Documentation, https://github.com/eddiethedean/capper#readme
|
|
7
|
+
Project-URL: Repository, https://github.com/eddiethedean/capper
|
|
8
|
+
Requires-Python: >=3.10
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
Requires-Dist: Faker>=20.0
|
|
11
|
+
Requires-Dist: Polyfactory>=2.0
|
|
12
|
+
Provides-Extra: pydantic
|
|
13
|
+
Requires-Dist: pydantic>=2.0; extra == "pydantic"
|
|
14
|
+
Provides-Extra: hypothesis
|
|
15
|
+
Requires-Dist: hypothesis>=6.0; extra == "hypothesis"
|
|
16
|
+
Provides-Extra: dev
|
|
17
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
18
|
+
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
19
|
+
Requires-Dist: pytest-xdist>=3.0; extra == "dev"
|
|
20
|
+
Requires-Dist: pydantic>=2.0; extra == "dev"
|
|
21
|
+
Requires-Dist: ruff>=0.4; extra == "dev"
|
|
22
|
+
Requires-Dist: mypy>=1.0; extra == "dev"
|
|
23
|
+
Requires-Dist: hypothesis>=6.0; extra == "dev"
|
|
24
|
+
Provides-Extra: docs
|
|
25
|
+
Requires-Dist: mkdocs>=1.5; extra == "docs"
|
|
26
|
+
Requires-Dist: mkdocstrings[python]>=0.24; extra == "docs"
|
|
27
|
+
|
|
1
28
|
# Capper
|
|
2
29
|
|
|
3
30
|
[](https://pypi.org/project/capper/)
|
|
4
|
-
[](https://pypi.org/project/capper/)
|
|
5
32
|
[](https://github.com/eddiethedean/capper/actions/workflows/ci.yml)
|
|
6
33
|
[](https://docs.astral.sh/ruff/)
|
|
7
34
|
[](https://mypy-lang.org/)
|
|
@@ -23,11 +50,10 @@ Semantic, typed wrappers for [Faker](https://faker.readthedocs.io/) with automat
|
|
|
23
50
|
pip install capper
|
|
24
51
|
```
|
|
25
52
|
|
|
26
|
-
Requires **Python 3.
|
|
53
|
+
Requires **Python 3.10+**, **Faker >= 20.0**, and **Polyfactory >= 2.0**. Optional extras:
|
|
27
54
|
|
|
28
|
-
|
|
29
|
-
pip install capper[
|
|
30
|
-
```
|
|
55
|
+
- **Pydantic** (for Pydantic models): `pip install capper[pydantic]`
|
|
56
|
+
- **Hypothesis** (for property-based tests with `st.from_type(...)`): `pip install capper[hypothesis]`
|
|
31
57
|
|
|
32
58
|
## Usage
|
|
33
59
|
|
|
@@ -98,6 +124,10 @@ Works automatically. No extra steps. IDE autocompletion.
|
|
|
98
124
|
- **Text**: `Paragraph`, `Sentence`
|
|
99
125
|
- **Phone**: `PhoneNumber`, `CountryCallingCode`
|
|
100
126
|
- **Finance**: `CreditCardNumber`, `CreditCardExpiry`, `CreditCardProvider`
|
|
127
|
+
- **File**: `FilePath`, `FileName`, `FileExtension`
|
|
128
|
+
- **Misc**: `UUID`
|
|
129
|
+
- **Color**: `HexColor`
|
|
130
|
+
- **Barcode**: `EAN13`, `EAN8`
|
|
101
131
|
|
|
102
132
|
Import from the top level: `from capper import Name, Email, Address, ...`
|
|
103
133
|
See [docs/FAKER_PROVIDERS.md](https://github.com/eddiethedean/capper/blob/main/docs/FAKER_PROVIDERS.md) for the Faker provider used by each type.
|
|
@@ -106,9 +136,20 @@ See [docs/FAKER_PROVIDERS.md](https://github.com/eddiethedean/capper/blob/main/d
|
|
|
106
136
|
|
|
107
137
|
**Custom types:** Subclass `FakerType`, set `faker_provider` to the Faker method name (e.g. `"company"`), and optionally `faker_kwargs`. The type auto-registers with Polyfactory when the class is defined.
|
|
108
138
|
|
|
139
|
+
## CLI
|
|
140
|
+
|
|
141
|
+
Generate fake values from the command line:
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
capper generate Name Email --count 5
|
|
145
|
+
capper generate Name Email --count 3 --seed 42
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Use `-n`/`--count` for the number of rows and `-s`/`--seed` for reproducible output. Type names are the same as the Python types (e.g. `Name`, `Email`, `Address`).
|
|
149
|
+
|
|
109
150
|
## Compatibility
|
|
110
151
|
|
|
111
|
-
Capper targets **Faker >= 20.0
|
|
152
|
+
Capper targets **Python 3.10+**, **Faker >= 20.0**, and **Polyfactory >= 2.0**. For version ranges, upgrade guidance, and the deprecation policy, see [Compatibility](docs/compatibility.md).
|
|
112
153
|
|
|
113
154
|
## Development
|
|
114
155
|
|
|
@@ -117,6 +158,8 @@ pip install -e ".[dev]"
|
|
|
117
158
|
pytest capper/tests
|
|
118
159
|
```
|
|
119
160
|
|
|
161
|
+
Lint and type-check: `ruff check .`, `ruff format .`, `mypy capper`.
|
|
162
|
+
|
|
120
163
|
Run tests with coverage: `pytest capper/tests --cov=capper --cov-report=term-missing`.
|
|
121
164
|
|
|
122
165
|
**Reproducibility:** Capper and Polyfactory share the same Faker instance, so one seed controls both capper types and built-in types (`str`, `int`, etc.):
|
|
@@ -140,20 +183,23 @@ UserFactory.seed_random(42)
|
|
|
140
183
|
user2 = UserFactory.build() # same data as user1 if you seed the same before each
|
|
141
184
|
```
|
|
142
185
|
|
|
143
|
-
Use `UserFactory.__random_seed__ = 42` to seed once when the factory class is created, or call `seed(42)` / `UserFactory.seed_random(42)` before each build for identical builds.
|
|
186
|
+
Use `UserFactory.__random_seed__ = 42` to seed once when the factory class is created, or call `seed(42)` / `UserFactory.seed_random(42)` before each build for identical builds. For a custom locale (e.g. German names), use **`use_faker(Faker('de_DE'))`** so both Capper and Polyfactory use the same Faker instance; see [Reproducible data](docs/user_guides/reproducible_data.md#locales-and-custom-faker).
|
|
144
187
|
|
|
145
188
|
## Publishing
|
|
146
189
|
|
|
147
190
|
Releases are built and published to PyPI via [GitHub Actions](https://github.com/eddiethedean/capper/blob/main/.github/workflows/publish.yml). To publish:
|
|
148
191
|
|
|
149
|
-
1.
|
|
150
|
-
2.
|
|
192
|
+
1. Update [CHANGELOG.md](CHANGELOG.md): move Unreleased entries into a new version section and date it.
|
|
193
|
+
2. Add a `PYPI_API_TOKEN` secret (PyPI API token) to the repo.
|
|
194
|
+
3. Create a GitHub release (tag e.g. `v0.3.0`). The workflow runs tests, builds the package, and uploads to PyPI.
|
|
151
195
|
|
|
152
196
|
To build and upload manually: `pip install build twine`, `python -m build`, `twine upload dist/*`.
|
|
153
197
|
|
|
154
198
|
## Documentation
|
|
155
199
|
|
|
156
200
|
- **[Docs index](docs/README.md)** — overview and links to all documentation
|
|
201
|
+
- **[API reference](docs/api.md)** — generated API docs (build with `mkdocs serve`; see [Contributing](CONTRIBUTING.md))
|
|
202
|
+
- **[Contributing](CONTRIBUTING.md)** — dev setup and how to add new types
|
|
157
203
|
- **User guides** (step-by-step, with runnable examples):
|
|
158
204
|
- [Getting started](docs/user_guides/getting_started.md) — install, first model, first factory
|
|
159
205
|
- [Models and factories](docs/user_guides/models_and_factories.md) — Pydantic, dataclasses, batches
|
|
@@ -162,3 +208,4 @@ To build and upload manually: `pip install build twine`, `python -m build`, `twi
|
|
|
162
208
|
- [Package plan](docs/capper_package_plan.md) — design and rationale
|
|
163
209
|
- [Roadmap](docs/ROADMAP.md) — development phases and status
|
|
164
210
|
- [Faker provider mapping](docs/FAKER_PROVIDERS.md) — which Faker method each type uses
|
|
211
|
+
- [Example notebooks](docs/notebooks/README.md) — Jupyter notebooks in `docs/notebooks/`
|
|
@@ -1,23 +1,32 @@
|
|
|
1
1
|
README.md
|
|
2
2
|
pyproject.toml
|
|
3
3
|
capper/__init__.py
|
|
4
|
+
capper/barcode.py
|
|
4
5
|
capper/base.py
|
|
6
|
+
capper/cli.py
|
|
7
|
+
capper/color.py
|
|
5
8
|
capper/commerce.py
|
|
6
9
|
capper/date_time.py
|
|
10
|
+
capper/file.py
|
|
7
11
|
capper/finance.py
|
|
8
12
|
capper/geo.py
|
|
9
13
|
capper/internet.py
|
|
14
|
+
capper/misc.py
|
|
10
15
|
capper/person.py
|
|
11
16
|
capper/phone.py
|
|
17
|
+
capper/strategies.py
|
|
12
18
|
capper/text.py
|
|
13
19
|
capper.egg-info/PKG-INFO
|
|
14
20
|
capper.egg-info/SOURCES.txt
|
|
15
21
|
capper.egg-info/dependency_links.txt
|
|
22
|
+
capper.egg-info/entry_points.txt
|
|
16
23
|
capper.egg-info/requires.txt
|
|
17
24
|
capper.egg-info/top_level.txt
|
|
18
25
|
capper/examples/user_factory.py
|
|
19
26
|
capper/tests/__init__.py
|
|
20
27
|
capper/tests/conftest.py
|
|
28
|
+
capper/tests/test_cli.py
|
|
21
29
|
capper/tests/test_docs_examples.py
|
|
30
|
+
capper/tests/test_hypothesis_strategies.py
|
|
22
31
|
capper/tests/test_polyfactory_integration.py
|
|
23
32
|
capper/tests/test_types.py
|
|
@@ -4,10 +4,10 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "capper"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.0"
|
|
8
8
|
description = "Semantic, typed wrappers for Faker with automatic Polyfactory integration"
|
|
9
9
|
readme = "README.md"
|
|
10
|
-
requires-python = ">=3.
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
11
|
authors = [
|
|
12
12
|
{ name = "Odos Matthews", email = "odosmatthews@gmail.com" },
|
|
13
13
|
]
|
|
@@ -20,17 +20,30 @@ dependencies = [
|
|
|
20
20
|
pydantic = [
|
|
21
21
|
"pydantic>=2.0",
|
|
22
22
|
]
|
|
23
|
+
hypothesis = [
|
|
24
|
+
"hypothesis>=6.0",
|
|
25
|
+
]
|
|
23
26
|
dev = [
|
|
24
27
|
"pytest>=7.0",
|
|
25
28
|
"pytest-cov>=4.0",
|
|
26
29
|
"pytest-xdist>=3.0",
|
|
27
30
|
"pydantic>=2.0",
|
|
31
|
+
"ruff>=0.4",
|
|
32
|
+
"mypy>=1.0",
|
|
33
|
+
"hypothesis>=6.0",
|
|
34
|
+
]
|
|
35
|
+
docs = [
|
|
36
|
+
"mkdocs>=1.5",
|
|
37
|
+
"mkdocstrings[python]>=0.24",
|
|
28
38
|
]
|
|
29
39
|
|
|
30
40
|
[project.urls]
|
|
31
41
|
Documentation = "https://github.com/eddiethedean/capper#readme"
|
|
32
42
|
Repository = "https://github.com/eddiethedean/capper"
|
|
33
43
|
|
|
44
|
+
[project.scripts]
|
|
45
|
+
capper = "capper.cli:main"
|
|
46
|
+
|
|
34
47
|
[tool.setuptools.packages.find]
|
|
35
48
|
where = ["."]
|
|
36
49
|
include = ["capper*"]
|
|
@@ -39,3 +52,18 @@ include = ["capper*"]
|
|
|
39
52
|
testpaths = ["capper/tests"]
|
|
40
53
|
pythonpath = ["."]
|
|
41
54
|
addopts = ["-v", "--tb=short"]
|
|
55
|
+
|
|
56
|
+
[tool.ruff]
|
|
57
|
+
line-length = 100
|
|
58
|
+
target-version = "py310"
|
|
59
|
+
exclude = ["docs/notebooks"]
|
|
60
|
+
|
|
61
|
+
[tool.ruff.lint]
|
|
62
|
+
select = ["E", "F", "I"]
|
|
63
|
+
|
|
64
|
+
[tool.mypy]
|
|
65
|
+
python_version = "3.10"
|
|
66
|
+
warn_return_any = true
|
|
67
|
+
warn_unused_configs = true
|
|
68
|
+
disallow_untyped_defs = false
|
|
69
|
+
exclude = ["capper/tests"]
|
|
File without changes
|
|
File without changes
|
|
@@ -4,8 +4,8 @@ Run: python -m capper.examples.user_factory
|
|
|
4
4
|
Output varies each run; use capper.seed(n) for reproducible data.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
from pydantic import BaseModel
|
|
8
7
|
from polyfactory.factories.pydantic_factory import ModelFactory
|
|
8
|
+
from pydantic import BaseModel
|
|
9
9
|
|
|
10
10
|
from capper import Email, Name
|
|
11
11
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|