Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 90 additions & 0 deletions .auxiliary/notes/pickling.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# Pickling Support for Absence Sentinels

## Current Implementation

Absence sentinels currently raise `OperationValidityError` when pickling is attempted via `__reduce__`. This was a pragmatic decision to avoid the complexity of implementing singleton-preserving pickle support without adequate time to think through the implications.

## The Challenge

Absence sentinels rely on identity checks (`is` operator) for correctness. The `is_absent()` predicate uses `absent is value` to determine if a value is the global absence sentinel. Singleton semantics are critical for this identity-based checking to work reliably.

Python's `pickle` module serializes objects for storage or transmission and recreates them during unpickling. By default, unpickling creates new object instances, which breaks singleton semantics:

- `pickle.loads(pickle.dumps(absent)) is absent` would be `False`
- Multiple "copies" of sentinels would exist after unpickling
- Identity-based checks would fail unpredictably

## Options to Explore

### 1. Implement Singleton-Preserving Pickle Support

Custom `__reduce__` implementations can preserve singleton semantics during unpickling. This requires careful coordination but is achievable.

**Considerations:**
- What should the pickle representation look like?
- How do we ensure the unpickler returns the same singleton instance?
- Does this work across process boundaries (e.g., multiprocessing)?
- What happens if someone unpickles in a process without the absence package?
- How do we handle custom `AbsenceFactory` instances (each is its own singleton)?

**Possible approach:**
```python
def __reduce__(self):
# Return a callable that will return the singleton on unpickling
return (self.__class__, ())
```
But this still creates a new instance unless `__new__` returns the existing singleton.

### 2. Keep Current Non-Picklable Behavior

**Arguments for:**
- Absence sentinels represent transient "missing value" states in function calls
- Serializing absence sentinels may indicate design smell in calling code
- Failing explicitly is better than creating subtle identity bugs
- Forces users to handle absence before serialization boundaries
- Simpler implementation with fewer edge cases

**Arguments against:**
- Users cannot serialize data structures containing absence sentinels
- Requires wrapping or transformation before serialization
- May be inconvenient for some use cases (e.g., caching function results)

### 3. Allow Default Pickling (No Custom __reduce__)

**Arguments for:**
- Simpler - let Python handle it
- Inherits behavior from `falsifier.Falsifier` base class
- Less code to maintain

**Arguments against:**
- Breaks singleton semantics silently after unpickling
- Creates subtle bugs where `is` checks fail unexpectedly
- Violates principle of least surprise for sentinel objects
- Need to verify what `falsifier.Falsifier` actually does

## Questions to Answer

1. What are the actual use cases for pickling absence sentinels?
2. Can we examine how other singleton objects in Python handle pickling?
- `None` (how does it pickle?)
- `Ellipsis`
- `NotImplemented`
- `dataclasses.MISSING`
- `typing.NoDefault`
3. What does PEP 661 say about pickling sentinels?
4. How do similar libraries (e.g., `attrs`, `cattrs`) handle sentinel pickling?
5. Is there a real-world scenario where pickling an absence sentinel is the right design?

## Next Steps

- Research how Python's built-in singletons handle pickling
- Investigate PEP 661 recommendations
- Survey similar libraries for precedent
- Gather user feedback on whether pickle support is needed
- If implementing pickle support, write comprehensive tests for edge cases
- Consider whether to implement pickle support for `AbsenceFactory` instances

## References

- Current implementation: `sources/absence/objects.py:59-61` (`__reduce__` raises error)
- Exception: `sources/absence/exceptions.py:38-42` (`OperationValidityError`)
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
.. vim: set fileencoding=utf-8:
.. -*- coding: utf-8 -*-
.. +--------------------------------------------------------------------------+
| |
| Licensed under the Apache License, Version 2.0 (the "License"); |
| you may not use this file except in compliance with the License. |
| You may obtain a copy of the License at |
| |
| http://www.apache.org/licenses/LICENSE-2.0 |
| |
| Unless required by applicable law or agreed to in writing, software |
| distributed under the License is distributed on an "AS IS" BASIS, |
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| See the License for the specific language governing permissions and |
| limitations under the License. |
| |
+--------------------------------------------------------------------------+


*******************************************************************************
001. Falsey Behavior via Inheritance
*******************************************************************************

Status
===============================================================================

Accepted

Context
===============================================================================

Absence sentinels need to evaluate to ``False`` in boolean contexts to enable
natural conditional checks like ``if not is_absent(value)``. Python provides
two approaches for creating falsey custom objects:

1. Implement ``__bool__`` to return ``False``
2. Inherit from a base class that provides this behavior

The ``falsifier`` package provides a ``Falsifier`` base class specifically
designed for objects that should always evaluate to ``False``. This package is
maintained by the same author and follows the same design philosophy.

The key requirement is that absence sentinels should be falsey without requiring
each sentinel class to implement the behavior independently.

Decision
===============================================================================

Inherit from ``falsifier.Falsifier`` as the base class for ``AbsenceFactory``
rather than implementing ``__bool__`` directly.

Alternatives
===============================================================================

**Implement __bool__ directly in AbsenceFactory**

Rejected because:

- Duplicates functionality available in a focused, reusable library
- Violates DRY principle when a well-tested implementation exists
- Increases maintenance burden by duplicating logic across packages
- Misses opportunity to leverage specialized package for common pattern

**Use composition with a falsey mixin**

Rejected because:

- Adds unnecessary complexity for single-behavior inheritance
- Inheritance hierarchy is already simple and focused
- No compelling reason to prefer composition over inheritance here
- Would require additional boilerplate for the same result

**Do nothing (leave sentinels truthy)**

Rejected because:

- Contradicts the primary use case of representing absence
- Creates confusing semantics where "absent" evaluates to True
- Forces users to always use explicit ``is_absent()`` checks
- Inconsistent with Python conventions for sentinel values

Consequences
===============================================================================

**Positive:**

- Leverages well-tested, focused library for falsey behavior
- Maintains consistency with other packages in the project family
- Reduces code duplication across related packages
- Clear separation between absence semantics and falsey implementation
- Enables focus on absence-specific features rather than basic behavior

**Negative:**

- Adds external dependency on ``falsifier`` package
- Introduces small coupling between packages
- Users must install ``falsifier`` as a dependency
- Changes to ``falsifier`` API could require updates

**Neutral:**

- Establishes pattern for reusing behavior across package family
- Reinforces focus on single-purpose, composable packages
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
.. vim: set fileencoding=utf-8:
.. -*- coding: utf-8 -*-
.. +--------------------------------------------------------------------------+
| |
| Licensed under the Apache License, Version 2.0 (the "License"); |
| you may not use this file except in compliance with the License. |
| You may obtain a copy of the License at |
| |
| http://www.apache.org/licenses/LICENSE-2.0 |
| |
| Unless required by applicable law or agreed to in writing, software |
| distributed under the License is distributed on an "AS IS" BASIS, |
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| See the License for the specific language governing permissions and |
| limitations under the License. |
| |
+--------------------------------------------------------------------------+


*******************************************************************************
002. Singleton via __new__ Override
*******************************************************************************

Status
===============================================================================

Accepted

Context
===============================================================================

The global ``absent`` sentinel must be a singleton to enable identity-based
checking via the ``is`` operator. The ``is_absent()`` predicate relies on
``absent is value`` comparisons, which only work reliably if exactly one
instance exists.

Python provides several approaches for implementing singletons:

1. **Module-level instance**: Create instance at module level during import
2. **__new__ override**: Control instance creation in ``__new__`` method
3. **Metaclass**: Use custom metaclass to control class instantiation
4. **Decorator**: Apply singleton decorator to the class
5. **Borg pattern**: Share state between instances rather than identity

The chosen approach must ensure that ``AbsentSingleton()`` always returns the
same instance while maintaining clear, understandable code without excessive
complexity.

Decision
===============================================================================

Implement singleton pattern by overriding ``__new__`` to check for existing
instance in module globals and return it if present, otherwise create new
instance.

Alternatives
===============================================================================

**Module-level instance only (no enforced singleton)**

Rejected because:

- Users could accidentally create ``AbsentSingleton()`` and get different instance
- No protection against breaking identity-based checks
- Requires documentation and convention rather than enforcement
- Subtle bugs if users call constructor instead of using module constant

**Metaclass-based singleton**

Rejected because:

- Adds complexity for functionality that doesn't benefit from metaclass features
- Harder to understand for developers unfamiliar with metaclasses
- Inheritance complications if subclasses needed different behavior
- Overkill for simple singleton requirement

**Singleton decorator**

Rejected because:

- Requires external decorator implementation or dependency
- Adds layer of indirection in class definition
- Less explicit control over singleton behavior
- Decorator pattern less familiar in this context

**Borg pattern (shared state)**

Rejected because:

- Identity checks (``is``) require same object, not shared state
- More complex than needed for immutable sentinel
- Doesn't actually solve the requirement
- Philosophical mismatch with sentinel concept

Consequences
===============================================================================

**Positive:**

- Explicit, self-contained singleton implementation
- No external dependencies or metaclass complexity
- Works naturally with existing inheritance from ``AbsenceFactory``
- Clear control flow in ``__new__`` method
- Prevents accidental creation of multiple instances
- Familiar pattern to Python developers

**Negative:**

- Slightly more complex than module-level constant alone
- ``__new__`` override can be subtle for developers unfamiliar with pattern
- Checks globals on each construction attempt (minor performance cost)
- Does not prevent subclasses from breaking singleton semantics

**Neutral:**

- Standard Python idiom for enforced singletons
- Complements module-level ``absent`` constant
- Balances simplicity with enforcement
4 changes: 2 additions & 2 deletions documentation/architecture/decisions/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ Architectural Decision Records
.. toctree::
:maxdepth: 2


.. todo:: Add architectural decision records to toctree.
001-falsey-behavior-via-inheritance
002-singleton-via-new-override

For ADR format and guidance, see the `architecture documentation guide
<https://emcd.github.io/python-project-common/stable/sphinx-html/common/architecture.html>`_.
Loading