capper 0.2.0__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.2.0 → capper-0.3.0}/PKG-INFO +18 -8
- {capper-0.2.0 → capper-0.3.0}/README.md +13 -6
- {capper-0.2.0 → capper-0.3.0}/capper/__init__.py +20 -1
- capper-0.3.0/capper/barcode.py +15 -0
- {capper-0.2.0 → capper-0.3.0}/capper/base.py +30 -1
- {capper-0.2.0 → capper-0.3.0}/capper/cli.py +1 -1
- 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.2.0 → capper-0.3.0}/capper/strategies.py +22 -2
- capper-0.3.0/capper/tests/test_cli.py +41 -0
- {capper-0.2.0 → capper-0.3.0}/capper/tests/test_types.py +73 -2
- {capper-0.2.0 → capper-0.3.0}/capper.egg-info/PKG-INFO +18 -8
- {capper-0.2.0 → capper-0.3.0}/capper.egg-info/SOURCES.txt +5 -0
- {capper-0.2.0 → capper-0.3.0}/capper.egg-info/requires.txt +4 -0
- {capper-0.2.0 → capper-0.3.0}/pyproject.toml +9 -4
- {capper-0.2.0 → capper-0.3.0}/capper/commerce.py +0 -0
- {capper-0.2.0 → capper-0.3.0}/capper/date_time.py +0 -0
- {capper-0.2.0 → capper-0.3.0}/capper/examples/user_factory.py +0 -0
- {capper-0.2.0 → capper-0.3.0}/capper/finance.py +0 -0
- {capper-0.2.0 → capper-0.3.0}/capper/geo.py +0 -0
- {capper-0.2.0 → capper-0.3.0}/capper/internet.py +0 -0
- {capper-0.2.0 → capper-0.3.0}/capper/person.py +0 -0
- {capper-0.2.0 → capper-0.3.0}/capper/phone.py +0 -0
- {capper-0.2.0 → capper-0.3.0}/capper/tests/__init__.py +0 -0
- {capper-0.2.0 → capper-0.3.0}/capper/tests/conftest.py +0 -0
- {capper-0.2.0 → capper-0.3.0}/capper/tests/test_docs_examples.py +0 -0
- {capper-0.2.0 → capper-0.3.0}/capper/tests/test_hypothesis_strategies.py +0 -0
- {capper-0.2.0 → capper-0.3.0}/capper/tests/test_polyfactory_integration.py +0 -0
- {capper-0.2.0 → capper-0.3.0}/capper/text.py +0 -0
- {capper-0.2.0 → capper-0.3.0}/capper.egg-info/dependency_links.txt +0 -0
- {capper-0.2.0 → capper-0.3.0}/capper.egg-info/entry_points.txt +0 -0
- {capper-0.2.0 → capper-0.3.0}/capper.egg-info/top_level.txt +0 -0
- {capper-0.2.0 → capper-0.3.0}/setup.cfg +0 -0
|
@@ -1,11 +1,11 @@
|
|
|
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
|
|
@@ -21,11 +21,14 @@ Requires-Dist: pydantic>=2.0; extra == "dev"
|
|
|
21
21
|
Requires-Dist: ruff>=0.4; extra == "dev"
|
|
22
22
|
Requires-Dist: mypy>=1.0; extra == "dev"
|
|
23
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"
|
|
24
27
|
|
|
25
28
|
# Capper
|
|
26
29
|
|
|
27
30
|
[](https://pypi.org/project/capper/)
|
|
28
|
-
[](https://pypi.org/project/capper/)
|
|
29
32
|
[](https://github.com/eddiethedean/capper/actions/workflows/ci.yml)
|
|
30
33
|
[](https://docs.astral.sh/ruff/)
|
|
31
34
|
[](https://mypy-lang.org/)
|
|
@@ -47,7 +50,7 @@ Semantic, typed wrappers for [Faker](https://faker.readthedocs.io/) with automat
|
|
|
47
50
|
pip install capper
|
|
48
51
|
```
|
|
49
52
|
|
|
50
|
-
Requires **Python 3.
|
|
53
|
+
Requires **Python 3.10+**, **Faker >= 20.0**, and **Polyfactory >= 2.0**. Optional extras:
|
|
51
54
|
|
|
52
55
|
- **Pydantic** (for Pydantic models): `pip install capper[pydantic]`
|
|
53
56
|
- **Hypothesis** (for property-based tests with `st.from_type(...)`): `pip install capper[hypothesis]`
|
|
@@ -121,6 +124,10 @@ Works automatically. No extra steps. IDE autocompletion.
|
|
|
121
124
|
- **Text**: `Paragraph`, `Sentence`
|
|
122
125
|
- **Phone**: `PhoneNumber`, `CountryCallingCode`
|
|
123
126
|
- **Finance**: `CreditCardNumber`, `CreditCardExpiry`, `CreditCardProvider`
|
|
127
|
+
- **File**: `FilePath`, `FileName`, `FileExtension`
|
|
128
|
+
- **Misc**: `UUID`
|
|
129
|
+
- **Color**: `HexColor`
|
|
130
|
+
- **Barcode**: `EAN13`, `EAN8`
|
|
124
131
|
|
|
125
132
|
Import from the top level: `from capper import Name, Email, Address, ...`
|
|
126
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.
|
|
@@ -142,7 +149,7 @@ Use `-n`/`--count` for the number of rows and `-s`/`--seed` for reproducible out
|
|
|
142
149
|
|
|
143
150
|
## Compatibility
|
|
144
151
|
|
|
145
|
-
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).
|
|
146
153
|
|
|
147
154
|
## Development
|
|
148
155
|
|
|
@@ -176,20 +183,23 @@ UserFactory.seed_random(42)
|
|
|
176
183
|
user2 = UserFactory.build() # same data as user1 if you seed the same before each
|
|
177
184
|
```
|
|
178
185
|
|
|
179
|
-
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).
|
|
180
187
|
|
|
181
188
|
## Publishing
|
|
182
189
|
|
|
183
190
|
Releases are built and published to PyPI via [GitHub Actions](https://github.com/eddiethedean/capper/blob/main/.github/workflows/publish.yml). To publish:
|
|
184
191
|
|
|
185
|
-
1.
|
|
186
|
-
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.
|
|
187
195
|
|
|
188
196
|
To build and upload manually: `pip install build twine`, `python -m build`, `twine upload dist/*`.
|
|
189
197
|
|
|
190
198
|
## Documentation
|
|
191
199
|
|
|
192
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
|
|
193
203
|
- **User guides** (step-by-step, with runnable examples):
|
|
194
204
|
- [Getting started](docs/user_guides/getting_started.md) — install, first model, first factory
|
|
195
205
|
- [Models and factories](docs/user_guides/models_and_factories.md) — Pydantic, dataclasses, batches
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Capper
|
|
2
2
|
|
|
3
3
|
[](https://pypi.org/project/capper/)
|
|
4
|
-
[](https://pypi.org/project/capper/)
|
|
5
5
|
[](https://github.com/eddiethedean/capper/actions/workflows/ci.yml)
|
|
6
6
|
[](https://docs.astral.sh/ruff/)
|
|
7
7
|
[](https://mypy-lang.org/)
|
|
@@ -23,7 +23,7 @@ Semantic, typed wrappers for [Faker](https://faker.readthedocs.io/) with automat
|
|
|
23
23
|
pip install capper
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
-
Requires **Python 3.
|
|
26
|
+
Requires **Python 3.10+**, **Faker >= 20.0**, and **Polyfactory >= 2.0**. Optional extras:
|
|
27
27
|
|
|
28
28
|
- **Pydantic** (for Pydantic models): `pip install capper[pydantic]`
|
|
29
29
|
- **Hypothesis** (for property-based tests with `st.from_type(...)`): `pip install capper[hypothesis]`
|
|
@@ -97,6 +97,10 @@ Works automatically. No extra steps. IDE autocompletion.
|
|
|
97
97
|
- **Text**: `Paragraph`, `Sentence`
|
|
98
98
|
- **Phone**: `PhoneNumber`, `CountryCallingCode`
|
|
99
99
|
- **Finance**: `CreditCardNumber`, `CreditCardExpiry`, `CreditCardProvider`
|
|
100
|
+
- **File**: `FilePath`, `FileName`, `FileExtension`
|
|
101
|
+
- **Misc**: `UUID`
|
|
102
|
+
- **Color**: `HexColor`
|
|
103
|
+
- **Barcode**: `EAN13`, `EAN8`
|
|
100
104
|
|
|
101
105
|
Import from the top level: `from capper import Name, Email, Address, ...`
|
|
102
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.
|
|
@@ -118,7 +122,7 @@ Use `-n`/`--count` for the number of rows and `-s`/`--seed` for reproducible out
|
|
|
118
122
|
|
|
119
123
|
## Compatibility
|
|
120
124
|
|
|
121
|
-
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).
|
|
122
126
|
|
|
123
127
|
## Development
|
|
124
128
|
|
|
@@ -152,20 +156,23 @@ UserFactory.seed_random(42)
|
|
|
152
156
|
user2 = UserFactory.build() # same data as user1 if you seed the same before each
|
|
153
157
|
```
|
|
154
158
|
|
|
155
|
-
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).
|
|
156
160
|
|
|
157
161
|
## Publishing
|
|
158
162
|
|
|
159
163
|
Releases are built and published to PyPI via [GitHub Actions](https://github.com/eddiethedean/capper/blob/main/.github/workflows/publish.yml). To publish:
|
|
160
164
|
|
|
161
|
-
1.
|
|
162
|
-
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.
|
|
163
168
|
|
|
164
169
|
To build and upload manually: `pip install build twine`, `python -m build`, `twine upload dist/*`.
|
|
165
170
|
|
|
166
171
|
## Documentation
|
|
167
172
|
|
|
168
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
|
|
169
176
|
- **User guides** (step-by-step, with runnable examples):
|
|
170
177
|
- [Getting started](docs/user_guides/getting_started.md) — install, first model, first factory
|
|
171
178
|
- [Models and factories](docs/user_guides/models_and_factories.md) — Pydantic, dataclasses, batches
|
|
@@ -6,12 +6,23 @@ With Hypothesis installed (pip install capper[hypothesis]), import capper.strate
|
|
|
6
6
|
and use st.from_type(Name) for property-based tests.
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
|
-
|
|
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
|
|
10
19
|
from .commerce import Company, Currency, Price, Product
|
|
11
20
|
from .date_time import Date, DateTime, Time
|
|
21
|
+
from .file import FileExtension, FileName, FilePath
|
|
12
22
|
from .finance import CreditCardExpiry, CreditCardNumber, CreditCardProvider
|
|
13
23
|
from .geo import Address, City, Country
|
|
14
24
|
from .internet import IP, URL, Email, UserName
|
|
25
|
+
from .misc import UUID
|
|
15
26
|
from .person import FirstName, Job, LastName, Name
|
|
16
27
|
from .phone import CountryCallingCode, PhoneNumber
|
|
17
28
|
from .text import Paragraph, Sentence
|
|
@@ -28,9 +39,15 @@ __all__ = [
|
|
|
28
39
|
"Currency",
|
|
29
40
|
"Date",
|
|
30
41
|
"DateTime",
|
|
42
|
+
"EAN13",
|
|
43
|
+
"EAN8",
|
|
31
44
|
"Email",
|
|
32
45
|
"FakerType",
|
|
46
|
+
"FileExtension",
|
|
47
|
+
"FileName",
|
|
48
|
+
"FilePath",
|
|
33
49
|
"FirstName",
|
|
50
|
+
"HexColor",
|
|
34
51
|
"IP",
|
|
35
52
|
"Job",
|
|
36
53
|
"LastName",
|
|
@@ -42,7 +59,9 @@ __all__ = [
|
|
|
42
59
|
"Sentence",
|
|
43
60
|
"Time",
|
|
44
61
|
"URL",
|
|
62
|
+
"UUID",
|
|
45
63
|
"UserName",
|
|
46
64
|
"faker",
|
|
47
65
|
"seed",
|
|
66
|
+
"use_faker",
|
|
48
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:
|
|
@@ -62,7 +83,15 @@ _install_pydantic_schema()
|
|
|
62
83
|
|
|
63
84
|
def _register(cls: type, provider_name: str) -> None:
|
|
64
85
|
"""Register a FakerType subclass with Polyfactory so factories can generate values."""
|
|
65
|
-
|
|
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 {})
|
|
66
95
|
|
|
67
96
|
def _provide() -> str:
|
|
68
97
|
value = getattr(faker, provider_name)(**provider_kwargs)
|
|
@@ -13,7 +13,7 @@ def _type_registry() -> dict[str, Type[FakerType]]:
|
|
|
13
13
|
"""Map type name -> FakerType subclass (only types with faker_provider)."""
|
|
14
14
|
registry: dict[str, Type[FakerType]] = {}
|
|
15
15
|
for name in getattr(capper, "__all__", []):
|
|
16
|
-
if name in ("FakerType", "faker", "seed"):
|
|
16
|
+
if name in ("FakerType", "faker", "seed", "use_faker"):
|
|
17
17
|
continue
|
|
18
18
|
obj = getattr(capper, name, None)
|
|
19
19
|
provider = getattr(obj, "faker_provider", None)
|
|
@@ -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"
|
|
@@ -4,16 +4,22 @@ Use ``st.from_type(Name)`` after importing capper and capper.strategies, or call
|
|
|
4
4
|
``strategies.for_type(Name)`` to get a strategy that generates instances of that type.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
from
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Type, TypeVar, cast
|
|
8
10
|
|
|
9
11
|
from hypothesis import strategies as st
|
|
10
12
|
|
|
13
|
+
from .barcode import EAN8, EAN13
|
|
11
14
|
from .base import FakerType, faker
|
|
15
|
+
from .color import HexColor
|
|
12
16
|
from .commerce import Company, Currency, Price, Product
|
|
13
17
|
from .date_time import Date, DateTime, Time
|
|
18
|
+
from .file import FileExtension, FileName, FilePath
|
|
14
19
|
from .finance import CreditCardExpiry, CreditCardNumber, CreditCardProvider
|
|
15
20
|
from .geo import Address, City, Country
|
|
16
21
|
from .internet import IP, URL, Email, UserName
|
|
22
|
+
from .misc import UUID
|
|
17
23
|
from .person import FirstName, Job, LastName, Name
|
|
18
24
|
from .phone import CountryCallingCode, PhoneNumber
|
|
19
25
|
from .text import Paragraph, Sentence
|
|
@@ -33,8 +39,14 @@ _BUILTIN_TYPES: tuple[type[FakerType], ...] = (
|
|
|
33
39
|
Currency,
|
|
34
40
|
Date,
|
|
35
41
|
DateTime,
|
|
42
|
+
EAN13,
|
|
43
|
+
EAN8,
|
|
36
44
|
Email,
|
|
45
|
+
FileExtension,
|
|
46
|
+
FileName,
|
|
47
|
+
FilePath,
|
|
37
48
|
FirstName,
|
|
49
|
+
HexColor,
|
|
38
50
|
IP,
|
|
39
51
|
Job,
|
|
40
52
|
LastName,
|
|
@@ -46,6 +58,7 @@ _BUILTIN_TYPES: tuple[type[FakerType], ...] = (
|
|
|
46
58
|
Sentence,
|
|
47
59
|
Time,
|
|
48
60
|
URL,
|
|
61
|
+
UUID,
|
|
49
62
|
UserName,
|
|
50
63
|
)
|
|
51
64
|
|
|
@@ -55,6 +68,13 @@ def for_type(cls: Type[T]) -> st.SearchStrategy[T]:
|
|
|
55
68
|
provider = getattr(cls, "faker_provider", None)
|
|
56
69
|
if not provider:
|
|
57
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__}).")
|
|
58
78
|
kwargs = getattr(cls, "faker_kwargs", None) or {}
|
|
59
79
|
|
|
60
80
|
@st.composite
|
|
@@ -64,7 +84,7 @@ def for_type(cls: Type[T]) -> st.SearchStrategy[T]:
|
|
|
64
84
|
value = getattr(faker, provider)(**kwargs)
|
|
65
85
|
return cls(str(value))
|
|
66
86
|
|
|
67
|
-
return _draw()
|
|
87
|
+
return cast(st.SearchStrategy[T], _draw())
|
|
68
88
|
|
|
69
89
|
|
|
70
90
|
def _register_strategies() -> None:
|
|
@@ -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
|
|
@@ -3,8 +3,11 @@
|
|
|
3
3
|
import pytest
|
|
4
4
|
|
|
5
5
|
from capper import (
|
|
6
|
+
EAN8,
|
|
7
|
+
EAN13,
|
|
6
8
|
IP,
|
|
7
9
|
URL,
|
|
10
|
+
UUID,
|
|
8
11
|
Address,
|
|
9
12
|
City,
|
|
10
13
|
Company,
|
|
@@ -17,7 +20,11 @@ from capper import (
|
|
|
17
20
|
Date,
|
|
18
21
|
DateTime,
|
|
19
22
|
Email,
|
|
23
|
+
FileExtension,
|
|
24
|
+
FileName,
|
|
25
|
+
FilePath,
|
|
20
26
|
FirstName,
|
|
27
|
+
HexColor,
|
|
21
28
|
Job,
|
|
22
29
|
LastName,
|
|
23
30
|
Name,
|
|
@@ -59,6 +66,13 @@ 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:
|
|
@@ -67,14 +81,27 @@ def test_type_generates_non_empty_string(type_class: type) -> None:
|
|
|
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."""
|
|
@@ -121,6 +148,15 @@ 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
162
|
from polyfactory.factories.pydantic_factory import ModelFactory
|
|
@@ -139,3 +175,38 @@ def test_seed_reproducibility(seeded_faker: None) -> None:
|
|
|
139
175
|
seed(42)
|
|
140
176
|
user2 = UserFactory.build()
|
|
141
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,11 +1,11 @@
|
|
|
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
|
|
@@ -21,11 +21,14 @@ Requires-Dist: pydantic>=2.0; extra == "dev"
|
|
|
21
21
|
Requires-Dist: ruff>=0.4; extra == "dev"
|
|
22
22
|
Requires-Dist: mypy>=1.0; extra == "dev"
|
|
23
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"
|
|
24
27
|
|
|
25
28
|
# Capper
|
|
26
29
|
|
|
27
30
|
[](https://pypi.org/project/capper/)
|
|
28
|
-
[](https://pypi.org/project/capper/)
|
|
29
32
|
[](https://github.com/eddiethedean/capper/actions/workflows/ci.yml)
|
|
30
33
|
[](https://docs.astral.sh/ruff/)
|
|
31
34
|
[](https://mypy-lang.org/)
|
|
@@ -47,7 +50,7 @@ Semantic, typed wrappers for [Faker](https://faker.readthedocs.io/) with automat
|
|
|
47
50
|
pip install capper
|
|
48
51
|
```
|
|
49
52
|
|
|
50
|
-
Requires **Python 3.
|
|
53
|
+
Requires **Python 3.10+**, **Faker >= 20.0**, and **Polyfactory >= 2.0**. Optional extras:
|
|
51
54
|
|
|
52
55
|
- **Pydantic** (for Pydantic models): `pip install capper[pydantic]`
|
|
53
56
|
- **Hypothesis** (for property-based tests with `st.from_type(...)`): `pip install capper[hypothesis]`
|
|
@@ -121,6 +124,10 @@ Works automatically. No extra steps. IDE autocompletion.
|
|
|
121
124
|
- **Text**: `Paragraph`, `Sentence`
|
|
122
125
|
- **Phone**: `PhoneNumber`, `CountryCallingCode`
|
|
123
126
|
- **Finance**: `CreditCardNumber`, `CreditCardExpiry`, `CreditCardProvider`
|
|
127
|
+
- **File**: `FilePath`, `FileName`, `FileExtension`
|
|
128
|
+
- **Misc**: `UUID`
|
|
129
|
+
- **Color**: `HexColor`
|
|
130
|
+
- **Barcode**: `EAN13`, `EAN8`
|
|
124
131
|
|
|
125
132
|
Import from the top level: `from capper import Name, Email, Address, ...`
|
|
126
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.
|
|
@@ -142,7 +149,7 @@ Use `-n`/`--count` for the number of rows and `-s`/`--seed` for reproducible out
|
|
|
142
149
|
|
|
143
150
|
## Compatibility
|
|
144
151
|
|
|
145
|
-
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).
|
|
146
153
|
|
|
147
154
|
## Development
|
|
148
155
|
|
|
@@ -176,20 +183,23 @@ UserFactory.seed_random(42)
|
|
|
176
183
|
user2 = UserFactory.build() # same data as user1 if you seed the same before each
|
|
177
184
|
```
|
|
178
185
|
|
|
179
|
-
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).
|
|
180
187
|
|
|
181
188
|
## Publishing
|
|
182
189
|
|
|
183
190
|
Releases are built and published to PyPI via [GitHub Actions](https://github.com/eddiethedean/capper/blob/main/.github/workflows/publish.yml). To publish:
|
|
184
191
|
|
|
185
|
-
1.
|
|
186
|
-
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.
|
|
187
195
|
|
|
188
196
|
To build and upload manually: `pip install build twine`, `python -m build`, `twine upload dist/*`.
|
|
189
197
|
|
|
190
198
|
## Documentation
|
|
191
199
|
|
|
192
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
|
|
193
203
|
- **User guides** (step-by-step, with runnable examples):
|
|
194
204
|
- [Getting started](docs/user_guides/getting_started.md) — install, first model, first factory
|
|
195
205
|
- [Models and factories](docs/user_guides/models_and_factories.md) — Pydantic, dataclasses, batches
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
README.md
|
|
2
2
|
pyproject.toml
|
|
3
3
|
capper/__init__.py
|
|
4
|
+
capper/barcode.py
|
|
4
5
|
capper/base.py
|
|
5
6
|
capper/cli.py
|
|
7
|
+
capper/color.py
|
|
6
8
|
capper/commerce.py
|
|
7
9
|
capper/date_time.py
|
|
10
|
+
capper/file.py
|
|
8
11
|
capper/finance.py
|
|
9
12
|
capper/geo.py
|
|
10
13
|
capper/internet.py
|
|
14
|
+
capper/misc.py
|
|
11
15
|
capper/person.py
|
|
12
16
|
capper/phone.py
|
|
13
17
|
capper/strategies.py
|
|
@@ -21,6 +25,7 @@ capper.egg-info/top_level.txt
|
|
|
21
25
|
capper/examples/user_factory.py
|
|
22
26
|
capper/tests/__init__.py
|
|
23
27
|
capper/tests/conftest.py
|
|
28
|
+
capper/tests/test_cli.py
|
|
24
29
|
capper/tests/test_docs_examples.py
|
|
25
30
|
capper/tests/test_hypothesis_strategies.py
|
|
26
31
|
capper/tests/test_polyfactory_integration.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
|
]
|
|
@@ -32,6 +32,10 @@ dev = [
|
|
|
32
32
|
"mypy>=1.0",
|
|
33
33
|
"hypothesis>=6.0",
|
|
34
34
|
]
|
|
35
|
+
docs = [
|
|
36
|
+
"mkdocs>=1.5",
|
|
37
|
+
"mkdocstrings[python]>=0.24",
|
|
38
|
+
]
|
|
35
39
|
|
|
36
40
|
[project.urls]
|
|
37
41
|
Documentation = "https://github.com/eddiethedean/capper#readme"
|
|
@@ -51,14 +55,15 @@ addopts = ["-v", "--tb=short"]
|
|
|
51
55
|
|
|
52
56
|
[tool.ruff]
|
|
53
57
|
line-length = 100
|
|
54
|
-
target-version = "
|
|
58
|
+
target-version = "py310"
|
|
55
59
|
exclude = ["docs/notebooks"]
|
|
56
60
|
|
|
57
61
|
[tool.ruff.lint]
|
|
58
62
|
select = ["E", "F", "I"]
|
|
59
63
|
|
|
60
64
|
[tool.mypy]
|
|
61
|
-
python_version = "3.
|
|
65
|
+
python_version = "3.10"
|
|
62
66
|
warn_return_any = true
|
|
63
67
|
warn_unused_configs = true
|
|
64
68
|
disallow_untyped_defs = false
|
|
69
|
+
exclude = ["capper/tests"]
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|