From d7f3c345d22671d71b129036783ce0ce33105956 Mon Sep 17 00:00:00 2001 From: Mohammed Imaad Sharieff Date: Wed, 7 Jan 2026 11:18:32 +0530 Subject: [PATCH 1/5] Fix factory validation order --- src/attr/_make.py | 48 +++++++++++++++++++++++++++-------------------- 1 file changed, 28 insertions(+), 20 deletions(-) diff --git a/src/attr/_make.py b/src/attr/_make.py index d24d9ba98..c9ceae865 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -2280,6 +2280,7 @@ def _attrs_to_init_script( init_factory_name = _INIT_FACTORY_PAT % (a.name,) if converter is not None: + # arg was passed explicitly → validate immediately lines.append( " " + fmt_setter_with_converter( @@ -2287,11 +2288,25 @@ def _attrs_to_init_script( ) ) lines.append("else:") + # no arg passed → run factory → validate → assign + lines.append( + " " + + f"val = {init_factory_name}({maybe_self})" + ) + if a.validator is not None: + val_name = "__attr_validator_" + a.name + attr_name_ref = "__attr_" + a.name + lines.append( + " " + + f"{val_name}(self, {attr_name_ref}, val)" + ) + names_for_globals[val_name] = a.validator + names_for_globals[attr_name_ref] = a lines.append( " " + fmt_setter_with_converter( attr_name, - init_factory_name + "(" + maybe_self + ")", + "val", has_on_setattr, converter, ) @@ -2300,37 +2315,30 @@ def _attrs_to_init_script( converter.converter ) else: + # arg passed explicitly → validate immediately lines.append( " " + fmt_setter(attr_name, arg_name, has_on_setattr) ) lines.append("else:") + # no arg passed → run factory → validate → assign + lines.append( + " " + + f"val = {init_factory_name}({maybe_self})" + ) + lines.append( + " " + + f"{validator_name}(self, a, val)" + ) lines.append( " " + fmt_setter( attr_name, - init_factory_name + "(" + maybe_self + ")", + "val", has_on_setattr, ) ) - names_for_globals[init_factory_name] = a.default.factory - else: - if a.kw_only: - kw_only_args.append(arg_name) - else: - args.append(arg_name) - pre_init_args.append(arg_name) - if converter is not None: - lines.append( - fmt_setter_with_converter( - attr_name, arg_name, has_on_setattr, converter - ) - ) - names_for_globals[converter._get_global_name(a.name)] = ( - converter.converter - ) - else: - lines.append(fmt_setter(attr_name, arg_name, has_on_setattr)) + names_for_globals[init_factory_name] = a.default.factory if a.init is True: if a.type is not None and converter is None: From 81bbf6bb01595433489894bd8787208fbaaaa3f9 Mon Sep 17 00:00:00 2001 From: Mohammed Imaad Sharieff Date: Wed, 7 Jan 2026 11:22:12 +0530 Subject: [PATCH 2/5] Fix factory validation order --- src/attr/_make.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/attr/_make.py b/src/attr/_make.py index c9ceae865..40737e490 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -2325,10 +2325,15 @@ def _attrs_to_init_script( " " + f"val = {init_factory_name}({maybe_self})" ) - lines.append( - " " - + f"{validator_name}(self, a, val)" - ) + if a.validator is not None: + val_name = "__attr_validator_" + a.name + attr_name_ref = "__attr_" + a.name + lines.append( + " " + + f"{val_name}(self, {attr_name_ref}, val)" + ) + names_for_globals[val_name] = a.validator + names_for_globals[attr_name_ref] = a lines.append( " " + fmt_setter( From b68d6d4885bbff2c934aa5e5d9dbb7ed1c448bd9 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 7 Jan 2026 06:06:21 +0000 Subject: [PATCH 3/5] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/attr/_make.py | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/src/attr/_make.py b/src/attr/_make.py index 40737e490..c64863153 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -2289,17 +2289,11 @@ def _attrs_to_init_script( ) lines.append("else:") # no arg passed → run factory → validate → assign - lines.append( - " " - + f"val = {init_factory_name}({maybe_self})" - ) + lines.append(f" val = {init_factory_name}({maybe_self})") if a.validator is not None: val_name = "__attr_validator_" + a.name attr_name_ref = "__attr_" + a.name - lines.append( - " " - + f"{val_name}(self, {attr_name_ref}, val)" - ) + lines.append(f" {val_name}(self, {attr_name_ref}, val)") names_for_globals[val_name] = a.validator names_for_globals[attr_name_ref] = a lines.append( @@ -2321,17 +2315,11 @@ def _attrs_to_init_script( ) lines.append("else:") # no arg passed → run factory → validate → assign - lines.append( - " " - + f"val = {init_factory_name}({maybe_self})" - ) + lines.append(f" val = {init_factory_name}({maybe_self})") if a.validator is not None: val_name = "__attr_validator_" + a.name attr_name_ref = "__attr_" + a.name - lines.append( - " " - + f"{val_name}(self, {attr_name_ref}, val)" - ) + lines.append(f" {val_name}(self, {attr_name_ref}, val)") names_for_globals[val_name] = a.validator names_for_globals[attr_name_ref] = a lines.append( From 82b79db2b42f9556d0adb8ebf05c2f78c2bddaff Mon Sep 17 00:00:00 2001 From: Mohammed Imaad Sharieff Date: Sat, 10 Jan 2026 10:37:36 +0530 Subject: [PATCH 4/5] Fix --- Dockerfile | 6 +++ Github-setup.md | 12 ++++++ PROBLEM.md | 54 +++++++++++++++++++++++++ Test.patch | Bin 0 -> 7966 bytes classification.md | 9 +++++ commit.txt | Bin 0 -> 86 bytes description.md | 26 ++++++++++++ solution.patch | Bin 0 -> 1978 bytes test.sh | 14 +++++++ tests/test_factory_validation_order.py | 54 +++++++++++++++++++++++++ 10 files changed, 175 insertions(+) create mode 100644 Dockerfile create mode 100644 Github-setup.md create mode 100644 PROBLEM.md create mode 100644 Test.patch create mode 100644 classification.md create mode 100644 commit.txt create mode 100644 description.md create mode 100644 solution.patch create mode 100644 test.sh create mode 100644 tests/test_factory_validation_order.py diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..970b469fa --- /dev/null +++ b/Dockerfile @@ -0,0 +1,6 @@ +FROM python:3.11-slim +WORKDIR /app +COPY . . +RUN pip install -e . +RUN pip install pytest +CMD ["/bin/bash"] \ No newline at end of file diff --git a/Github-setup.md b/Github-setup.md new file mode 100644 index 000000000..5bd7b38b7 --- /dev/null +++ b/Github-setup.md @@ -0,0 +1,12 @@ +Repository URL: +https://github.com/Imaad00/attrs + +Commit Hash Used: +d7f3c345d22671d71b129036783ce0ce33105956 + +Directory Touched: +src/attr/_make.py + +Files Introduced: +tests/test_factory_validation_order.py +test.sh diff --git a/PROBLEM.md b/PROBLEM.md new file mode 100644 index 000000000..d9a4f73f6 --- /dev/null +++ b/PROBLEM.md @@ -0,0 +1,54 @@ +Incorrect Default Factory Execution Order in attrs Causing Validation Errors +---------------------------------------------------------------------------- + +Brief +----- +Classes defined using `@define` in the attrs library support dynamic default +values via a `factory=` argument and value validation via validators. In +certain cases where a required field, a factory-provided field, and one or more +validators exist together, attrs initializes default values too early. This +causes validators to run on incomplete or incorrect state. + +This produces mismatches between user intent and runtime behavior — validators +may see pre-factory values, incorrectly succeed, or incorrectly fail. + +Expected Behavior +----------------- +Default factories must run only after all explicitly provided required values +have been: +1. Assigned +2. Validated individually + +Validators must always receive: +• The resolved value returned by the default factory +• A fully initialized set of attributes +• Complete cross-field state + +Required Functional Behavior +---------------------------- +1. Required fields missing → raise a validation error (unchanged behavior). +2. The default factory must execute *only if* the user does not pass a value. +3. The default factory must run *after* successful validation of required fields. +4. All validators — including cross-field validators — must observe the final + resolved default value. +5. If any validator raises, object initialization must halt without returning + a partially initialized instance. +6. Classes without factories or cross-field validators must behave identically + to current releases (no regressions). + +Edge Cases to Support +--------------------- +• Field B default depends on validated A +• Validators referencing multiple attributes +• Factories returning mutable objects (must not reuse previous instance) +• Missing required fields +• Multiple factories in one class + +Success Outcome +--------------- +After applying a fix, all validators must execute against final initialized +state, default factories must run in the correct order, and all existing behavior +must remain unchanged unless explicitly addressed in this brief. + +Everything described must be testable deterministically, with no randomness, +timers, or network interaction. diff --git a/Test.patch b/Test.patch new file mode 100644 index 0000000000000000000000000000000000000000..6e359291d9ee854f5bad5520000d6df2b84a9948 GIT binary patch literal 7966 zcmeHM>uMWE6h6>@g}lQe6G*VFNb)7NNlHqBNdrwwZ73y_MV2f@jbaH&xfI7w(Rb>b z^fl6c-yEH;_cEH%B>Xt$)0p@@5=$+TeuF< z+H`$Qxj?NY$I`$Ewja6Cmw4~UCF*M$S^=M*T6Z)S(=N4MF(RYA)!_hjEcd*rO zpwaI<2(p zF4hv6qP-&z+>9CO4`uC$aPLE2O1jFW{K8L_s0ltjXfcEY31s6sUBU8P zLt3@vBpIM>>a?0+ERiymdyww_d`tr)sWqwG9617^_@F zjUyk*N?EvO^oxBwkFmyWSeQ6E#(jymOQ#vVpaG=pn3q50NdgB*I*B01am zZOrRIenknrQ|+9>I>gRvtYdiPxjyE{d4ylb zxcR1W5rbiC*i6M>ZSGqdSFW9BX!OckA&Wpa@RMs`w+(;0z)CRA(ffLGgB4Lv2uq=u z36Il=#9bn9eeUS%MkTX^QqD9~ThyRg7T?nf{wcJj-w{`Jth`3(<<`&kkBoZ?&2)ER zq%TUbKF3WV&#bpP0*!(p@p^9K%ofC&p^JIXHEgW=Z8pa2b>p%gUCVeJnhddcwDGpu z-1mEkEfbV|c)`f!G($-Js-z9`R<3C^nwP^(41d3^GD*nQbn=&rG2@?C>?vZU*zbQ= zjI6d}`l)Xk)F*{4s9LMsGVrJG}Df?RA zL*k;R&nng)E`9mOvS$XysVWjtmoFK{<}!>}bm=(1Yaq7ZVA}GS}VN08e@IX9QWsnLnq$@BVP{q(FC<_!X$)U2coBVaO)S`m-O$ zRmj|2uC99c^Te1n-E?&K{Rhfow{f&H&T~Ysci1&FiW_drWkz9!n9n~OMxprch91ek o9#vjubMpUr_!mwkZ(+|mz&CXr-`REd&w`XjsBxdr|IkF)KcsZua{vGU literal 0 HcmV?d00001 diff --git a/classification.md b/classification.md new file mode 100644 index 000000000..5f7661702 --- /dev/null +++ b/classification.md @@ -0,0 +1,9 @@ +Language: Python +Difficulty: Hard +Category: Bug Fix + Feature Clarification +Repository: attrs (https://github.com/python-attrs/attrs) +License: MIT +Reasoning: +- Touches core initialization logic +- Requires test-driven development +- Needs correct validation ordering diff --git a/commit.txt b/commit.txt new file mode 100644 index 0000000000000000000000000000000000000000..ae4172d8516ccef530e9802955ecf742dd7c1f61 GIT binary patch literal 86 zcmW-YK@NZ*3#87sgTL)_tpm*U#PHJ^vL<$4V97HoZIT;kEyqYc6-rp(aCh&V==pr=uI zjC1;CLK|Z#yAj^PBSR_I88*DjZiUH^EXL$+LB29v>AiULaU1^8&Tr}2rDp;kAH=rg z`iWP5f-2$rSYzF>jv>)TtgXU<7;|0aJ!`8pe#Hn0zjPQ+m~F=1DQebaxzvahISQD6 z%b~PN3{O&Tw^8NE-P7`v7fdavQPKN`HT%saBeQ@sR$sx+}Sm4m{ObuJtJAZ)s!sI@IXU9gC|T z?XGRL<&;Z`4wr>($&&H!k`A)>etc?F>7w8OurYT-8dm VhqW*oS+URMFQPbSyzh#?$PW-s@00)l literal 0 HcmV?d00001 diff --git a/test.sh b/test.sh new file mode 100644 index 000000000..7784467ea --- /dev/null +++ b/test.sh @@ -0,0 +1,14 @@ +#!/bin/bash +set -e +case "$1" in + base) + pytest tests/ --ignore=tests/test_factory_validation_order.py + ;; + new) + pytest tests/test_factory_validation_order.py + ;; + *) + echo "Usage: ./test.sh {base|new}" + exit 1 + ;; +esac diff --git a/tests/test_factory_validation_order.py b/tests/test_factory_validation_order.py new file mode 100644 index 000000000..3cb4f27b5 --- /dev/null +++ b/tests/test_factory_validation_order.py @@ -0,0 +1,54 @@ +from attrs import define, field, validators +import pytest + +def test_default_factory_runs_after_required_validation(): + def make_b(): + return 10 + + @define + class Item: + a: int = field(validator=validators.gt(0)) + b: int = field(default=None, factory=make_b, + validator=validators.gt(0)) + + obj = Item(5) + assert obj.b == 10 + + +def test_cross_field_validator_sees_final_defaults(): + def cross_validate(instance, attribute, value): + # b must always be greater than a + if instance.b <= instance.a: + raise ValueError("b must be greater than a") + + @define + class Item: + a: int = field() + b: int = field(default=None, factory=lambda: 5, + validator=cross_validate) + + with pytest.raises(ValueError): + Item(5) + + # When user provides b manually, it should succeed + assert Item(5, 10).b == 10 + + +def test_missing_required_still_errors(): + @define + class Thing: + a: int = field() + b: int = field(default=None, factory=lambda: 3) + + with pytest.raises(TypeError): + Thing() + + +def test_independent_fields_unchanged(): + @define + class Simple: + x: int = field(validator=validators.gt(0)) + y: str = field(default="hi") + + obj = Simple(10) + assert obj.y == "hi" From cad240638a31bc720b03571a8df9ef73a972aee2 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 12 Jan 2026 07:09:39 +0000 Subject: [PATCH 5/5] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- Dockerfile | 2 +- Test.patch | Bin 7966 -> 7967 bytes commit.txt | Bin 86 -> 87 bytes solution.patch | Bin 1978 -> 1979 bytes tests/test_factory_validation_order.py | 14 +++++++++----- 5 files changed, 10 insertions(+), 6 deletions(-) diff --git a/Dockerfile b/Dockerfile index 970b469fa..7199182e4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,4 +3,4 @@ WORKDIR /app COPY . . RUN pip install -e . RUN pip install pytest -CMD ["/bin/bash"] \ No newline at end of file +CMD ["/bin/bash"] diff --git a/Test.patch b/Test.patch index 6e359291d9ee854f5bad5520000d6df2b84a9948..0e28fbaff401dba731409d478b0739f4c2931cff 100644 GIT binary patch delta 9 QcmbPdH{Wi9oIE2J01>4EvH$=8 delta 7 OcmbPlH_vW^oIC&wO#-I? diff --git a/commit.txt b/commit.txt index ae4172d8516ccef530e9802955ecf742dd7c1f61..f0894620d5c8f39c5c3dcebf4a1ecb79f068236d 100644 GIT binary patch delta 6 NcmWFwpAg2#1po&N0iOT> delta 4 LcmWF!n-B&71K0tS diff --git a/solution.patch b/solution.patch index 5cf7853f7ef7fb32dd4836ec5838e68f6f5d882e..7c5ecf2d7b0c3c3a051b24aa07e99476cada3512 100644 GIT binary patch delta 9 QcmdnRzng!