nondet 0.0.1__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.
nondet-0.0.1/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Seth Wheeler
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
nondet-0.0.1/PKG-INFO ADDED
@@ -0,0 +1,194 @@
1
+ Metadata-Version: 2.4
2
+ Name: nondet
3
+ Version: 0.0.1
4
+ Summary: Run a function in fresh processes and see if it answers the same. A witness is a fact; agreement is not.
5
+ License: MIT
6
+ Keywords: determinism,nondeterminism,reproducibility,purity,side-effects,hash-randomisation,pythonhashseed,memoize,caching,flaky,testing,zero-dependency
7
+ Classifier: Development Status :: 3 - Alpha
8
+ Classifier: Intended Audience :: Developers
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Topic :: Software Development :: Testing
12
+ Requires-Python: >=3.9
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+ Dynamic: license-file
16
+
17
+ # `nondet`
18
+
19
+ Run a function more than once, **in fresh processes**, and see if it answers the same.
20
+
21
+ ```sh
22
+ nondet src/ # every module-level function under a tree
23
+ nondet src/util.py::normalise # one function
24
+ ```
25
+
26
+ ```
27
+ FINDINGS — 1, each with a witness:
28
+ nondeterministic src/features.py::resolve_features
29
+ [["alpha","beta","gamma","delta","epsilon"]] -> V:set["epsilon","delta",…] then V:set["beta","gamma",…]
30
+ a witness is a fact: this function gave two answers to one input
31
+ ```
32
+
33
+ ## Why fresh processes, which is the whole design
34
+
35
+ Calling a function twice in one interpreter is the check anybody writes first, and it is
36
+ blind to the commonest source of nondeterminism in Python:
37
+
38
+ ```
39
+ $ for i in 1 2 3; do python3 -c "print(list({'alpha','beta','gamma'}))"; done
40
+ ['gamma', 'alpha', 'beta']
41
+ ['beta', 'gamma', 'alpha']
42
+ ['alpha', 'gamma', 'beta']
43
+
44
+ $ python3 -c "
45
+ > for i in range(3): print(list({'alpha','beta','gamma'}))"
46
+ ['beta', 'alpha', 'gamma']
47
+ ['beta', 'alpha', 'gamma'] # <- stable, twenty times out of twenty
48
+ ['beta', 'alpha', 'gamma']
49
+ ```
50
+
51
+ String hashing is randomised per interpreter, so set and dict iteration order is stable
52
+ **within** a process and different in every new one. An in-process repeat check reports
53
+ that function deterministic every time it is asked, and the build that depends on it
54
+ breaks on a machine that started the process differently.
55
+
56
+ That control is a test: `test_in_process_repetition_would_have_missed_it` asserts the
57
+ in-process check finds *no* variation over 20 calls and that `nondet` finds it anyway.
58
+ If in-process repetition ever catches it, fresh processes are expensive theatre and the
59
+ test says so.
60
+
61
+ ## The verdicts are asymmetric
62
+
63
+ | verdict | means | worth |
64
+ |---|---|---|
65
+ | `nondeterministic` | an input, and two different answers to it | **a witness, and a witness is a fact** |
66
+ | `deterministic` | no run disagreed, across N runs over a finite ladder | the absence of a counterexample, which is not proof of its absence |
67
+ | `look` | could not be probed — and why | never a finding, never fails the run |
68
+
69
+ The `deterministic` line says so in the output rather than letting you read it as a
70
+ guarantee.
71
+
72
+ ## What it is measured at
73
+
74
+ **A labelled fixture set** (`fixtures/known.py`, 18 functions, labels written down
75
+ separately from the names so the checker is not graded against its own convention):
76
+
77
+ | | |
78
+ |---|---|
79
+ | nondeterministic caught | **9 of 9** |
80
+ | deterministic falsely flagged | **0 of 9** |
81
+
82
+ The pairs are the point. `dedup_unsorted` and `dedup_sorted` are one `sorted()` apart.
83
+ `seeded` uses `random.Random(42)` — deterministic, and the specific false positive a
84
+ static gate that greps for `random` produces. `duration_arithmetic` imports `time` and
85
+ never reads the clock.
86
+
87
+ **A real tree** — `trainingResearch/tools`, 283 functions, not written with this tool in
88
+ mind:
89
+
90
+ | | with the safety gate | `--unsafe` |
91
+ |---|---|---|
92
+ | probed | 127 (45%) | 169 (60%) |
93
+ | nondeterministic | **2** | **4** |
94
+ | not probed | 156 | 114 |
95
+
96
+ Both findings are genuine: a function returning a `set`, and one whose value differs
97
+ across runs. The gate costs recall and the table says so — one of the two findings it
98
+ hides (`harness_backlog`, which returns a path under a fresh temp directory) is a true
99
+ positive that the gate can no longer see.
100
+
101
+ ## It executes the code it is asked about
102
+
103
+ This is the sharpest thing to know, and it was found the hard way. Pointed at a real
104
+ tree, `nondet` reported a function raising `TypeError` on one run and `FileExistsError`
105
+ on the next — a true finding, and proof that the probe had **created a file on
106
+ somebody's disk**.
107
+
108
+ So there are two gates, asking different questions:
109
+
110
+ - **is it safe to run?** — static, conservative, and refusing costs a `look`
111
+ - **is it deterministic?** — dynamic, and the point of the package
112
+
113
+ The line is drawn at **writing and communicating**, not at impurity. `time`, `random`,
114
+ `uuid`, `os.getpid` and set ordering are read-only and nondeterministic — they are
115
+ exactly the target, and a gate that refused them would refuse the findings. What gets
116
+ refused is anything that could change the world outside the process: `open()`,
117
+ `subprocess`, `shutil`, sockets, `os.remove` and friends.
118
+
119
+ `--unsafe` lifts it. A test asserts the gate does not eat the read-only findings, and
120
+ another asserts a file-writing function is refused *and the file does not appear*.
121
+
122
+ ## What it is for
123
+
124
+ - **What is safe to memoize or cache.** A function that answers differently per process
125
+ is not.
126
+ - **Finding hidden global state**, module-level caches, and accidental clock or
127
+ environment dependence.
128
+ - **Reproducible builds and artefacts.** A generator returning a set is how a build
129
+ output changes between machines with no source change.
130
+ - **Pre-filtering** candidates for snapshot testing or property testing.
131
+
132
+ ## Prior art
133
+
134
+ Swept on mechanism nouns across **both registries** — the first sweep queried npm hard
135
+ and PyPI only by guessing names, which is how it nearly missed the entry below.
136
+
137
+ On npm, `keywords:purity` returns 26 packages and **every one is static analysis**
138
+ (`pure-react-check`, `@efct/efct`, `@tslite/analysis`, `@ogaga/spacta`);
139
+ `keywords:nondeterminism` returns one replay recorder.
140
+
141
+ On PyPI and in the literature, three neighbours are real and none of them is this:
142
+
143
+ - **[`reprotest`](https://pypi.org/project/reprotest/)** asks the same question at
144
+ **build** granularity: run it twice under deliberately varied conditions and diff the
145
+ output. It is the direct ancestor of this package's `VARIATIONS`, which were added
146
+ after reading it. It cannot tell you *which function* moved.
147
+ - **Groce & Holmes, [*Practical Automatic Lightweight Nondeterminism and Flaky Test
148
+ Detection and Debugging for Python*](https://agroce.github.io/qrs20-2.pdf)** (QRS 2020)
149
+ is the academic prior art, at **test** granularity.
150
+ - **`pytest-flakefinder`**, **`pytest-randomly`** and **`flaky`** rerun or reorder
151
+ *tests*. `pytest-randomly` surfaces nondeterminism by controlling seeds; none of them
152
+ names a function or produces a witness input.
153
+
154
+ The gap is the granularity: nothing found points at an arbitrary function, walks a
155
+ ladder, and hands back the input that distinguished two runs.
156
+
157
+ Static and dynamic are complements rather than rivals, and this ships both: the static
158
+ gate decides what is *safe to execute*, the dynamic check decides what is *actually
159
+ nondeterministic*. A static gate alone refuses `seeded` and `duration_arithmetic`; a
160
+ dynamic check alone writes to your disk.
161
+
162
+ ## Limits
163
+
164
+ - **The ladder is fixed and small.** Arity 1–3, no variadics, no keyword-only. On a real
165
+ tree that is most of the 156 `look`s, and the census names every one.
166
+ - **Values with no canonical form are excluded, not reported.** `<Thing object at
167
+ 0x10f3c2e50>` differs every run and means nothing; flagging it would flag every
168
+ codebase in the world. The count of excluded rungs is printed.
169
+ - **Exception type, not message.** Messages carry paths, addresses and timings.
170
+ - **`PYTHONHASHSEED` is cleared for the workers**, so a fixed seed in your environment
171
+ cannot blind the check — and the fact that it was set is reported either way.
172
+ - **The environment is varied between runs** — timezone and locale, borrowed from
173
+ `reprotest`. Hash randomisation is free with every new process; these are not. So
174
+ `deterministic` here means *"the same answer under these varied conditions"*, which is
175
+ a stronger claim than three runs on one machine. `epoch_year` in the fixtures is the
176
+ control: `fromtimestamp(0).year` is 1970 in UTC and 1969 west of it, does not move
177
+ with the clock, and is caught **only** because the timezone varies.
178
+ - Zero dependencies, Python 3.9+.
179
+
180
+ ## Tests
181
+
182
+ ```sh
183
+ python3 -m unittest discover -s tests
184
+ ```
185
+
186
+ 25 tests. Two of them are regressions for bugs *in this tool* that first looked like
187
+ findings about the code under test: loading a package module by file path broke relative
188
+ imports and refused 56 of 68 functions, and sending the result vector over stdout meant
189
+ any function that printed corrupted it. Both were caught by pointing the tool at a real
190
+ codebase and disbelieving the refusal rate.
191
+
192
+ One of those fixes is worth its own note: it took reach from 2/68 to 60/68 **while
193
+ breaking correctness on all 17 fixtures**. A tool watched only by "how many did it
194
+ probe" would have scored that as an improvement.
nondet-0.0.1/README.md ADDED
@@ -0,0 +1,178 @@
1
+ # `nondet`
2
+
3
+ Run a function more than once, **in fresh processes**, and see if it answers the same.
4
+
5
+ ```sh
6
+ nondet src/ # every module-level function under a tree
7
+ nondet src/util.py::normalise # one function
8
+ ```
9
+
10
+ ```
11
+ FINDINGS — 1, each with a witness:
12
+ nondeterministic src/features.py::resolve_features
13
+ [["alpha","beta","gamma","delta","epsilon"]] -> V:set["epsilon","delta",…] then V:set["beta","gamma",…]
14
+ a witness is a fact: this function gave two answers to one input
15
+ ```
16
+
17
+ ## Why fresh processes, which is the whole design
18
+
19
+ Calling a function twice in one interpreter is the check anybody writes first, and it is
20
+ blind to the commonest source of nondeterminism in Python:
21
+
22
+ ```
23
+ $ for i in 1 2 3; do python3 -c "print(list({'alpha','beta','gamma'}))"; done
24
+ ['gamma', 'alpha', 'beta']
25
+ ['beta', 'gamma', 'alpha']
26
+ ['alpha', 'gamma', 'beta']
27
+
28
+ $ python3 -c "
29
+ > for i in range(3): print(list({'alpha','beta','gamma'}))"
30
+ ['beta', 'alpha', 'gamma']
31
+ ['beta', 'alpha', 'gamma'] # <- stable, twenty times out of twenty
32
+ ['beta', 'alpha', 'gamma']
33
+ ```
34
+
35
+ String hashing is randomised per interpreter, so set and dict iteration order is stable
36
+ **within** a process and different in every new one. An in-process repeat check reports
37
+ that function deterministic every time it is asked, and the build that depends on it
38
+ breaks on a machine that started the process differently.
39
+
40
+ That control is a test: `test_in_process_repetition_would_have_missed_it` asserts the
41
+ in-process check finds *no* variation over 20 calls and that `nondet` finds it anyway.
42
+ If in-process repetition ever catches it, fresh processes are expensive theatre and the
43
+ test says so.
44
+
45
+ ## The verdicts are asymmetric
46
+
47
+ | verdict | means | worth |
48
+ |---|---|---|
49
+ | `nondeterministic` | an input, and two different answers to it | **a witness, and a witness is a fact** |
50
+ | `deterministic` | no run disagreed, across N runs over a finite ladder | the absence of a counterexample, which is not proof of its absence |
51
+ | `look` | could not be probed — and why | never a finding, never fails the run |
52
+
53
+ The `deterministic` line says so in the output rather than letting you read it as a
54
+ guarantee.
55
+
56
+ ## What it is measured at
57
+
58
+ **A labelled fixture set** (`fixtures/known.py`, 18 functions, labels written down
59
+ separately from the names so the checker is not graded against its own convention):
60
+
61
+ | | |
62
+ |---|---|
63
+ | nondeterministic caught | **9 of 9** |
64
+ | deterministic falsely flagged | **0 of 9** |
65
+
66
+ The pairs are the point. `dedup_unsorted` and `dedup_sorted` are one `sorted()` apart.
67
+ `seeded` uses `random.Random(42)` — deterministic, and the specific false positive a
68
+ static gate that greps for `random` produces. `duration_arithmetic` imports `time` and
69
+ never reads the clock.
70
+
71
+ **A real tree** — `trainingResearch/tools`, 283 functions, not written with this tool in
72
+ mind:
73
+
74
+ | | with the safety gate | `--unsafe` |
75
+ |---|---|---|
76
+ | probed | 127 (45%) | 169 (60%) |
77
+ | nondeterministic | **2** | **4** |
78
+ | not probed | 156 | 114 |
79
+
80
+ Both findings are genuine: a function returning a `set`, and one whose value differs
81
+ across runs. The gate costs recall and the table says so — one of the two findings it
82
+ hides (`harness_backlog`, which returns a path under a fresh temp directory) is a true
83
+ positive that the gate can no longer see.
84
+
85
+ ## It executes the code it is asked about
86
+
87
+ This is the sharpest thing to know, and it was found the hard way. Pointed at a real
88
+ tree, `nondet` reported a function raising `TypeError` on one run and `FileExistsError`
89
+ on the next — a true finding, and proof that the probe had **created a file on
90
+ somebody's disk**.
91
+
92
+ So there are two gates, asking different questions:
93
+
94
+ - **is it safe to run?** — static, conservative, and refusing costs a `look`
95
+ - **is it deterministic?** — dynamic, and the point of the package
96
+
97
+ The line is drawn at **writing and communicating**, not at impurity. `time`, `random`,
98
+ `uuid`, `os.getpid` and set ordering are read-only and nondeterministic — they are
99
+ exactly the target, and a gate that refused them would refuse the findings. What gets
100
+ refused is anything that could change the world outside the process: `open()`,
101
+ `subprocess`, `shutil`, sockets, `os.remove` and friends.
102
+
103
+ `--unsafe` lifts it. A test asserts the gate does not eat the read-only findings, and
104
+ another asserts a file-writing function is refused *and the file does not appear*.
105
+
106
+ ## What it is for
107
+
108
+ - **What is safe to memoize or cache.** A function that answers differently per process
109
+ is not.
110
+ - **Finding hidden global state**, module-level caches, and accidental clock or
111
+ environment dependence.
112
+ - **Reproducible builds and artefacts.** A generator returning a set is how a build
113
+ output changes between machines with no source change.
114
+ - **Pre-filtering** candidates for snapshot testing or property testing.
115
+
116
+ ## Prior art
117
+
118
+ Swept on mechanism nouns across **both registries** — the first sweep queried npm hard
119
+ and PyPI only by guessing names, which is how it nearly missed the entry below.
120
+
121
+ On npm, `keywords:purity` returns 26 packages and **every one is static analysis**
122
+ (`pure-react-check`, `@efct/efct`, `@tslite/analysis`, `@ogaga/spacta`);
123
+ `keywords:nondeterminism` returns one replay recorder.
124
+
125
+ On PyPI and in the literature, three neighbours are real and none of them is this:
126
+
127
+ - **[`reprotest`](https://pypi.org/project/reprotest/)** asks the same question at
128
+ **build** granularity: run it twice under deliberately varied conditions and diff the
129
+ output. It is the direct ancestor of this package's `VARIATIONS`, which were added
130
+ after reading it. It cannot tell you *which function* moved.
131
+ - **Groce & Holmes, [*Practical Automatic Lightweight Nondeterminism and Flaky Test
132
+ Detection and Debugging for Python*](https://agroce.github.io/qrs20-2.pdf)** (QRS 2020)
133
+ is the academic prior art, at **test** granularity.
134
+ - **`pytest-flakefinder`**, **`pytest-randomly`** and **`flaky`** rerun or reorder
135
+ *tests*. `pytest-randomly` surfaces nondeterminism by controlling seeds; none of them
136
+ names a function or produces a witness input.
137
+
138
+ The gap is the granularity: nothing found points at an arbitrary function, walks a
139
+ ladder, and hands back the input that distinguished two runs.
140
+
141
+ Static and dynamic are complements rather than rivals, and this ships both: the static
142
+ gate decides what is *safe to execute*, the dynamic check decides what is *actually
143
+ nondeterministic*. A static gate alone refuses `seeded` and `duration_arithmetic`; a
144
+ dynamic check alone writes to your disk.
145
+
146
+ ## Limits
147
+
148
+ - **The ladder is fixed and small.** Arity 1–3, no variadics, no keyword-only. On a real
149
+ tree that is most of the 156 `look`s, and the census names every one.
150
+ - **Values with no canonical form are excluded, not reported.** `<Thing object at
151
+ 0x10f3c2e50>` differs every run and means nothing; flagging it would flag every
152
+ codebase in the world. The count of excluded rungs is printed.
153
+ - **Exception type, not message.** Messages carry paths, addresses and timings.
154
+ - **`PYTHONHASHSEED` is cleared for the workers**, so a fixed seed in your environment
155
+ cannot blind the check — and the fact that it was set is reported either way.
156
+ - **The environment is varied between runs** — timezone and locale, borrowed from
157
+ `reprotest`. Hash randomisation is free with every new process; these are not. So
158
+ `deterministic` here means *"the same answer under these varied conditions"*, which is
159
+ a stronger claim than three runs on one machine. `epoch_year` in the fixtures is the
160
+ control: `fromtimestamp(0).year` is 1970 in UTC and 1969 west of it, does not move
161
+ with the clock, and is caught **only** because the timezone varies.
162
+ - Zero dependencies, Python 3.9+.
163
+
164
+ ## Tests
165
+
166
+ ```sh
167
+ python3 -m unittest discover -s tests
168
+ ```
169
+
170
+ 25 tests. Two of them are regressions for bugs *in this tool* that first looked like
171
+ findings about the code under test: loading a package module by file path broke relative
172
+ imports and refused 56 of 68 functions, and sending the result vector over stdout meant
173
+ any function that printed corrupted it. Both were caught by pointing the tool at a real
174
+ codebase and disbelieving the refusal rate.
175
+
176
+ One of those fixes is worth its own note: it took reach from 2/68 to 60/68 **while
177
+ breaking correctness on all 17 fixtures**. A tool watched only by "how many did it
178
+ probe" would have scored that as an improvement.
@@ -0,0 +1,24 @@
1
+ """nondet — run it twice in fresh processes and see if it answers the same.
2
+
3
+ from nondet import check, scan
4
+
5
+ `nondeterministic` carries a witness and is a fact. `deterministic` is the absence of
6
+ one across a finite number of runs over a finite ladder, and is worth exactly what that
7
+ is worth. The two are never printed as though they were the same kind of claim.
8
+ """
9
+
10
+ from .core import (
11
+ Census,
12
+ Verdict,
13
+ check,
14
+ functions_in,
15
+ ladder,
16
+ scan,
17
+ LADDER_VALUES,
18
+ MAX_ARITY,
19
+ RUNS,
20
+ )
21
+
22
+ __all__ = ["check", "scan", "Verdict", "Census", "functions_in", "ladder",
23
+ "LADDER_VALUES", "MAX_ARITY", "RUNS"]
24
+ __version__ = "0.0.1"
@@ -0,0 +1,112 @@
1
+ """`nondet` — run it twice in fresh processes and see if it answers the same.
2
+
3
+ nondet src/ # every module-level function under a tree
4
+ nondet src/util.py::normalise # one function
5
+ nondet src/ --runs 5 --json
6
+
7
+ Exit codes: 0 nothing found · 1 at least one function is nondeterministic · 2 the tool
8
+ could not run.
9
+
10
+ A `look` NEVER fails the run. A check that cries wolf is one nobody runs, and a function
11
+ this could not probe is not a finding about that function — it is a gap in the probe,
12
+ and it is counted and named rather than hidden.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import argparse
18
+ import json
19
+ import os
20
+ import sys
21
+
22
+ from .core import RUNS, check, functions_in, scan
23
+
24
+
25
+ def _one(ref: str, runs: int, unsafe: bool = False):
26
+ path, _, name = ref.partition("::")
27
+ for candidate, arity in functions_in(path):
28
+ if candidate == name:
29
+ return check(path, name, arity, runs=runs, unsafe=unsafe)
30
+ return None
31
+
32
+
33
+ def main(argv=None) -> int:
34
+ parser = argparse.ArgumentParser(
35
+ prog="nondet",
36
+ description="Run a function in fresh processes and see if it answers the same.",
37
+ )
38
+ parser.add_argument("paths", nargs="+", help="files, directories, or FILE::NAME")
39
+ parser.add_argument("--runs", type=int, default=RUNS,
40
+ help=f"fresh interpreters per function (default {RUNS})")
41
+ parser.add_argument("--json", action="store_true", dest="as_json")
42
+ parser.add_argument("--unsafe", action="store_true",
43
+ help="run functions the static gate refuses as side-effecting. "
44
+ "This tool EXECUTES what it probes; the gate is why it does "
45
+ "not leave files behind on a working tree.")
46
+ parser.add_argument("-q", "--quiet", action="store_true",
47
+ help="only print findings and the census")
48
+ args = parser.parse_args(argv)
49
+
50
+ verdicts = []
51
+ for ref in args.paths:
52
+ if "::" in ref:
53
+ verdict = _one(ref, args.runs, args.unsafe)
54
+ if verdict is None:
55
+ sys.stderr.write(f"nondet: {ref} names no module-level function\n")
56
+ return 2
57
+ verdicts.append(verdict)
58
+ else:
59
+ if not os.path.exists(ref):
60
+ sys.stderr.write(f"nondet: {ref} does not exist\n")
61
+ return 2
62
+ verdicts.extend(scan([ref], runs=args.runs, unsafe=args.unsafe).verdicts)
63
+
64
+ findings = [v for v in verdicts if v.state == "nondeterministic"]
65
+ looks = [v for v in verdicts if v.state == "look"]
66
+ clean = [v for v in verdicts if v.state == "deterministic"]
67
+
68
+ if args.as_json:
69
+ json.dump(
70
+ {"functions": len(verdicts),
71
+ "nondeterministic": len(findings),
72
+ "deterministic": len(clean),
73
+ "look": len(looks),
74
+ "verdicts": [
75
+ {"ref": v.ref, "state": v.state, "detail": v.detail,
76
+ "witness": v.witness, "compared": v.compared, "total": v.total,
77
+ "unstateable": v.unstateable, "hash_seed_fixed": v.hash_seed_fixed}
78
+ for v in verdicts
79
+ ]},
80
+ sys.stdout, indent=2, sort_keys=True,
81
+ )
82
+ sys.stdout.write("\n")
83
+ return 1 if findings else 0
84
+
85
+ if findings:
86
+ print(f"\nFINDINGS — {len(findings)}, each with a witness:")
87
+ for v in findings:
88
+ print(" " + str(v))
89
+ if looks and not args.quiet:
90
+ print(f"\nLOOK — {len(looks)} the probe could not settle. These never fail the run.")
91
+ for v in looks:
92
+ print(" " + str(v))
93
+
94
+ # THE DENOMINATOR IS PRINTED, ALWAYS. A run that probed nothing and a run that
95
+ # probed everything and found nothing print the same word otherwise, and they are
96
+ # not the same result.
97
+ print(f"\n{len(verdicts)} functions: {len(clean)} deterministic, "
98
+ f"{len(findings)} nondeterministic, {len(looks)} not probed")
99
+ if not verdicts:
100
+ print(" nothing was probed, so this is not a clean result — it is no result")
101
+ unstateable = sum(v.unstateable for v in verdicts)
102
+ if unstateable:
103
+ print(f" {unstateable} rung(s) held values with no canonical form and were "
104
+ f"excluded from every comparison")
105
+ if any(v.hash_seed_fixed for v in verdicts):
106
+ print(" PYTHONHASHSEED was set in this environment; it was cleared for the "
107
+ "workers so hash-order\n nondeterminism could still be seen")
108
+ return 1 if findings else 0
109
+
110
+
111
+ if __name__ == "__main__": # pragma: no cover
112
+ raise SystemExit(main())