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.
Files changed (34) hide show
  1. {capper-0.2.0 → capper-0.3.0}/PKG-INFO +18 -8
  2. {capper-0.2.0 → capper-0.3.0}/README.md +13 -6
  3. {capper-0.2.0 → capper-0.3.0}/capper/__init__.py +20 -1
  4. capper-0.3.0/capper/barcode.py +15 -0
  5. {capper-0.2.0 → capper-0.3.0}/capper/base.py +30 -1
  6. {capper-0.2.0 → capper-0.3.0}/capper/cli.py +1 -1
  7. capper-0.3.0/capper/color.py +10 -0
  8. capper-0.3.0/capper/file.py +21 -0
  9. capper-0.3.0/capper/misc.py +9 -0
  10. {capper-0.2.0 → capper-0.3.0}/capper/strategies.py +22 -2
  11. capper-0.3.0/capper/tests/test_cli.py +41 -0
  12. {capper-0.2.0 → capper-0.3.0}/capper/tests/test_types.py +73 -2
  13. {capper-0.2.0 → capper-0.3.0}/capper.egg-info/PKG-INFO +18 -8
  14. {capper-0.2.0 → capper-0.3.0}/capper.egg-info/SOURCES.txt +5 -0
  15. {capper-0.2.0 → capper-0.3.0}/capper.egg-info/requires.txt +4 -0
  16. {capper-0.2.0 → capper-0.3.0}/pyproject.toml +9 -4
  17. {capper-0.2.0 → capper-0.3.0}/capper/commerce.py +0 -0
  18. {capper-0.2.0 → capper-0.3.0}/capper/date_time.py +0 -0
  19. {capper-0.2.0 → capper-0.3.0}/capper/examples/user_factory.py +0 -0
  20. {capper-0.2.0 → capper-0.3.0}/capper/finance.py +0 -0
  21. {capper-0.2.0 → capper-0.3.0}/capper/geo.py +0 -0
  22. {capper-0.2.0 → capper-0.3.0}/capper/internet.py +0 -0
  23. {capper-0.2.0 → capper-0.3.0}/capper/person.py +0 -0
  24. {capper-0.2.0 → capper-0.3.0}/capper/phone.py +0 -0
  25. {capper-0.2.0 → capper-0.3.0}/capper/tests/__init__.py +0 -0
  26. {capper-0.2.0 → capper-0.3.0}/capper/tests/conftest.py +0 -0
  27. {capper-0.2.0 → capper-0.3.0}/capper/tests/test_docs_examples.py +0 -0
  28. {capper-0.2.0 → capper-0.3.0}/capper/tests/test_hypothesis_strategies.py +0 -0
  29. {capper-0.2.0 → capper-0.3.0}/capper/tests/test_polyfactory_integration.py +0 -0
  30. {capper-0.2.0 → capper-0.3.0}/capper/text.py +0 -0
  31. {capper-0.2.0 → capper-0.3.0}/capper.egg-info/dependency_links.txt +0 -0
  32. {capper-0.2.0 → capper-0.3.0}/capper.egg-info/entry_points.txt +0 -0
  33. {capper-0.2.0 → capper-0.3.0}/capper.egg-info/top_level.txt +0 -0
  34. {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.2.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.9
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
  [![PyPI](https://img.shields.io/pypi/v/capper.svg)](https://pypi.org/project/capper/)
28
- [![Python](https://img.shields.io/badge/python-3.9+-blue.svg)](https://pypi.org/project/capper/)
31
+ [![Python](https://img.shields.io/badge/python-3.10+-blue.svg)](https://pypi.org/project/capper/)
29
32
  [![CI](https://img.shields.io/github/actions/workflow/status/eddiethedean/capper/ci.yml?branch=main&label=CI)](https://github.com/eddiethedean/capper/actions/workflows/ci.yml)
30
33
  [![Ruff](https://img.shields.io/badge/code%20style-ruff-000000.svg)](https://docs.astral.sh/ruff/)
31
34
  [![mypy](https://img.shields.io/badge/mypy-checked-blue.svg)](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.9+**, **Faker >= 20.0**, and **Polyfactory >= 2.0**. Optional extras:
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** and **Polyfactory >= 2.0**. Major Faker upgrades may change or rename provider methods; if a type fails, check [Faker's changelog](https://faker.readthedocs.io/en/stable/changelog.html) and [docs/FAKER_PROVIDERS.md](https://github.com/eddiethedean/capper/blob/main/docs/FAKER_PROVIDERS.md) and update the provider name if needed.
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. Add a `PYPI_API_TOKEN` secret (PyPI API token) to the repo.
186
- 2. Create a GitHub release (tag e.g. `v0.2.0`). The workflow runs tests, builds the package, and uploads to PyPI.
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
  [![PyPI](https://img.shields.io/pypi/v/capper.svg)](https://pypi.org/project/capper/)
4
- [![Python](https://img.shields.io/badge/python-3.9+-blue.svg)](https://pypi.org/project/capper/)
4
+ [![Python](https://img.shields.io/badge/python-3.10+-blue.svg)](https://pypi.org/project/capper/)
5
5
  [![CI](https://img.shields.io/github/actions/workflow/status/eddiethedean/capper/ci.yml?branch=main&label=CI)](https://github.com/eddiethedean/capper/actions/workflows/ci.yml)
6
6
  [![Ruff](https://img.shields.io/badge/code%20style-ruff-000000.svg)](https://docs.astral.sh/ruff/)
7
7
  [![mypy](https://img.shields.io/badge/mypy-checked-blue.svg)](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.9+**, **Faker >= 20.0**, and **Polyfactory >= 2.0**. Optional extras:
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** and **Polyfactory >= 2.0**. Major Faker upgrades may change or rename provider methods; if a type fails, check [Faker's changelog](https://faker.readthedocs.io/en/stable/changelog.html) and [docs/FAKER_PROVIDERS.md](https://github.com/eddiethedean/capper/blob/main/docs/FAKER_PROVIDERS.md) and update the provider name if needed.
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. Add a `PYPI_API_TOKEN` secret (PyPI API token) to the repo.
162
- 2. Create a GitHub release (tag e.g. `v0.2.0`). The workflow runs tests, builds the package, and uploads to PyPI.
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
- from .base import FakerType, faker, seed
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
- provider_kwargs = getattr(cls, "faker_kwargs", None) or {}
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"
@@ -0,0 +1,9 @@
1
+ """Miscellaneous semantic Faker types (e.g. UUID)."""
2
+
3
+ from .base import FakerType
4
+
5
+
6
+ class UUID(FakerType):
7
+ """UUID v4 string. Uses Faker provider ``uuid4``."""
8
+
9
+ faker_provider = "uuid4"
@@ -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 typing import Type, TypeVar
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
- value = getattr(faker, provider_name)()
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
- [Name, Email, PhoneNumber, FirstName, Address, Sentence, CreditCardNumber],
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.2.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.9
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
  [![PyPI](https://img.shields.io/pypi/v/capper.svg)](https://pypi.org/project/capper/)
28
- [![Python](https://img.shields.io/badge/python-3.9+-blue.svg)](https://pypi.org/project/capper/)
31
+ [![Python](https://img.shields.io/badge/python-3.10+-blue.svg)](https://pypi.org/project/capper/)
29
32
  [![CI](https://img.shields.io/github/actions/workflow/status/eddiethedean/capper/ci.yml?branch=main&label=CI)](https://github.com/eddiethedean/capper/actions/workflows/ci.yml)
30
33
  [![Ruff](https://img.shields.io/badge/code%20style-ruff-000000.svg)](https://docs.astral.sh/ruff/)
31
34
  [![mypy](https://img.shields.io/badge/mypy-checked-blue.svg)](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.9+**, **Faker >= 20.0**, and **Polyfactory >= 2.0**. Optional extras:
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** and **Polyfactory >= 2.0**. Major Faker upgrades may change or rename provider methods; if a type fails, check [Faker's changelog](https://faker.readthedocs.io/en/stable/changelog.html) and [docs/FAKER_PROVIDERS.md](https://github.com/eddiethedean/capper/blob/main/docs/FAKER_PROVIDERS.md) and update the provider name if needed.
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. Add a `PYPI_API_TOKEN` secret (PyPI API token) to the repo.
186
- 2. Create a GitHub release (tag e.g. `v0.2.0`). The workflow runs tests, builds the package, and uploads to PyPI.
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
@@ -10,6 +10,10 @@ ruff>=0.4
10
10
  mypy>=1.0
11
11
  hypothesis>=6.0
12
12
 
13
+ [docs]
14
+ mkdocs>=1.5
15
+ mkdocstrings[python]>=0.24
16
+
13
17
  [hypothesis]
14
18
  hypothesis>=6.0
15
19
 
@@ -4,10 +4,10 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "capper"
7
- version = "0.2.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.9"
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 = "py39"
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.9"
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