Skip to content

Commit e26f979

Browse files
committed
Support constant interpolation
- Allows one constant to use another - Automatically promotes a constant to an Accessor if that constant tries to interpolate an object. Will use `__str__` for the object string representation. - Tests.
1 parent d41d433 commit e26f979

5 files changed

Lines changed: 580 additions & 12 deletions

File tree

apywire/compiler.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
CACHE_ATTR_PREFIX,
1616
COMPILED_ARG_PREFIX,
1717
COMPILED_VAR_PREFIX,
18+
SYNTHETIC_CONST,
1819
)
1920
from apywire.wiring import (
2021
WiringBase,
@@ -551,7 +552,9 @@ def compile(self, *, aio: bool = False, thread_safe: bool = False) -> str:
551552
# Add import statements
552553
modules = set()
553554
for module_name, _, _, _ in self._parsed.values():
554-
modules.add(module_name)
555+
# Skip synthetic __pconst__ module
556+
if module_name != SYNTHETIC_CONST:
557+
modules.add(module_name)
555558
if aio:
556559
modules.add("asyncio")
557560
if thread_safe:
@@ -620,13 +623,19 @@ def compile(self, *, aio: bool = False, thread_safe: bool = False) -> str:
620623
factory_method,
621624
data,
622625
) in self._parsed.items():
626+
# Skip synthetic auto-promoted constants
627+
# These require runtime interpolation and can't be pre-computed
628+
if module_name == SYNTHETIC_CONST and class_name == "str":
629+
continue
630+
631+
# Regular wired object
623632
class_body.append(
624633
self._compile_property(
625634
name,
626635
module_name,
627636
class_name,
628637
factory_method,
629-
data,
638+
cast(_ResolvedSpecMapping, data),
630639
aio=aio,
631640
thread_safe=thread_safe,
632641
)

apywire/constants.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,20 @@
1919
PLACEHOLDER_END = "}"
2020
"""End marker for placeholder references in spec values."""
2121

22+
# Placeholder regex pattern (compiled for efficiency)
23+
PLACEHOLDER_PATTERN = r"\{([^{}]+)\}"
24+
"""Regex pattern for matching placeholders like {name}.
25+
26+
Matches placeholder syntax with the following rules:
27+
- Starts with { and ends with }
28+
- Captures the placeholder name (group 1)
29+
- Does not allow nested braces in the name
30+
- Example: "{host}" matches with name="host"
31+
"""
32+
33+
SYNTHETIC_CONST = "__sconst__"
34+
"""Synthetic module name used to represent promoted constants."""
35+
2236
# Cache attribute constants
2337
CACHE_ATTR_PREFIX = "_"
2438
"""Prefix for cache attributes on compiled/runtime instances."""

apywire/runtime.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@
99

1010
import asyncio
1111
import importlib
12+
import re
1213
from typing import Awaitable, Callable, Protocol, cast, final
1314

15+
from apywire.constants import SYNTHETIC_CONST
1416
from apywire.exceptions import (
1517
CircularWiringError,
1618
UnknownPlaceholderError,
@@ -205,6 +207,14 @@ def _instantiate_impl(self, name: str) -> _RuntimeValue:
205207
)
206208

207209
module_name, class_name, factory_method, data = self._parsed[name]
210+
211+
# Check for synthetic auto-promoted constant
212+
if module_name == SYNTHETIC_CONST and class_name == "str":
213+
# This is an auto-promoted constant with string interpolation
214+
value = self._format_string_constant(data, context=name)
215+
self._values[name] = value
216+
return value
217+
208218
module = importlib.import_module(module_name)
209219
cls = cast(_Constructor, getattr(module, class_name))
210220

@@ -271,6 +281,55 @@ def _resolve_runtime(
271281
return tuple(self._resolve_runtime(v, context) for v in o)
272282
return o
273283

284+
def _format_string_constant(
285+
self, template: _ResolvedValue, context: str
286+
) -> str:
287+
"""Format a string constant with wired object interpolation.
288+
289+
Converts template like "Server {host} at {now}" by:
290+
1. Finding all placeholders in the string
291+
2. Resolving each to its wired object or constant
292+
3. Converting to string via str()
293+
4. Performing string interpolation
294+
295+
Args:
296+
template: Template string with placeholders
297+
context: Name of the constant being formatted (for error messages)
298+
299+
Returns:
300+
Fully formatted string with all placeholders resolved
301+
302+
Raises:
303+
WiringError: If template is not a string
304+
UnknownPlaceholderError: If placeholder references unknown object
305+
"""
306+
# Template should be a string
307+
if not isinstance(template, str):
308+
raise WiringError(
309+
f"Auto-promoted constant '{context}' template is not a string"
310+
)
311+
312+
# Build lookup dict that resolves wired objects on access
313+
# We can't use _interpolate_placeholders directly because we need
314+
# to call getattr() for wired objects, not just lookup in a dict
315+
def replace_placeholder(match: re.Match[str]) -> str:
316+
ref_name = match.group(1)
317+
318+
# Check if the referenced name exists
319+
if ref_name not in self._values and ref_name not in self._parsed:
320+
raise UnknownPlaceholderError(
321+
f"Unknown placeholder '{ref_name}' "
322+
f"in auto-promoted constant '{context}'"
323+
)
324+
325+
# Get the value (instantiate if needed via accessor)
326+
value = cast(object, getattr(self, ref_name)())
327+
328+
# Convert to string
329+
return str(value)
330+
331+
return re.sub(self._placeholder_pattern, replace_placeholder, template)
332+
274333

275334
@final
276335
class Accessor:

0 commit comments

Comments
 (0)