capper 0.3.0__tar.gz → 0.4.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 (40) hide show
  1. {capper-0.3.0 → capper-0.4.0}/PKG-INFO +21 -4
  2. {capper-0.3.0 → capper-0.4.0}/README.md +19 -3
  3. {capper-0.3.0 → capper-0.4.0}/capper/__init__.py +1 -1
  4. capper-0.4.0/capper/base.py +127 -0
  5. {capper-0.3.0 → capper-0.4.0}/capper/cli.py +15 -20
  6. capper-0.4.0/capper/registry.py +26 -0
  7. {capper-0.3.0 → capper-0.4.0}/capper/strategies.py +5 -47
  8. capper-0.4.0/capper/tests/benchmark_core.py +56 -0
  9. capper-0.4.0/capper/tests/test_cli.py +78 -0
  10. capper-0.4.0/capper/tests/test_edge_cases.py +141 -0
  11. capper-0.4.0/capper/tests/test_thread_safety.py +102 -0
  12. {capper-0.3.0 → capper-0.4.0}/capper/tests/test_types.py +23 -8
  13. {capper-0.3.0 → capper-0.4.0}/capper.egg-info/PKG-INFO +21 -4
  14. {capper-0.3.0 → capper-0.4.0}/capper.egg-info/SOURCES.txt +4 -0
  15. {capper-0.3.0 → capper-0.4.0}/capper.egg-info/requires.txt +1 -0
  16. {capper-0.3.0 → capper-0.4.0}/pyproject.toml +10 -1
  17. capper-0.3.0/capper/base.py +0 -100
  18. capper-0.3.0/capper/tests/test_cli.py +0 -41
  19. {capper-0.3.0 → capper-0.4.0}/capper/barcode.py +0 -0
  20. {capper-0.3.0 → capper-0.4.0}/capper/color.py +0 -0
  21. {capper-0.3.0 → capper-0.4.0}/capper/commerce.py +0 -0
  22. {capper-0.3.0 → capper-0.4.0}/capper/date_time.py +0 -0
  23. {capper-0.3.0 → capper-0.4.0}/capper/examples/user_factory.py +0 -0
  24. {capper-0.3.0 → capper-0.4.0}/capper/file.py +0 -0
  25. {capper-0.3.0 → capper-0.4.0}/capper/finance.py +0 -0
  26. {capper-0.3.0 → capper-0.4.0}/capper/geo.py +0 -0
  27. {capper-0.3.0 → capper-0.4.0}/capper/internet.py +0 -0
  28. {capper-0.3.0 → capper-0.4.0}/capper/misc.py +0 -0
  29. {capper-0.3.0 → capper-0.4.0}/capper/person.py +0 -0
  30. {capper-0.3.0 → capper-0.4.0}/capper/phone.py +0 -0
  31. {capper-0.3.0 → capper-0.4.0}/capper/tests/__init__.py +0 -0
  32. {capper-0.3.0 → capper-0.4.0}/capper/tests/conftest.py +0 -0
  33. {capper-0.3.0 → capper-0.4.0}/capper/tests/test_docs_examples.py +0 -0
  34. {capper-0.3.0 → capper-0.4.0}/capper/tests/test_hypothesis_strategies.py +0 -0
  35. {capper-0.3.0 → capper-0.4.0}/capper/tests/test_polyfactory_integration.py +0 -0
  36. {capper-0.3.0 → capper-0.4.0}/capper/text.py +0 -0
  37. {capper-0.3.0 → capper-0.4.0}/capper.egg-info/dependency_links.txt +0 -0
  38. {capper-0.3.0 → capper-0.4.0}/capper.egg-info/entry_points.txt +0 -0
  39. {capper-0.3.0 → capper-0.4.0}/capper.egg-info/top_level.txt +0 -0
  40. {capper-0.3.0 → capper-0.4.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: capper
3
- Version: 0.3.0
3
+ Version: 0.4.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
@@ -15,6 +15,7 @@ Provides-Extra: hypothesis
15
15
  Requires-Dist: hypothesis>=6.0; extra == "hypothesis"
16
16
  Provides-Extra: dev
17
17
  Requires-Dist: pytest>=7.0; extra == "dev"
18
+ Requires-Dist: pytest-benchmark>=4.0; extra == "dev"
18
19
  Requires-Dist: pytest-cov>=4.0; extra == "dev"
19
20
  Requires-Dist: pytest-xdist>=3.0; extra == "dev"
20
21
  Requires-Dist: pydantic>=2.0; extra == "dev"
@@ -29,7 +30,7 @@ Requires-Dist: mkdocstrings[python]>=0.24; extra == "docs"
29
30
 
30
31
  [![PyPI](https://img.shields.io/pypi/v/capper.svg)](https://pypi.org/project/capper/)
31
32
  [![Python](https://img.shields.io/badge/python-3.10+-blue.svg)](https://pypi.org/project/capper/)
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)
33
+ [![CI](https://img.shields.io/github/actions/workflow/status/eddiethedean/capper/ci.yml?branch=main&label=CI%20(lint%2C%20types%2C%20tests%2C%20docs))](https://github.com/eddiethedean/capper/actions/workflows/ci.yml)
33
34
  [![Ruff](https://img.shields.io/badge/code%20style-ruff-000000.svg)](https://docs.astral.sh/ruff/)
34
35
  [![mypy](https://img.shields.io/badge/mypy-checked-blue.svg)](https://mypy-lang.org/)
35
36
 
@@ -37,11 +38,21 @@ Semantic, typed wrappers for [Faker](https://faker.readthedocs.io/) with automat
37
38
 
38
39
  **Source:** [github.com/eddiethedean/capper](https://github.com/eddiethedean/capper)
39
40
 
41
+ ## CI pipeline
42
+
43
+ The `ci.yml` workflow runs on pushes and PRs to `main` and includes:
44
+
45
+ - **Linting:** `ruff check .` and `ruff format --check .`
46
+ - **Type checking:** `mypy capper`
47
+ - **Tests:** `pytest -n auto capper/tests -v -m "not benchmark" --cov=capper --cov-report=term-missing --cov-fail-under=98`
48
+ - **Docs:** `mkdocs build --strict`
49
+
40
50
  ## Why Capper?
41
51
 
42
52
  - **Zero config** — Import a type; Polyfactory uses the right Faker provider. No manual registration.
43
53
  - **Typed** — Use `Name`, `Email`, `PhoneNumber`, etc. in your models for clear intent and IDE support.
44
54
  - **Multi-backend** — Works with Pydantic, dataclasses, attrs, and other [Polyfactory-supported](https://polyfactory.litestar.dev/) model types.
55
+ - **Thread-safe** — Per-thread Faker via a proxy; seeding and locales are isolated per thread, so concurrent tests are safe.
45
56
  - **Optional Pydantic** — Install `capper` alone for dataclasses/attrs; add `capper[pydantic]` when you use Pydantic models.
46
57
 
47
58
  ## Install
@@ -151,6 +162,12 @@ Use `-n`/`--count` for the number of rows and `-s`/`--seed` for reproducible out
151
162
 
152
163
  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).
153
164
 
165
+ ## What's new in 0.4.0
166
+
167
+ - **Thread safety:** Capper is now thread-safe via a per-thread Faker proxy; `seed()` and `use_faker()` only affect the current thread.
168
+ - **Reliability and coverage:** Phase 9 adds a coverage gate (≥ 98% for `capper/`), targeted edge-case tests, and a lightweight performance check in CI.
169
+ - **Tooling and docs:** CI runs Ruff, mypy, tests (with coverage gate), and a strict MkDocs build on all supported Python versions; docs and roadmap have been updated to reflect Phase 9.
170
+
154
171
  ## Development
155
172
 
156
173
  ```bash
@@ -160,7 +177,7 @@ pytest capper/tests
160
177
 
161
178
  Lint and type-check: `ruff check .`, `ruff format .`, `mypy capper`.
162
179
 
163
- Run tests with coverage: `pytest capper/tests --cov=capper --cov-report=term-missing`.
180
+ Run tests with coverage: `pytest capper/tests --cov=capper --cov-report=term-missing`. CI requires coverage ≥ 98% for the `capper/` package (`--cov-fail-under=98`).
164
181
 
165
182
  **Reproducibility:** Capper and Polyfactory share the same Faker instance, so one seed controls both capper types and built-in types (`str`, `int`, etc.):
166
183
 
@@ -191,7 +208,7 @@ Releases are built and published to PyPI via [GitHub Actions](https://github.com
191
208
 
192
209
  1. Update [CHANGELOG.md](CHANGELOG.md): move Unreleased entries into a new version section and date it.
193
210
  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.
211
+ 3. Create a GitHub release (tag e.g. `v0.4.0`). The workflow runs tests, builds the package, and uploads to PyPI.
195
212
 
196
213
  To build and upload manually: `pip install build twine`, `python -m build`, `twine upload dist/*`.
197
214
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![PyPI](https://img.shields.io/pypi/v/capper.svg)](https://pypi.org/project/capper/)
4
4
  [![Python](https://img.shields.io/badge/python-3.10+-blue.svg)](https://pypi.org/project/capper/)
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)
5
+ [![CI](https://img.shields.io/github/actions/workflow/status/eddiethedean/capper/ci.yml?branch=main&label=CI%20(lint%2C%20types%2C%20tests%2C%20docs))](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/)
8
8
 
@@ -10,11 +10,21 @@ Semantic, typed wrappers for [Faker](https://faker.readthedocs.io/) with automat
10
10
 
11
11
  **Source:** [github.com/eddiethedean/capper](https://github.com/eddiethedean/capper)
12
12
 
13
+ ## CI pipeline
14
+
15
+ The `ci.yml` workflow runs on pushes and PRs to `main` and includes:
16
+
17
+ - **Linting:** `ruff check .` and `ruff format --check .`
18
+ - **Type checking:** `mypy capper`
19
+ - **Tests:** `pytest -n auto capper/tests -v -m "not benchmark" --cov=capper --cov-report=term-missing --cov-fail-under=98`
20
+ - **Docs:** `mkdocs build --strict`
21
+
13
22
  ## Why Capper?
14
23
 
15
24
  - **Zero config** — Import a type; Polyfactory uses the right Faker provider. No manual registration.
16
25
  - **Typed** — Use `Name`, `Email`, `PhoneNumber`, etc. in your models for clear intent and IDE support.
17
26
  - **Multi-backend** — Works with Pydantic, dataclasses, attrs, and other [Polyfactory-supported](https://polyfactory.litestar.dev/) model types.
27
+ - **Thread-safe** — Per-thread Faker via a proxy; seeding and locales are isolated per thread, so concurrent tests are safe.
18
28
  - **Optional Pydantic** — Install `capper` alone for dataclasses/attrs; add `capper[pydantic]` when you use Pydantic models.
19
29
 
20
30
  ## Install
@@ -124,6 +134,12 @@ Use `-n`/`--count` for the number of rows and `-s`/`--seed` for reproducible out
124
134
 
125
135
  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).
126
136
 
137
+ ## What's new in 0.4.0
138
+
139
+ - **Thread safety:** Capper is now thread-safe via a per-thread Faker proxy; `seed()` and `use_faker()` only affect the current thread.
140
+ - **Reliability and coverage:** Phase 9 adds a coverage gate (≥ 98% for `capper/`), targeted edge-case tests, and a lightweight performance check in CI.
141
+ - **Tooling and docs:** CI runs Ruff, mypy, tests (with coverage gate), and a strict MkDocs build on all supported Python versions; docs and roadmap have been updated to reflect Phase 9.
142
+
127
143
  ## Development
128
144
 
129
145
  ```bash
@@ -133,7 +149,7 @@ pytest capper/tests
133
149
 
134
150
  Lint and type-check: `ruff check .`, `ruff format .`, `mypy capper`.
135
151
 
136
- Run tests with coverage: `pytest capper/tests --cov=capper --cov-report=term-missing`.
152
+ Run tests with coverage: `pytest capper/tests --cov=capper --cov-report=term-missing`. CI requires coverage ≥ 98% for the `capper/` package (`--cov-fail-under=98`).
137
153
 
138
154
  **Reproducibility:** Capper and Polyfactory share the same Faker instance, so one seed controls both capper types and built-in types (`str`, `int`, etc.):
139
155
 
@@ -164,7 +180,7 @@ Releases are built and published to PyPI via [GitHub Actions](https://github.com
164
180
 
165
181
  1. Update [CHANGELOG.md](CHANGELOG.md): move Unreleased entries into a new version section and date it.
166
182
  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.
183
+ 3. Create a GitHub release (tag e.g. `v0.4.0`). The workflow runs tests, builds the package, and uploads to PyPI.
168
184
 
169
185
  To build and upload manually: `pip install build twine`, `python -m build`, `twine upload dist/*`.
170
186
 
@@ -11,7 +11,7 @@ try:
11
11
 
12
12
  __version__ = _version("capper")
13
13
  except Exception: # Package not installed (e.g. dev tree) or metadata missing
14
- __version__ = "0.3.0"
14
+ __version__ = "0.4.0"
15
15
 
16
16
  from .barcode import EAN8, EAN13
17
17
  from .base import FakerType, faker, seed, use_faker
@@ -0,0 +1,127 @@
1
+ """FakerType base class and per-thread Faker proxy with automatic Polyfactory registration.
2
+
3
+ The module-level ``faker`` is a proxy to a per-thread Faker instance. Each thread has its own
4
+ Faker; ``seed(n)`` and ``use_faker(instance)`` only affect the current thread. Polyfactory's
5
+ BaseFactory.__faker__ is set to the same proxy, so one seed per thread controls both capper
6
+ types and built-in types. Thread-safe for concurrent use from multiple threads.
7
+ """
8
+
9
+ import threading
10
+ from typing import Any, cast
11
+
12
+ from faker import Faker
13
+ from polyfactory.factories.base import BaseFactory
14
+
15
+ _faker_local = threading.local()
16
+
17
+
18
+ def _get_faker() -> Faker:
19
+ """Return the current thread's Faker instance, creating one if needed."""
20
+ try:
21
+ instance = _faker_local.current
22
+ except AttributeError:
23
+ instance = None
24
+ if instance is None:
25
+ instance = Faker()
26
+ _faker_local.current = instance
27
+ return cast(Faker, instance)
28
+
29
+
30
+ class _FakerProxy:
31
+ """Proxy that forwards all attribute access to the current thread's Faker."""
32
+
33
+ __slots__ = ()
34
+
35
+ def __getattr__(self, name: str) -> Any:
36
+ return getattr(_get_faker(), name)
37
+
38
+
39
+ # Single process-wide proxy; each thread gets its own Faker via _get_faker().
40
+ faker: Faker = _FakerProxy() # type: ignore[assignment]
41
+ BaseFactory.__faker__ = faker
42
+
43
+
44
+ def seed(seed_value: int) -> None:
45
+ """Seed the current thread's Faker instance for reproducible data.
46
+
47
+ Args:
48
+ seed_value: Integer seed; same value produces the same sequence of values.
49
+ """
50
+ _get_faker().seed_instance(seed_value)
51
+
52
+
53
+ def use_faker(instance: Faker | None) -> None:
54
+ """Use a custom Faker instance for the current thread only.
55
+
56
+ Sets the Faker used by Capper and Polyfactory for this thread (e.g. a
57
+ locale-specific Faker). Other threads are unaffected. Call before building
58
+ any models in this thread. Pass None to reset this thread to a new default
59
+ Faker (e.g. after temporarily using a custom instance).
60
+
61
+ Args:
62
+ instance: The Faker instance to use (e.g. Faker('de_DE')), or None to reset.
63
+ """
64
+ if instance is None or instance is faker:
65
+ try:
66
+ del _faker_local.current
67
+ except AttributeError:
68
+ pass
69
+ else:
70
+ _faker_local.current = instance
71
+
72
+
73
+ def _install_pydantic_schema() -> None:
74
+ """If Pydantic is available, attach __get_pydantic_core_schema__ to FakerType."""
75
+ try:
76
+ from pydantic import GetCoreSchemaHandler
77
+ from pydantic_core import CoreSchema, core_schema
78
+ except ImportError:
79
+ return # pragma: no cover — pydantic not installed
80
+
81
+ def __get_pydantic_core_schema__(
82
+ cls, source_type: Any, handler: GetCoreSchemaHandler
83
+ ) -> CoreSchema:
84
+ """Validate as str then coerce to the FakerType subclass."""
85
+ return core_schema.no_info_after_validator_function(cls, handler(str))
86
+
87
+ FakerType.__get_pydantic_core_schema__ = classmethod(__get_pydantic_core_schema__) # type: ignore[attr-defined]
88
+
89
+
90
+ class FakerType(str):
91
+ """Base class for semantic Faker types; subclasses auto-register with Polyfactory.
92
+
93
+ Subclasses must set a non-empty ``faker_provider`` (the Faker method name).
94
+ Optional ``faker_kwargs`` is a dict of keyword arguments passed to that provider
95
+ (e.g. ``faker_kwargs = {"nb_words": 10}`` for ``sentence``).
96
+ When Hypothesis is installed, use ``st.from_type(YourFakerType)`` for property-based tests.
97
+ """
98
+
99
+ faker_provider: str = ""
100
+
101
+ def __init_subclass__(cls, **kwargs: object) -> None:
102
+ super().__init_subclass__(**kwargs)
103
+ provider = getattr(cls, "faker_provider", None)
104
+ if provider:
105
+ _register(cls, provider)
106
+
107
+
108
+ _install_pydantic_schema()
109
+
110
+
111
+ def _register(cls: type[FakerType], provider_name: str) -> None:
112
+ """Register a FakerType subclass with Polyfactory so factories can generate values."""
113
+ if not hasattr(faker, provider_name):
114
+ raise AttributeError(
115
+ f"Faker has no provider {provider_name!r} (used by {cls.__name__}). "
116
+ "Check faker_provider on the type."
117
+ )
118
+ provider_fn = getattr(faker, provider_name)
119
+ if not callable(provider_fn):
120
+ raise TypeError(f"Faker.{provider_name} is not callable (used by {cls.__name__}).")
121
+ provider_kwargs = dict(getattr(cls, "faker_kwargs", None) or {})
122
+
123
+ def _provide() -> str:
124
+ value = getattr(faker, provider_name)(**provider_kwargs)
125
+ return str(value)
126
+
127
+ BaseFactory.add_provider(cls, _provide)
@@ -2,29 +2,17 @@
2
2
 
3
3
  import argparse
4
4
  import sys
5
- from typing import Type
5
+ from typing import Mapping, Sequence, Type
6
6
 
7
7
  import capper
8
8
 
9
9
  from .base import FakerType, faker, seed
10
+ from .registry import build_type_registry
10
11
 
11
12
 
12
- def _type_registry() -> dict[str, Type[FakerType]]:
13
+ def _type_registry() -> Mapping[str, Type[FakerType]]:
13
14
  """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
15
+ return build_type_registry(capper)
28
16
 
29
17
 
30
18
  def _generate_one(typ: Type[FakerType]) -> str:
@@ -35,6 +23,15 @@ def _generate_one(typ: Type[FakerType]) -> str:
35
23
  return str(value)
36
24
 
37
25
 
26
+ def _generate_rows(types: Sequence[Type[FakerType]], count: int) -> list[str]:
27
+ """Generate tab-separated rows for the given types."""
28
+ rows: list[str] = []
29
+ for _ in range(max(0, count)):
30
+ row = [_generate_one(t) for t in types]
31
+ rows.append("\t".join(row))
32
+ return rows
33
+
34
+
38
35
  def cmd_generate(args: argparse.Namespace) -> int:
39
36
  """Run generate subcommand."""
40
37
  registry = _type_registry()
@@ -49,10 +46,8 @@ def cmd_generate(args: argparse.Namespace) -> int:
49
46
  if args.seed is not None:
50
47
  seed(args.seed)
51
48
 
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))
49
+ for line in _generate_rows(types, args.count):
50
+ print(line)
56
51
  return 0
57
52
 
58
53
 
@@ -0,0 +1,26 @@
1
+ from __future__ import annotations
2
+
3
+ from types import ModuleType
4
+ from typing import Dict, Type
5
+
6
+ from .base import FakerType
7
+
8
+
9
+ def build_type_registry(module: ModuleType) -> dict[str, Type[FakerType]]:
10
+ """Return a mapping of public name -> FakerType subclass for the given module.
11
+
12
+ Only names exported via ``module.__all__`` and backed by FakerType subclasses
13
+ with a non-empty ``faker_provider`` are included.
14
+ """
15
+ registry: Dict[str, Type[FakerType]] = {}
16
+ for name in getattr(module, "__all__", []):
17
+ obj = getattr(module, name, None)
18
+ provider = getattr(obj, "faker_provider", None)
19
+ if (
20
+ isinstance(obj, type)
21
+ and issubclass(obj, FakerType)
22
+ and isinstance(provider, str)
23
+ and len(provider) > 0
24
+ ):
25
+ registry[name] = obj
26
+ return registry
@@ -10,57 +10,15 @@ from typing import Type, TypeVar, cast
10
10
 
11
11
  from hypothesis import strategies as st
12
12
 
13
- from .barcode import EAN8, EAN13
13
+ import capper
14
+
14
15
  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
16
+ from .registry import build_type_registry
26
17
 
27
18
  T = TypeVar("T", bound=FakerType)
28
19
 
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
- )
20
+ # All built-in FakerType subclasses for registration, discovered from the public API.
21
+ _BUILTIN_TYPES: tuple[type[FakerType], ...] = tuple(build_type_registry(capper).values())
64
22
 
65
23
 
66
24
  def for_type(cls: Type[T]) -> st.SearchStrategy[T]:
@@ -0,0 +1,56 @@
1
+ """Performance benchmarks for core flows (ModelFactory, DataclassFactory, Hypothesis strategy).
2
+
3
+ Run with: pytest capper/tests/benchmark_core.py -v --benchmark-only
4
+ Exclude from default test run: pytest -m 'not benchmark'
5
+ """
6
+
7
+ from dataclasses import dataclass
8
+
9
+ import pytest
10
+
11
+ from capper import Email, Name
12
+
13
+
14
+ @pytest.mark.benchmark
15
+ def test_bench_model_factory_build(benchmark: object) -> None:
16
+ """Benchmark ModelFactory.build() with 2 Capper types (Name, Email)."""
17
+ from polyfactory.factories.pydantic_factory import ModelFactory
18
+ from pydantic import BaseModel
19
+
20
+ class User(BaseModel):
21
+ name: Name
22
+ email: Email
23
+
24
+ class UserFactory(ModelFactory[User]):
25
+ pass
26
+
27
+ benchmark(UserFactory.build)
28
+
29
+
30
+ @pytest.mark.benchmark
31
+ def test_bench_dataclass_factory_build(benchmark: object) -> None:
32
+ """Benchmark DataclassFactory.build() with 2 Capper types (Name, Email)."""
33
+ from polyfactory.factories import DataclassFactory
34
+
35
+ @dataclass
36
+ class Person:
37
+ name: Name
38
+ email: Email
39
+
40
+ class PersonFactory(DataclassFactory[Person]):
41
+ pass
42
+
43
+ benchmark(PersonFactory.build)
44
+
45
+
46
+ @pytest.mark.benchmark
47
+ def test_bench_hypothesis_strategy_example(benchmark: object) -> None:
48
+ """Benchmark drawing one value from st.from_type(Name) via strategy.example()."""
49
+ from capper import strategies
50
+
51
+ strategy = strategies.for_type(Name)
52
+
53
+ def draw_one() -> None:
54
+ strategy.example()
55
+
56
+ benchmark(draw_one)
@@ -0,0 +1,78 @@
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
42
+
43
+
44
+ @pytest.mark.skipif(sys.version_info < (3, 10), reason="capper CLI uses Python 3.10+ syntax")
45
+ def test_type_registry_includes_known_type() -> None:
46
+ """Internal type registry used by CLI contains a known FakerType."""
47
+ assert hasattr(cli, "_type_registry")
48
+ registry = cli._type_registry()
49
+ assert "Name" in registry
50
+ name_type = registry["Name"]
51
+ assert getattr(name_type, "faker_provider", "") == "name"
52
+
53
+
54
+ @pytest.mark.skipif(sys.version_info < (3, 10), reason="capper CLI uses Python 3.10+ syntax")
55
+ def test_cli_unknown_type_exits_nonzero_and_stderr(capsys: pytest.CaptureFixture[str]) -> None:
56
+ """Passing an unknown type name prints to stderr and returns 1."""
57
+ orig_argv = sys.argv
58
+ try:
59
+ sys.argv = ["capper", "generate", "NonExistentType", "--count", "1"]
60
+ exit_code = cli.main()
61
+ assert exit_code == 1
62
+ err = capsys.readouterr().err
63
+ assert "Unknown type" in err
64
+ assert "NonExistentType" in err
65
+ finally:
66
+ sys.argv = orig_argv
67
+
68
+
69
+ @pytest.mark.skipif(sys.version_info < (3, 10), reason="capper CLI uses Python 3.10+ syntax")
70
+ def test_cli_seed_used() -> None:
71
+ """Generate with --seed runs and uses seed (deterministic output)."""
72
+ orig_argv = sys.argv
73
+ try:
74
+ sys.argv = ["capper", "generate", "Name", "--count", "2", "--seed", "42"]
75
+ exit_code = cli.main()
76
+ assert exit_code == 0
77
+ finally:
78
+ sys.argv = orig_argv
@@ -0,0 +1,141 @@
1
+ """Edge-case tests: version fallback, use_faker/thread, registration and strategy errors."""
2
+
3
+ import sys
4
+ from unittest.mock import patch
5
+
6
+ import pytest
7
+
8
+ from capper import use_faker
9
+ from capper.base import FakerType
10
+
11
+
12
+ def test_version_fallback_when_metadata_missing() -> None:
13
+ """When importlib.metadata.version raises, __version__ falls back to 0.4.0."""
14
+ with patch("importlib.metadata.version", side_effect=Exception("no metadata")):
15
+ if "capper" in sys.modules:
16
+ del sys.modules["capper"]
17
+ import capper as capper_mod
18
+
19
+ assert capper_mod.__version__ == "0.4.0"
20
+ # Reimport so later tests see the real version
21
+ if "capper" in sys.modules:
22
+ del sys.modules["capper"]
23
+ import capper # noqa: F401
24
+
25
+
26
+ def test_use_faker_none_in_fresh_thread_covers_attribute_error() -> None:
27
+ """use_faker(None) in a thread that never set faker hits the except AttributeError path."""
28
+ result: list[Exception | None] = []
29
+
30
+ def run_in_thread() -> None:
31
+ try:
32
+ use_faker(None)
33
+ result.append(None)
34
+ except Exception as e:
35
+ result.append(e)
36
+
37
+ import threading
38
+
39
+ t = threading.Thread(target=run_in_thread)
40
+ t.start()
41
+ t.join()
42
+ assert result == [None]
43
+
44
+
45
+ def test_register_invalid_provider_raises_attribute_error() -> None:
46
+ """Defining a FakerType subclass with nonexistent faker_provider raises AttributeError."""
47
+ with pytest.raises(AttributeError, match="Faker has no provider .nonexistent_xyz."):
48
+
49
+ class BadType(FakerType):
50
+ faker_provider = "nonexistent_xyz"
51
+
52
+
53
+ def test_register_non_callable_provider_raises_type_error() -> None:
54
+ """When Faker has the provider name but it is not callable, _register raises TypeError."""
55
+ from faker import Faker
56
+
57
+ my_faker = Faker()
58
+ with patch.object(my_faker, "name", "not_callable"):
59
+ use_faker(my_faker)
60
+ try:
61
+ with pytest.raises(TypeError, match="is not callable"):
62
+
63
+ class BadCallableType(FakerType):
64
+ faker_provider = "name"
65
+
66
+ finally:
67
+ use_faker(None)
68
+
69
+
70
+ def test_strategies_for_type_no_provider_raises_value_error() -> None:
71
+ """strategies.for_type() on a class with no faker_provider raises ValueError."""
72
+ from capper import strategies
73
+
74
+ class NoProvider(FakerType):
75
+ faker_provider = ""
76
+
77
+ with pytest.raises(ValueError, match="has no faker_provider"):
78
+ strategies.for_type(NoProvider)
79
+
80
+
81
+ def test_strategies_for_type_invalid_provider_raises_attribute_error() -> None:
82
+ """strategies.for_type() when current Faker has no such provider raises AttributeError."""
83
+ from capper import Name, strategies
84
+
85
+ class FakeFakerWithoutName:
86
+ """Minimal Faker-like object missing the 'name' provider."""
87
+
88
+ pass
89
+
90
+ use_faker(FakeFakerWithoutName())
91
+ try:
92
+ with pytest.raises(AttributeError, match="Faker has no provider"):
93
+ strategies.for_type(Name)
94
+ finally:
95
+ use_faker(None)
96
+
97
+
98
+ def test_use_faker_locale_german() -> None:
99
+ """use_faker(Faker('de_DE')) produces locale-appropriate data; reset with use_faker(None)."""
100
+ from faker import Faker
101
+ from polyfactory.factories.pydantic_factory import ModelFactory
102
+ from pydantic import BaseModel
103
+
104
+ from capper import Address, Name
105
+
106
+ class User(BaseModel):
107
+ name: Name
108
+ address: Address
109
+
110
+ class UserFactory(ModelFactory[User]):
111
+ pass
112
+
113
+ try:
114
+ use_faker(Faker("de_DE"))
115
+ user = UserFactory.build()
116
+ assert len(str(user.name)) > 0
117
+ assert len(str(user.address)) > 0
118
+ # German locale often yields umlauts or typical patterns; at least non-ASCII is possible
119
+ combined = str(user.name) + str(user.address)
120
+ assert combined.isascii() or any(c in "äöüßÄÖÜ" for c in combined)
121
+ finally:
122
+ use_faker(None)
123
+
124
+
125
+ def test_strategies_for_type_non_callable_provider_raises_type_error() -> None:
126
+ """strategies.for_type() when Faker's provider is not callable raises TypeError."""
127
+ from faker import Faker
128
+
129
+ from capper import strategies
130
+
131
+ class NotCallableProvider(FakerType):
132
+ faker_provider = "name"
133
+
134
+ my_faker = Faker()
135
+ with patch.object(my_faker, "name", "not_callable"):
136
+ use_faker(my_faker)
137
+ try:
138
+ with pytest.raises(TypeError, match="is not callable"):
139
+ strategies.for_type(NotCallableProvider)
140
+ finally:
141
+ use_faker(None)
@@ -0,0 +1,102 @@
1
+ """Tests for thread-safe per-thread Faker: use_faker() and seed() are isolated per thread."""
2
+
3
+ import threading
4
+
5
+ from capper import Name, seed, use_faker
6
+ from capper.base import _get_faker
7
+
8
+
9
+ def test_use_faker_per_thread_isolation() -> None:
10
+ """Two threads each set use_faker() to a different locale; builds use that thread's Faker."""
11
+ from faker import Faker
12
+ from polyfactory.factories.pydantic_factory import ModelFactory
13
+ from pydantic import BaseModel
14
+
15
+ results: dict[str, str] = {}
16
+
17
+ def thread_de() -> None:
18
+ use_faker(Faker("de_DE"))
19
+ assert _get_faker() is not None
20
+
21
+ # Build a model; German locale often produces umlauts in names
22
+ class User(BaseModel):
23
+ name: Name
24
+
25
+ class UserFactory(ModelFactory[User]):
26
+ pass
27
+
28
+ user = UserFactory.build()
29
+ results["de"] = user.name
30
+
31
+ def thread_en() -> None:
32
+ use_faker(Faker("en_US"))
33
+ assert _get_faker() is not None
34
+
35
+ class User(BaseModel):
36
+ name: Name
37
+
38
+ class UserFactory(ModelFactory[User]):
39
+ pass
40
+
41
+ user = UserFactory.build()
42
+ results["en"] = user.name
43
+
44
+ t1 = threading.Thread(target=thread_de)
45
+ t2 = threading.Thread(target=thread_en)
46
+ t1.start()
47
+ t2.start()
48
+ t1.join()
49
+ t2.join()
50
+
51
+ assert "de" in results and "en" in results
52
+ assert len(results["de"]) > 0 and len(results["en"]) > 0
53
+
54
+
55
+ def test_seed_per_thread_isolation() -> None:
56
+ """seed(n) in one thread does not affect another thread's sequence."""
57
+ from polyfactory.factories.pydantic_factory import ModelFactory
58
+ from pydantic import BaseModel
59
+
60
+ results: dict[str, str] = {}
61
+
62
+ def thread_seed_42() -> None:
63
+ seed(42)
64
+
65
+ class User(BaseModel):
66
+ name: Name
67
+
68
+ class UserFactory(ModelFactory[User]):
69
+ pass
70
+
71
+ results["42"] = UserFactory.build().name
72
+
73
+ def thread_seed_99() -> None:
74
+ seed(99)
75
+
76
+ class User(BaseModel):
77
+ name: Name
78
+
79
+ class UserFactory(ModelFactory[User]):
80
+ pass
81
+
82
+ results["99"] = UserFactory.build().name
83
+
84
+ t1 = threading.Thread(target=thread_seed_42)
85
+ t2 = threading.Thread(target=thread_seed_99)
86
+ t1.start()
87
+ t2.start()
88
+ t1.join()
89
+ t2.join()
90
+
91
+ assert "42" in results and "99" in results
92
+ # Same seed in same thread gives same result when run again
93
+ seed(42)
94
+
95
+ class User(BaseModel):
96
+ name: Name
97
+
98
+ class UserFactory(ModelFactory[User]):
99
+ pass
100
+
101
+ again = UserFactory.build().name
102
+ assert again == results["42"]
@@ -178,23 +178,17 @@ def test_seed_reproducibility(seeded_faker: None) -> None:
178
178
 
179
179
 
180
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)."""
181
+ """use_faker() sets this thread's Faker; next factory build uses that instance (e.g. seed)."""
182
182
  from faker import Faker
183
- from polyfactory.factories.base import BaseFactory
184
183
  from polyfactory.factories.pydantic_factory import ModelFactory
185
184
  from pydantic import BaseModel
186
185
 
187
186
  from capper import Name, use_faker
188
- from capper import faker as default_faker
189
187
 
190
188
  try:
191
189
  custom = Faker()
192
190
  custom.seed_instance(123)
193
191
  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
192
 
199
193
  class User(BaseModel):
200
194
  name: Name
@@ -209,4 +203,25 @@ def test_use_faker_switches_global_faker() -> None:
209
203
  user2 = UserFactory.build()
210
204
  assert user1.name == user2.name
211
205
  finally:
212
- use_faker(default_faker)
206
+ use_faker(None)
207
+
208
+
209
+ def test_performance_1000_builds_under_threshold() -> None:
210
+ """Lightweight performance gate: 1000 ModelFactory.build() calls complete in under 30s."""
211
+ import time
212
+
213
+ from polyfactory.factories.pydantic_factory import ModelFactory
214
+ from pydantic import BaseModel
215
+
216
+ class User(BaseModel):
217
+ name: Name
218
+ email: Email
219
+
220
+ class UserFactory(ModelFactory[User]):
221
+ pass
222
+
223
+ start = time.perf_counter()
224
+ for _ in range(1000):
225
+ UserFactory.build()
226
+ elapsed = time.perf_counter() - start
227
+ assert elapsed < 30.0, f"1000 builds took {elapsed:.1f}s (threshold 30s)"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: capper
3
- Version: 0.3.0
3
+ Version: 0.4.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
@@ -15,6 +15,7 @@ Provides-Extra: hypothesis
15
15
  Requires-Dist: hypothesis>=6.0; extra == "hypothesis"
16
16
  Provides-Extra: dev
17
17
  Requires-Dist: pytest>=7.0; extra == "dev"
18
+ Requires-Dist: pytest-benchmark>=4.0; extra == "dev"
18
19
  Requires-Dist: pytest-cov>=4.0; extra == "dev"
19
20
  Requires-Dist: pytest-xdist>=3.0; extra == "dev"
20
21
  Requires-Dist: pydantic>=2.0; extra == "dev"
@@ -29,7 +30,7 @@ Requires-Dist: mkdocstrings[python]>=0.24; extra == "docs"
29
30
 
30
31
  [![PyPI](https://img.shields.io/pypi/v/capper.svg)](https://pypi.org/project/capper/)
31
32
  [![Python](https://img.shields.io/badge/python-3.10+-blue.svg)](https://pypi.org/project/capper/)
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)
33
+ [![CI](https://img.shields.io/github/actions/workflow/status/eddiethedean/capper/ci.yml?branch=main&label=CI%20(lint%2C%20types%2C%20tests%2C%20docs))](https://github.com/eddiethedean/capper/actions/workflows/ci.yml)
33
34
  [![Ruff](https://img.shields.io/badge/code%20style-ruff-000000.svg)](https://docs.astral.sh/ruff/)
34
35
  [![mypy](https://img.shields.io/badge/mypy-checked-blue.svg)](https://mypy-lang.org/)
35
36
 
@@ -37,11 +38,21 @@ Semantic, typed wrappers for [Faker](https://faker.readthedocs.io/) with automat
37
38
 
38
39
  **Source:** [github.com/eddiethedean/capper](https://github.com/eddiethedean/capper)
39
40
 
41
+ ## CI pipeline
42
+
43
+ The `ci.yml` workflow runs on pushes and PRs to `main` and includes:
44
+
45
+ - **Linting:** `ruff check .` and `ruff format --check .`
46
+ - **Type checking:** `mypy capper`
47
+ - **Tests:** `pytest -n auto capper/tests -v -m "not benchmark" --cov=capper --cov-report=term-missing --cov-fail-under=98`
48
+ - **Docs:** `mkdocs build --strict`
49
+
40
50
  ## Why Capper?
41
51
 
42
52
  - **Zero config** — Import a type; Polyfactory uses the right Faker provider. No manual registration.
43
53
  - **Typed** — Use `Name`, `Email`, `PhoneNumber`, etc. in your models for clear intent and IDE support.
44
54
  - **Multi-backend** — Works with Pydantic, dataclasses, attrs, and other [Polyfactory-supported](https://polyfactory.litestar.dev/) model types.
55
+ - **Thread-safe** — Per-thread Faker via a proxy; seeding and locales are isolated per thread, so concurrent tests are safe.
45
56
  - **Optional Pydantic** — Install `capper` alone for dataclasses/attrs; add `capper[pydantic]` when you use Pydantic models.
46
57
 
47
58
  ## Install
@@ -151,6 +162,12 @@ Use `-n`/`--count` for the number of rows and `-s`/`--seed` for reproducible out
151
162
 
152
163
  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).
153
164
 
165
+ ## What's new in 0.4.0
166
+
167
+ - **Thread safety:** Capper is now thread-safe via a per-thread Faker proxy; `seed()` and `use_faker()` only affect the current thread.
168
+ - **Reliability and coverage:** Phase 9 adds a coverage gate (≥ 98% for `capper/`), targeted edge-case tests, and a lightweight performance check in CI.
169
+ - **Tooling and docs:** CI runs Ruff, mypy, tests (with coverage gate), and a strict MkDocs build on all supported Python versions; docs and roadmap have been updated to reflect Phase 9.
170
+
154
171
  ## Development
155
172
 
156
173
  ```bash
@@ -160,7 +177,7 @@ pytest capper/tests
160
177
 
161
178
  Lint and type-check: `ruff check .`, `ruff format .`, `mypy capper`.
162
179
 
163
- Run tests with coverage: `pytest capper/tests --cov=capper --cov-report=term-missing`.
180
+ Run tests with coverage: `pytest capper/tests --cov=capper --cov-report=term-missing`. CI requires coverage ≥ 98% for the `capper/` package (`--cov-fail-under=98`).
164
181
 
165
182
  **Reproducibility:** Capper and Polyfactory share the same Faker instance, so one seed controls both capper types and built-in types (`str`, `int`, etc.):
166
183
 
@@ -191,7 +208,7 @@ Releases are built and published to PyPI via [GitHub Actions](https://github.com
191
208
 
192
209
  1. Update [CHANGELOG.md](CHANGELOG.md): move Unreleased entries into a new version section and date it.
193
210
  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.
211
+ 3. Create a GitHub release (tag e.g. `v0.4.0`). The workflow runs tests, builds the package, and uploads to PyPI.
195
212
 
196
213
  To build and upload manually: `pip install build twine`, `python -m build`, `twine upload dist/*`.
197
214
 
@@ -14,6 +14,7 @@ capper/internet.py
14
14
  capper/misc.py
15
15
  capper/person.py
16
16
  capper/phone.py
17
+ capper/registry.py
17
18
  capper/strategies.py
18
19
  capper/text.py
19
20
  capper.egg-info/PKG-INFO
@@ -24,9 +25,12 @@ capper.egg-info/requires.txt
24
25
  capper.egg-info/top_level.txt
25
26
  capper/examples/user_factory.py
26
27
  capper/tests/__init__.py
28
+ capper/tests/benchmark_core.py
27
29
  capper/tests/conftest.py
28
30
  capper/tests/test_cli.py
29
31
  capper/tests/test_docs_examples.py
32
+ capper/tests/test_edge_cases.py
30
33
  capper/tests/test_hypothesis_strategies.py
31
34
  capper/tests/test_polyfactory_integration.py
35
+ capper/tests/test_thread_safety.py
32
36
  capper/tests/test_types.py
@@ -3,6 +3,7 @@ Polyfactory>=2.0
3
3
 
4
4
  [dev]
5
5
  pytest>=7.0
6
+ pytest-benchmark>=4.0
6
7
  pytest-cov>=4.0
7
8
  pytest-xdist>=3.0
8
9
  pydantic>=2.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "capper"
7
- version = "0.3.0"
7
+ version = "0.4.0"
8
8
  description = "Semantic, typed wrappers for Faker with automatic Polyfactory integration"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -25,6 +25,7 @@ hypothesis = [
25
25
  ]
26
26
  dev = [
27
27
  "pytest>=7.0",
28
+ "pytest-benchmark>=4.0",
28
29
  "pytest-cov>=4.0",
29
30
  "pytest-xdist>=3.0",
30
31
  "pydantic>=2.0",
@@ -52,6 +53,14 @@ include = ["capper*"]
52
53
  testpaths = ["capper/tests"]
53
54
  pythonpath = ["."]
54
55
  addopts = ["-v", "--tb=short"]
56
+ markers = ["benchmark: performance benchmarks (deselect with -m 'not benchmark')"]
57
+
58
+ [tool.coverage.run]
59
+ source = ["capper"]
60
+ omit = ["capper/tests/*"]
61
+
62
+ [tool.coverage.report]
63
+ exclude_lines = ["pragma: no cover"]
55
64
 
56
65
  [tool.ruff]
57
66
  line-length = 100
@@ -1,100 +0,0 @@
1
- """FakerType base class and shared Faker instance with automatic Polyfactory registration.
2
-
3
- The module-level ``faker`` is attached to Polyfactory's BaseFactory so that one seed
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.
8
- """
9
-
10
- from typing import Any
11
-
12
- from faker import Faker
13
- from polyfactory.factories.base import BaseFactory
14
-
15
- faker = Faker()
16
-
17
- # Share this Faker with Polyfactory so one seed controls both capper types and built-in types.
18
- BaseFactory.__faker__ = faker
19
-
20
-
21
- def seed(seed_value: int) -> None:
22
- """Seed the shared Faker instance for reproducible data.
23
-
24
- Args:
25
- seed_value: Integer seed; same value produces the same sequence of values.
26
- """
27
- faker.seed_instance(seed_value)
28
-
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
-
48
- def _install_pydantic_schema() -> None:
49
- """If Pydantic is available, attach __get_pydantic_core_schema__ to FakerType."""
50
- try:
51
- from pydantic import GetCoreSchemaHandler
52
- from pydantic_core import CoreSchema, core_schema
53
- except ImportError:
54
- return
55
-
56
- def __get_pydantic_core_schema__(source_type: Any, handler: GetCoreSchemaHandler) -> CoreSchema:
57
- """Validate as str then coerce to the FakerType subclass."""
58
- return core_schema.no_info_after_validator_function(source_type, handler(str))
59
-
60
- FakerType.__get_pydantic_core_schema__ = __get_pydantic_core_schema__ # type: ignore[attr-defined]
61
-
62
-
63
- class FakerType(str):
64
- """Base class for semantic Faker types; subclasses auto-register with Polyfactory.
65
-
66
- Subclasses must set a non-empty ``faker_provider`` (the Faker method name).
67
- Optional ``faker_kwargs`` is a dict of keyword arguments passed to that provider
68
- (e.g. ``faker_kwargs = {"nb_words": 10}`` for ``sentence``).
69
- When Hypothesis is installed, use ``st.from_type(YourFakerType)`` for property-based tests.
70
- """
71
-
72
- faker_provider: str = ""
73
-
74
- def __init_subclass__(cls, **kwargs: object) -> None:
75
- super().__init_subclass__(**kwargs)
76
- provider = getattr(cls, "faker_provider", None)
77
- if provider:
78
- _register(cls, provider)
79
-
80
-
81
- _install_pydantic_schema()
82
-
83
-
84
- def _register(cls: type, provider_name: str) -> None:
85
- """Register a FakerType subclass with Polyfactory so factories can generate values."""
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 {})
95
-
96
- def _provide() -> str:
97
- value = getattr(faker, provider_name)(**provider_kwargs)
98
- return str(value)
99
-
100
- BaseFactory.add_provider(cls, _provide)
@@ -1,41 +0,0 @@
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
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