aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authornatsuoto <[email protected]>2026-07-15 14:40:17 +0530
committernatsuoto <[email protected]>2026-07-15 14:40:17 +0530
commit578fd671992be4f45f87648e22343dc8171282ec (patch)
treeee66434f1991d0ecff80abe8da86cd3efdd2df34
parent86af31327dbff6f206a15e3172a8bedb18157c66 (diff)
downloadedify-578fd671992be4f45f87648e22343dc8171282ec.tar.xz
edify-578fd671992be4f45f87648e22343dc8171282ec.zip
docs(builder): document the immutability contract with branchability example in the API reference and README
-rw-r--r--README.rst1
-rw-r--r--docs/regex-builder/builder/index.rst27
-rw-r--r--edify/builder/builder.py10
3 files changed, 37 insertions, 1 deletions
diff --git a/README.rst b/README.rst
index e38d341..80e3288 100644
--- a/README.rst
+++ b/README.rst
@@ -246,6 +246,7 @@ That's where Edify becomes extremely useful. It allows you to create regular exp
- Order matters! Quantifiers are specified before the thing they change, just like in English (e.g. ``RegexBuilder().exactly(5).digit()``).
- If you make a mistake, you'll know how to fix it. Edify will guide you towards a fix if your expression is invalid.
- ``subexpressions`` can be used to create meaningful, reusable components.
+- Every chain method returns a new builder — the receiver is never mutated. Two extensions of the same base pattern are always independent, so reusable base builders are safe to share across modules, closures, and threads.
Edify turns those complex and unwieldy regexes that appear in code reviews into something that can be read, understood, and **properly reviewed** by your peers - and maintained by anyone!
diff --git a/docs/regex-builder/builder/index.rst b/docs/regex-builder/builder/index.rst
index 4b1702f..d7ca44b 100644
--- a/docs/regex-builder/builder/index.rst
+++ b/docs/regex-builder/builder/index.rst
@@ -8,6 +8,33 @@ RegexBuilder is a class that helps you build regular expressions. It is based on
- If you make a mistake, you'll know how to fix it. Edify will guide you towards a fix if your expression is invalid.
- ``subexpressions`` can be used to create meaningful, reusable components.
+Immutability contract
+---------------------
+
+Every chain method on ``RegexBuilder`` returns a **new** builder; the receiver is never mutated. Two consequences follow, and both are guaranteed by contract:
+
+1. **Branching is safe.** A base builder can be extended along multiple independent paths without any of them observing another's state:
+
+ .. code-block:: python
+
+ from edify import RegexBuilder
+
+ base = RegexBuilder().start_of_input().exactly(3).digit()
+ zip5 = base.exactly(2).digit().end_of_input()
+ zip3 = base.end_of_input()
+
+ assert zip5.to_regex_string() == "^\\d{3}\\d{2}$"
+ assert zip3.to_regex_string() == "^\\d{3}$"
+ assert base.to_regex_string() == "^\\d{3}"
+
+ ``zip5`` and ``zip3`` are independent chains rooted in the same ``base``; extending one has no effect on the other or on ``base`` itself.
+
+2. **Reusable base patterns are safe to share.** A base pattern held in a module-level constant, imported across files, or captured in a closure cannot be corrupted by any caller. The same object can be extended, forked, or handed to ``subexpression`` any number of times.
+
+The same guarantee holds for :class:`edify.Pattern` — it is a ``RegexBuilder`` under the hood and inherits the contract.
+
+The lazy compile cache preserves the guarantee: repeated no-kwargs ``.to_regex()`` calls on the same builder instance return the same :class:`Regex` object (``builder.to_regex() is builder.to_regex()``), but a chain step or ``fork()`` produces a fresh builder with its own empty cache.
+
.any_char()
-----------
diff --git a/edify/builder/builder.py b/edify/builder/builder.py
index a727ccf..cde72d9 100644
--- a/edify/builder/builder.py
+++ b/edify/builder/builder.py
@@ -37,7 +37,15 @@ class RegexBuilder(
TerminalsMixin,
TestingMixin,
):
- """A fluent, immutable, strongly-typed regex builder."""
+ """A fluent, immutable, strongly-typed regex builder.
+
+ Immutability is a hard contract: every chain method returns a *new* builder
+ and leaves the receiver untouched. A base builder held in a constant, closure,
+ or attribute can be extended along multiple independent paths without any of
+ them observing another's state. Repeat no-kwargs ``.to_regex()`` calls on the
+ same builder return the same cached :class:`Regex`; a chain step or ``fork()``
+ yields a fresh builder with its own empty cache.
+ """
@classmethod
def from_regex(cls, pattern_text: str) -> RegexBuilder: