This is an automated email from the ASF dual-hosted git repository.

sbp pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tooling-trusted-release.git


The following commit(s) were added to refs/heads/main by this push:
     new c472cdc  Add tips about using outcomes in difficult cases
c472cdc is described below

commit c472cdc23dfa2d7749c397c544e00447a9a1879f
Author: Sean B. Palmer <[email protected]>
AuthorDate: Mon Jul 21 11:43:43 2025 +0100

    Add tips about using outcomes in difficult cases
---
 docs/storage-interface.html | 30 ++++++++++++++++++++++++++++--
 docs/storage-interface.md   | 39 +++++++++++++++++++++++++++++++++++++++
 2 files changed, 67 insertions(+), 2 deletions(-)

diff --git a/docs/storage-interface.html b/docs/storage-interface.html
index 9c9165d..02cacc3 100644
--- a/docs/storage-interface.html
+++ b/docs/storage-interface.html
@@ -23,7 +23,7 @@
 <p>The <code>wafm</code> (<strong>w</strong>rite <strong>a</strong>s 
<strong>f</strong>oundation <strong>m</strong>ember) object exposes 
functionality which is only available to foundation members. The 
<code>wafm.keys.ensure_stored_one</code> method is an example of such 
functionality. The <code>wacm</code> object goes further and exposes 
functionality only available to committee members.</p>
 <p>In this case we decide to raise as soon as there is any error. We could 
also choose instead to display a warning, ignore the error, etc.</p>
 <p>The first few lines in the context session show the classic three step 
approach. Here they are again with comments:</p>
-<pre><code>    # 1. Request permissions
+<pre><code class="language-python">    # 1. Request permissions
     wafm = write.as_foundation_member().writer_or_raise()
 
     # 2. Use the exposed functionality
@@ -35,7 +35,7 @@
 <h2>How do we add functionality to the storage interface?</h2>
 <p>Add all the functionality to classes in modules in the 
<code>atr/storage/writers</code> directory. Code to write public keys to 
storage, for example, goes in <code>atr/storage/writers/keys.py</code>.</p>
 <p>Classes in modules in the <code>atr/storage/writers</code> directory must 
be named as follows:</p>
-<pre><code>class FoundationParticipant:
+<pre><code class="language-python">class FoundationParticipant:
     ...
 
 class FoundationMember(FoundationParticipant):
@@ -55,6 +55,32 @@ class CommitteeMember(CommitteeParticipant):
 <p>How do we represent this?</p>
 <p>Outcomes are one possibility. For each key we can return 
<code>OutcomeResult</code> for a success, and <code>OutcomeException</code> 
when there was a Python error. The caller can then decide what to do with this 
information. It might ignore the exception, raise it, or print an error message 
to the user. Better yet, we can aggregate these into an <code>Outcomes</code> 
list, which provides many useful methods for processing all of the outcomes 
together. It can count how many exceptions  [...]
 <p>We do not have to return outcomes from public storage interface methods, 
but these classes were designed to make the storage interface easy to use.</p>
+<h3>Outcome design patterns</h3>
+<p>One common pattern when designing outcome types is about how to handle an 
<strong>exception after a success</strong>, and how to handle a <strong>warning 
during success</strong>:</p>
+<ul>
+<li>An <strong>exception after a success</strong> is when an object is 
processed in multiple stages, and the first few stages succeed but then 
subsequently there is an exception.</li>
+<li>A <strong>warning during success</strong> is when an object is processed 
in multiple stages, an exception is raised, but we determine that we can 
proceed to subsequent stages as long as we keep a note of the exception.</li>
+</ul>
+<p>Both of these workflows appear incompatible with outcomes. In outcomes, we 
can record <em>either</em> a successful result, <em>or</em> an exception. But 
in exception after success we want to record the successes up to the exception; 
and in a warning during a success we want to record the exception even though 
we return a success result.</p>
+<p>The solution is similar in both cases: create a wrapper of the <em>primary 
type</em> which can hold an instance of the <em>secondary type</em>.</p>
+<p>In <em>exception after a success</em> the primary type is an exception, and 
the secondary type is the result which was obtained up to that exception. The 
type will look like this:</p>
+<pre><code class="language-python">class AfterSuccessError(Exception):
+    def __init__(self, result_before_error: Result):
+        self.result_before_error = result_before_error
+</code></pre>
+<p>In <em>warning during success</em>, the primary type is the result, and the 
secondary type is the exception raised during successful processing which we 
consider a warning. This is the inverse of the above, and the types are 
therefore inverted too.</p>
+<pre><code class="language-python">@dataclasses.dataclass
+class Result:
+    value: Value
+    warning: Exception | None
+</code></pre>
+<p>This could just as easily be a Pydantic class or whatever is appropriate in 
the situation, as long as it can hold the warning. If the warning is generated 
during an additional or side task, we can use <code>Outcome[SideValue]</code> 
instead. We do this, for example, in the type representing a linked 
committee:</p>
+<pre><code class="language-python">@dataclasses.dataclass
+class LinkedCommittee:
+    name: str
+    autogenerated_keys_file: Outcome[str]
+</code></pre>
+<p>In this case, if the autogenerated keys file call succeeded without an 
error, the <code>Outcome</code> will be an <code>OutcomeResult[str]</code> 
where the <code>str</code> represents the full path to the autogenerated 
file.</p>
 <h2>What makes this safe?</h2>
 <p>We can always open a database session or write to the filesystem, so there 
is no way to make storage access truly safe. But abstracting these operations 
to a well known interface makes it more likely that we use only this way of 
doing things, which we can then concentrate on getting right. This is in 
contrast to writing storage access in <em>ad hoc</em> ways, some of which may 
be correct and some of which may not.</p>
 <p>Code relative to a permissions level is only ever exposed in the storage 
interface when it is proven, at the type level and during runtime, that the 
user has credentials for those permissions. Helper code remains private due to 
the use of <code>__private_methods</code>, which undergo name mangling in 
Python. As mentioned in the introduction, the storage interface is also the 
suitable place to add audit logging, currently planned and not yet 
implemented.</p>
diff --git a/docs/storage-interface.md b/docs/storage-interface.md
index a12deae..af6b9e8 100644
--- a/docs/storage-interface.md
+++ b/docs/storage-interface.md
@@ -79,6 +79,45 @@ Outcomes are one possibility. For each key we can return 
`OutcomeResult` for a s
 
 We do not have to return outcomes from public storage interface methods, but 
these classes were designed to make the storage interface easy to use.
 
+### Outcome design patterns
+
+One common pattern when designing outcome types is about how to handle an 
**exception after a success**, and how to handle a **warning during success**:
+
+* An **exception after a success** is when an object is processed in multiple 
stages, and the first few stages succeed but then subsequently there is an 
exception.
+* A **warning during success** is when an object is processed in multiple 
stages, an exception is raised, but we determine that we can proceed to 
subsequent stages as long as we keep a note of the exception.
+
+Both of these workflows appear incompatible with outcomes. In outcomes, we can 
record _either_ a successful result, _or_ an exception. But in exception after 
success we want to record the successes up to the exception; and in a warning 
during a success we want to record the exception even though we return a 
success result.
+
+The solution is similar in both cases: create a wrapper of the _primary type_ 
which can hold an instance of the _secondary type_.
+
+In _exception after a success_ the primary type is an exception, and the 
secondary type is the result which was obtained up to that exception. The type 
will look like this:
+
+```python
+class AfterSuccessError(Exception):
+    def __init__(self, result_before_error: Result):
+        self.result_before_error = result_before_error
+```
+
+In _warning during success_, the primary type is the result, and the secondary 
type is the exception raised during successful processing which we consider a 
warning. This is the inverse of the above, and the types are therefore inverted 
too.
+
+```python
[email protected]
+class Result:
+    value: Value
+    warning: Exception | None
+```
+
+This could just as easily be a Pydantic class or whatever is appropriate in 
the situation, as long as it can hold the warning. If the warning is generated 
during an additional or side task, we can use `Outcome[SideValue]` instead. We 
do this, for example, in the type representing a linked committee:
+
+```python
[email protected]
+class LinkedCommittee:
+    name: str
+    autogenerated_keys_file: Outcome[str]
+```
+
+In this case, if the autogenerated keys file call succeeded without an error, 
the `Outcome` will be an `OutcomeResult[str]` where the `str` represents the 
full path to the autogenerated file.
+
 ## What makes this safe?
 
 We can always open a database session or write to the filesystem, so there is 
no way to make storage access truly safe. But abstracting these operations to a 
well known interface makes it more likely that we use only this way of doing 
things, which we can then concentrate on getting right. This is in contrast to 
writing storage access in _ad hoc_ ways, some of which may be correct and some 
of which may not.


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to