Skip to content
Closed
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
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ reuse:
--license ISC \
--recursive .

format: reuse
format:

Copilot AI Dec 19, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Makefile target format previously depended on reuse but this dependency has been removed. This means running make format will no longer ensure that license headers are properly added before formatting. This could lead to code being formatted without proper license headers, which may violate the project's licensing requirements.

Suggested change
format:
format: reuse

Copilot uses AI. Check for mistakes.
uv run black apywire tests
uv run isort apywire tests

Expand Down
2 changes: 1 addition & 1 deletion apywire/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,5 +192,5 @@ def main(argv: list[str] | None = None) -> int:
return func(args)


if __name__ == "__main__":
if __name__ == "__main__": # pragma: no cover - CLI entrypoint
sys.exit(main())
91 changes: 85 additions & 6 deletions apywire/compiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
SYNTHETIC_CONST,
)
from apywire.wiring import (
Spec,
WiringBase,
_ConstantValue,
_ResolvedSpecMapping,
Expand All @@ -40,6 +41,31 @@
class WiringCompiler(WiringBase):
"""Wiring container with compilation support."""

# Explicit attribute added for type checkers
allow_partial: bool
"""Wiring container with compilation support."""

Copilot AI Dec 19, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment states "Wiring container with compilation support" which duplicates the class docstring below. This appears to be a documentation mistake where a docstring was placed on the allow_partial attribute instead of documenting the attribute itself.

Suggested change
"""Wiring container with compilation support."""
"""If True, enable partial-construction support for circular dependencies."""

Copilot uses AI. Check for mistakes.

def __init__(
self,
spec: Spec,
*,
thread_safe: bool = False,
allow_partial: bool = False,
) -> None:
"""Initialize compiler with optional partial-construction support.

Args:
spec: wiring spec
thread_safe: pack thread-safe behavior into generated code
allow_partial: If True, the generated code will use a small helper
module to support partial-construction placeholders for
circular dependencies.
"""
super().__init__(
spec, thread_safe=thread_safe, allow_partial=allow_partial
)
self.allow_partial = allow_partial

def _astify(self, obj: _ResolvedValue, aio: bool = False) -> ast.expr:
"""Convert a Python object (possibly a `_WiredRef`) to AST.

Expand Down Expand Up @@ -358,6 +384,32 @@ def _compile_property(

call = ast.Call(func=module_attr, args=args, keywords=kwargs)

# If enabled, wrap the call in a helper that preserves partial
# construction semantics for compiled code.
if self.allow_partial:
helper_call = ast.Call(
func=ast.Attribute(
value=ast.Name(id="apywire.partial", ctx=ast.Load()),
attr="_compiled_instantiate_with_partial",
ctx=ast.Load(),
),
args=[
ast.Name(id="self", ctx=ast.Load()),
ast.Constant(value=name),
self._create_lambda_function(call),
ast.Attribute(
value=ast.Name(id=module_name, ctx=ast.Load()),
attr=class_name,
ctx=ast.Load(),
),
ast.Constant(value=bool(factory_method)),
],
keywords=[],
)
inst_expr = helper_call
else:
inst_expr = call

cache_attr = f"{CACHE_ATTR_PREFIX}{name}"

# Create reusable components
Expand All @@ -366,10 +418,10 @@ def _compile_property(
# Build the assignment that sets the cache value; different
# behavior is required for async vs sync callers.
if not aio:
assign_cache = self._create_cache_assignment(cache_attr, call)
assign_cache = self._create_cache_assignment(cache_attr, inst_expr)
else:
# Async path: compute local values and call in executor.
lambda_expr = self._create_lambda_function(call)
lambda_expr = self._create_lambda_function(inst_expr)
pre_statements.append(self._create_loop_assignment())
run_call = self._create_executor_call(lambda_expr)
assign_cache = self._create_cache_assignment(cache_attr, run_call)
Expand All @@ -384,7 +436,7 @@ def _compile_property(
)
elif not aio and thread_safe:
# Build sync thread-safe version using helper mixin
maker = self._create_lambda_function(call)
maker = self._create_lambda_function(inst_expr)
call_inst = ast.Call(
func=ast.Attribute(
value=ast.Name(id="self", ctx=ast.Load()),
Expand Down Expand Up @@ -610,6 +662,11 @@ def compile(self, *, aio: bool = False, thread_safe: bool = False) -> str:
# When compiling thread_safe, import thread-safety primitives
modules.add("apywire.threads")
modules.add("apywire.exceptions")
# If compiler is configured to support partial construction, ensure
# runtime helper is available in generated module.
if self.allow_partial:
modules.add("apywire.partial")
modules.add("apywire.exceptions")
for module in sorted(modules):
if module == "apywire.threads":
# Import ThreadSafeMixin from threads
Expand All @@ -633,15 +690,21 @@ def compile(self, *, aio: bool = False, thread_safe: bool = False) -> str:
level=0,
)
)
elif module == "apywire.partial":
# Import partial helper module used by compiled code
imp = ast.Import(names=[ast.alias(name="apywire.partial")])
body.append(imp)
else:
body.append(ast.Import(names=[ast.alias(name=module)]))

# Build class body
class_body: list[ast.stmt] = []
# When thread safe, add __init__ that calls helper mixin init
# Add __init__ if needed to support thread-safety init and/or
# partial-construction flag propagation.
init_body: list[ast.stmt] = []
needs_init = False
if thread_safe:
# class __init__
init_body: list[ast.stmt] = []
needs_init = True
# compiled class will inherit from the mixin and just call
# `_init_thread_safety` from its constructor
init_body.append(
Expand All @@ -657,6 +720,22 @@ def compile(self, *, aio: bool = False, thread_safe: bool = False) -> str:
),
)
)
if self.allow_partial:
needs_init = True
# Set instance flag so compiled container behaves like runtime
init_body.append(
ast.Assign(
targets=[
ast.Attribute(
value=ast.Name(id="self", ctx=ast.Load()),
attr="allow_partial",
ctx=ast.Store(),
)
],
value=ast.Constant(value=True),
)
)
if needs_init:
init_def = ast.FunctionDef(
name="__init__",
args=_PROPERTY_ARGS,
Expand Down
18 changes: 18 additions & 0 deletions apywire/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,24 @@ class CircularWiringError(WiringError):
"""


class PartialConstructionAccessError(WiringError):
"""Raised when attempting to access or call an object that is still in
partial construction (placeholder inserted but not yet finalized).

This error indicates the wiring is in a transient state and the user
attempted to use the placeholder before it was bound to the final
instance.
"""


class PartialConstructionError(WiringError):
"""Raised when partial construction cannot be completed successfully.

Examples include factories that bypass `__new__` and return a different
instance than the skeleton allocated by the wiring system.
"""


class LockUnavailableError(RuntimeError):
"""Exception raised when a per-attribute lock cannot be acquired in
optimistic (non-blocking) mode and the code must fall back to a global
Expand Down
Loading