Skip to content

Commit 0883844

Browse files
emcdclaude
andcommitted
Copier Template: Enhance foundation injection with immutable exceptions and common dependencies.
Foundation injection now includes dependencies (frigid, absence, dynadoc) and generates immutable exceptions subclassing frigid's Omniexception. CLI support now depends on foundations since emcd-appcore requires them. Changes: - Add frigid, absence, dynadoc dependencies when inject_foundations enabled - Update exceptions.py to inherit from __.immut.Omniexception - Add foundational imports to __/imports.py (immut, ddoc, Absential) - Reorder copier.yaml questions for logical flow - Make enable_cli conditional on inject_foundations Foundation packages (classcore, frigid, absence, accretive) should disable inject_foundations to avoid circular dependencies. Includes implementation plan document detailing rationale, tasks, and migration strategy. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent e5b6345 commit 0883844

File tree

6 files changed

+289
-16
lines changed

6 files changed

+289
-16
lines changed
Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
# Foundation Injection Enhancement Plan
2+
3+
**Date**: 2025-11-04
4+
**Context**: Enhancing foundation injection to use immutable exceptions and include common dependencies
5+
6+
## Background
7+
8+
We want to update the injected `Omniexception` base class to inherit from `__.immut.exceptions.Omniexception` (provided by the `frigid` package) to enforce class and instance immutability. However, this creates a bootstrapping problem for foundation packages like `frigid`, `absence`, and `classcore` that cannot depend on themselves or their dependents.
9+
10+
### Key Insight
11+
12+
Merge conflicts with injected exceptions have not been a problem in practice. Most customization of `exceptions.py` is **additive** (adding project-specific exception subclasses), which doesn't conflict with template updates to the base `Omniexception`/`Omnierror` classes.
13+
14+
### Current State
15+
16+
- `inject_foundations`: Boolean flag that gates `inject_exceptions`
17+
- `inject_exceptions`: When enabled, creates `exceptions.py` with plain `Omniexception` and `Omnierror` classes
18+
- Currently NOT used in any template files directly, only in `copier.yaml` as a conditional
19+
20+
## Proposed Approach
21+
22+
### 1. Update Foundation Injection Behavior
23+
24+
When `inject_foundations: true`:
25+
- **Add default dependencies**: `frigid`, `absence`, `dynadoc`
26+
- **Inject immutable exceptions**: Subclass `Omniexception` from `__.immut.exceptions.Omniexception`
27+
28+
### 2. Disable Foundation Injection for Foundation Packages
29+
30+
Foundation packages that cannot have these dependencies:
31+
- `classcore` - Provides base object system (already has `inject_foundations: false`)
32+
- `frigid` - Provides immutability (currently has `inject_foundations: true` - needs update)
33+
- `absence` - Provides absence sentinel (needs verification and possible update)
34+
- `accretive` - Sister package to frigid (needs verification and possible update)
35+
36+
### 3. Rationale for This Approach
37+
38+
**Why this is pragmatic**:
39+
- Only 3-4 packages need special treatment (foundation layer)
40+
- The vast majority of projects are application-level and benefit from immutable exceptions
41+
- We already manually add `frigid`, `absence`, and `dynadoc` to most projects—automating reduces toil
42+
- Merge conflicts have not been a problem in practice
43+
- Simple implementation with clear mental model
44+
45+
**When this works well**:
46+
- ✅ Most projects (>80%) are application-level and use these dependencies
47+
- ✅ Base exception classes in template rarely change (merge conflicts rare)
48+
- ✅ New foundation packages created infrequently
49+
- ✅ Template primarily for our ecosystem
50+
51+
## Implementation Tasks
52+
53+
### Task 1: Update copier.yaml
54+
55+
#### Update inject_foundations
56+
57+
```yaml
58+
inject_foundations:
59+
type: bool
60+
help: |
61+
Include foundational constructs (base exceptions using immutable patterns).
62+
63+
Dependencies added: frigid, absence, dynadoc
64+
65+
Set to FALSE for foundation packages that cannot depend on these:
66+
- classcore (provides base object system)
67+
- frigid (provides immutability)
68+
- absence (provides absence sentinel)
69+
- accretive (sister package to frigid)
70+
default: false
71+
```
72+
73+
#### Update inject_exceptions (keep as-is)
74+
75+
```yaml
76+
inject_exceptions:
77+
type: bool
78+
help: 'Include base exceptions for package?'
79+
when: "{{ inject_foundations }}"
80+
default: false
81+
```
82+
83+
### Task 2: Update pyproject.toml.jinja
84+
85+
Add conditional dependencies when `inject_foundations: true`:
86+
87+
```toml
88+
dependencies = [
89+
"python >= 3.10",
90+
{% if inject_foundations %}
91+
"absence >= 0.3", # Absence sentinel
92+
"dynadoc >= 0.3", # Dynamic documentation
93+
"frigid >= 0.15", # Immutable data structures
94+
{% endif %}
95+
# ... other dependencies
96+
]
97+
```
98+
99+
**Note**: Verify current minimum versions for these packages.
100+
101+
### Task 3: Update sources/{{ package_name }}/__/imports.py.jinja
102+
103+
Ensure frigid is aliased to `immut` when foundations are injected:
104+
105+
```python
106+
{% if inject_foundations %}
107+
import frigid as immut
108+
{% endif %}
109+
```
110+
111+
**Note**: Verify current import structure and integrate appropriately with existing import cascade pattern.
112+
113+
### Task 4: Update exceptions.py.jinja
114+
115+
Update to inherit from immutable base:
116+
117+
```python
118+
# template/sources/{{ package_name }}/{% if inject_exceptions %}exceptions.py{% endif %}.jinja
119+
120+
''' Family of exceptions for package API. '''
121+
122+
from . import __
123+
124+
125+
class Omniexception(__.immut.exceptions.Omniexception):
126+
''' Base for all exceptions raised by package API. '''
127+
128+
129+
class Omnierror(Omniexception, Exception):
130+
''' Base for error exceptions raised by package API. '''
131+
132+
def render_as_markdown(self) -> tuple[str, ...]:
133+
''' Renders exception as Markdown lines for display. '''
134+
return (f"❌ {self}",)
135+
```
136+
137+
**Note**: Review if `render_as_markdown()` should be included in base or if it's project-specific.
138+
139+
### Task 5: Update Foundation Packages
140+
141+
For packages that must disable foundation injection:
142+
143+
#### frigid
144+
- Update `.auxiliary/configuration/copier-answers.yaml`
145+
- Set `inject_foundations: false`
146+
- Set `inject_exceptions: false`
147+
- Keep existing hand-crafted `exceptions.py`
148+
149+
#### absence
150+
- Verify current copier answers
151+
- Update if needed: `inject_foundations: false`, `inject_exceptions: false`
152+
153+
#### accretive
154+
- Verify current copier answers
155+
- Update if needed: `inject_foundations: false`, `inject_exceptions: false`
156+
157+
#### classcore
158+
- Already has `inject_foundations: false` ✅
159+
- No changes needed
160+
161+
### Task 6: Documentation
162+
163+
Update template README to explain foundation injection:
164+
165+
```markdown
166+
## Foundation Injection
167+
168+
The template can inject foundational constructs including:
169+
- Base exception classes (`Omniexception`, `Omnierror`) with immutability
170+
- Common dependencies: `frigid`, `absence`, `dynadoc`
171+
172+
**For most projects**: Enable `inject_foundations: true` and `inject_exceptions: true`
173+
174+
**For foundation packages** (classcore, frigid, absence, accretive):
175+
- Set `inject_foundations: false`
176+
- These packages provide foundational constructs and cannot depend on themselves
177+
```
178+
179+
## Verification Steps
180+
181+
After implementation:
182+
183+
1. **Test new project generation**:
184+
- Generate test project with `inject_foundations: true`
185+
- Verify dependencies added to `pyproject.toml`
186+
- Verify `exceptions.py` uses `__.immut.exceptions.Omniexception`
187+
- Verify imports in `__/imports.py` include `frigid as immut`
188+
189+
2. **Test foundation package updates**:
190+
- Run `copier update` on frigid with `inject_foundations: false`
191+
- Verify no conflicts, no unwanted changes
192+
- Repeat for absence, accretive if applicable
193+
194+
3. **Test existing project updates**:
195+
- Run `copier update` on an existing project like agentsmgr
196+
- Verify smooth merge or identify expected conflicts
197+
- Document any manual migration steps needed
198+
199+
## Migration Notes
200+
201+
### For Existing Projects
202+
203+
When existing projects update to the new template version:
204+
205+
**If they had `inject_exceptions: true`**:
206+
- Will need to update `exceptions.py` base class inheritance
207+
- Will need to ensure `frigid as immut` import exists
208+
- Dependencies will be automatically added
209+
210+
**Migration steps**:
211+
1. Accept template updates to `pyproject.toml` (adds dependencies)
212+
2. Accept template updates to `__/imports.py` (adds frigid import)
213+
3. Review `exceptions.py` changes:
214+
- Base class changes from plain to `__.immut.exceptions.Omniexception`
215+
- Custom exception subclasses should merge cleanly (additive changes)
216+
217+
**If they had `inject_exceptions: false`**:
218+
- No changes needed
219+
- Can optionally enable foundations if desired
220+
221+
### For Foundation Packages
222+
223+
1. **Before template update**: Set `inject_foundations: false` in copier answers
224+
2. **Run template update**: Should see no changes to exceptions or dependencies
225+
3. **Verify**: Existing hand-crafted code remains untouched
226+
227+
## Open Questions
228+
229+
1. **Dependency versions**: What are the current minimum versions for `frigid`, `absence`, `dynadoc`?
230+
231+
2. **Import cascade structure**: What is the current pattern in `__/imports.py`? How should frigid import be integrated?
232+
233+
3. **render_as_markdown()**: Should this method be in the base `Omnierror` template, or is it project-specific?
234+
235+
4. **accretive and absence status**: Do these packages currently have foundation injection enabled? Need to verify their copier answers.
236+
237+
5. **Other common dependencies**: Are there other packages besides `frigid`, `absence`, `dynadoc` that should be added as defaults?
238+
239+
## Success Criteria
240+
241+
- ✅ New projects with `inject_foundations: true` get immutable exceptions and common dependencies
242+
- ✅ Foundation packages (frigid, absence, accretive, classcore) can disable injection cleanly
243+
- ✅ Existing projects can update with minimal conflicts
244+
- ✅ Documentation clearly explains when to enable/disable foundations
245+
- ✅ All verification tests pass
246+
247+
## Future Considerations
248+
249+
- Monitor merge conflicts over time to validate assumption they remain rare
250+
- If conflicts become problematic, consider adding example-based escape hatch
251+
- Consider whether other foundational constructs should be injected in future

copier.yaml

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -147,27 +147,28 @@ enable_publication:
147147
help: 'Enable package and documentation publication?'
148148
default: false
149149

150-
enable_cli:
150+
inject_foundations:
151151
type: bool
152-
help: 'Include command-line interface support?'
152+
help: 'Include foundational constructs and packages?'
153153
default: false
154154

155-
enable_executables:
155+
inject_exceptions:
156156
type: bool
157-
help: 'Generate standalone executables for the CLI?'
157+
help: 'Include base exceptions for package?'
158+
when: "{{ inject_foundations }}"
158159
default: false
159-
when: "{{ enable_cli }}"
160160

161-
inject_foundations:
161+
enable_cli:
162162
type: bool
163-
help: 'Include foundational constructs?'
163+
help: 'Include command-line interface support?'
164+
when: "{{ inject_foundations }}"
164165
default: false
165166

166-
inject_exceptions:
167+
enable_executables:
167168
type: bool
168-
help: 'Include base exceptions for package?'
169-
when: "{{ inject_foundations }}"
169+
help: 'Generate standalone executables for the CLI?'
170170
default: false
171+
when: "{{ enable_cli }}"
171172

172173
gh_owner:
173174
type: str

template/documentation/conf.py.jinja

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,16 @@ intersphinx_mapping = {
133133
'https://docs.python.org/3', None),
134134
'typing-extensions': (
135135
'https://typing-extensions.readthedocs.io/en/latest', None),
136+
# --- BEGIN: Injected by Copier ---
137+
{%- if inject_foundations %}
138+
'absence': (
139+
'https://emcd.github.io/python-absence/stable/sphinx-html', None),
140+
'dynadoc': (
141+
'https://emcd.github.io/python-dynadoc/stable/sphinx-html', None),
142+
'frigid': (
143+
'https://emcd.github.io/python-frigid/stable/sphinx-html', None),
144+
{%- endif %}
145+
# --- END: Injected by Copier ---
136146
}
137147

138148
# -- Options for todo extension ----------------------------------------------

template/pyproject.toml.jinja

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ requires-python = '>= {{ python_version_min }}'
2525
dependencies = [
2626
'typing-extensions',
2727
# --- BEGIN: Injected by Copier ---
28+
{%- if inject_foundations %}
29+
'absence~=1.1',
30+
'dynadoc~=1.4',
31+
'frigid~=4.2',
32+
{%- endif %}
2833
{%- if enable_cli %}
2934
'emcd-appcore[cli]~=1.6',
3035
{%- endif %}

template/sources/{{ package_name }}/__/imports.py.jinja

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,22 @@
2323
# ruff: noqa: F401
2424

2525

26-
import collections.abc as cabc
26+
import collections.abc as cabc
2727
import types
2828

2929
import typing_extensions as typx
3030
# --- BEGIN: Injected by Copier ---
31+
{%- if inject_foundations %}
32+
import dynadoc as ddoc
33+
import frigid as immut
34+
{%- endif %}
3135
{%- if enable_cli %}
3236
import tyro
3337
{%- endif %}
3438
# --- END: Injected by Copier ---
39+
40+
# --- BEGIN: Injected by Copier ---
41+
{%- if inject_foundations %}
42+
from absence import Absential, absent, is_absent
43+
{%- endif %}
44+
# --- END: Injected by Copier ---

template/sources/{{ package_name }}/{% if inject_exceptions %}exceptions.py{% endif %}.jinja

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,8 @@
2424
from . import __
2525

2626

27-
class Omniexception( BaseException ):
27+
class Omniexception( __.immut.exceptions.Omniexception ):
2828
''' Base for all exceptions raised by package API. '''
29-
# TODO: Class and instance attribute concealment and immutability.
30-
31-
_attribute_visibility_includes_: __.cabc.Collection[ str ] = (
32-
frozenset( ( '__cause__', '__context__', ) ) )
3329

3430

3531
class Omnierror( Omniexception, Exception ):

0 commit comments

Comments
 (0)