Skip to content

Commit ca4bcbf

Browse files
committed
Small performance improvements
- Less dictionary lookups - Better logic in places like checking instance types - Overall review of code comments
1 parent 6ca8f41 commit ca4bcbf

5 files changed

Lines changed: 61 additions & 99 deletions

File tree

apywire/compiler.py

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from __future__ import annotations
99

1010
import ast
11+
from operator import itemgetter
1112
from types import EllipsisType
1213
from typing import cast
1314

@@ -25,7 +26,6 @@
2526
_WiredRef,
2627
)
2728

28-
# Constant for property AST arguments (used in compile methods)
2929
_PROPERTY_ARGS = ast.arguments(
3030
posonlyargs=[],
3131
args=[ast.arg(arg="self")],
@@ -122,17 +122,13 @@ def _compile_property(
122122
attr=class_name,
123123
ctx=ast.Load(),
124124
)
125-
kwargs: list[ast.keyword] = []
126-
# When compiling async accessors we must precompute awaited
127-
# referenced attributes into locals so that the constructor
128-
# lambda passed to a thread pool executor is pure synchronous.
129125
pre_statements: list[ast.stmt] = []
130126

131-
# Counter for generating unique variable names (mutable list)
132-
counter = [0]
127+
counter = [0] # For generating unique variable names
133128

134129
args: list[ast.expr] = []
135130

131+
kwargs: list[ast.keyword] = []
136132
# Normalize data into args_data (list) and kwargs_data (dict)
137133
args_data: list[_ResolvedValue] = []
138134
kwargs_data: dict[str, _ResolvedValue] = {}
@@ -141,12 +137,16 @@ def _compile_property(
141137
args_data = data
142138
else:
143139
data_dict = data
144-
int_keys = sorted(k for k in data_dict if isinstance(k, int))
145-
str_keys = [k for k in data_dict if isinstance(k, str)]
146-
for k_int in int_keys:
147-
args_data.append(data_dict[k_int])
148-
for k_str in str_keys:
149-
kwargs_data[k_str] = data_dict[k_str]
140+
# Iterate once over data.items() to separate args and kwargs
141+
args_items = []
142+
kwargs_data = {}
143+
for k, v in data_dict.items():
144+
if isinstance(k, int):
145+
args_items.append((k, v))
146+
elif isinstance(k, str):
147+
kwargs_data[k] = v
148+
args_items.sort(key=itemgetter(0))
149+
args_data = [v for _, v in args_items]
150150

151151
# Process positional args
152152
for i, value in enumerate(args_data):
@@ -189,12 +189,8 @@ def _compile_property(
189189
kw_val = raw_val_ast
190190
kwargs.append(ast.keyword(arg=key, value=kw_val))
191191

192-
# Build the actual constructor call (module.Class(*args, **kwargs))
193192
call = ast.Call(func=module_attr, args=args, keywords=kwargs)
194193

195-
# Cache attribute name like `_name` used to store instantiated
196-
# objects on `self` at runtime. The compiled accessor returns
197-
# `self._name` when present.
198194
cache_attr = f"{CACHE_ATTR_PREFIX}{name}"
199195

200196
# if not hasattr(self, '_name'): ...

apywire/constants.py

Lines changed: 10 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -8,38 +8,19 @@
88
to improve maintainability and reduce duplication.
99
"""
1010

11-
# Spec parsing constants
12-
SPEC_KEY_DELIMITER = " "
13-
"""Delimiter used to separate module.Class from name in spec keys."""
11+
SPEC_KEY_DELIMITER = " " # Separates module.Class from name in spec keys
1412

15-
# Placeholder constants
16-
PLACEHOLDER_START = "{"
17-
"""Start marker for placeholder references in spec values."""
13+
PLACEHOLDER_START = "{" # Start marker for placeholder references
14+
PLACEHOLDER_END = "}" # End marker for placeholder references
1815

19-
PLACEHOLDER_END = "}"
20-
"""End marker for placeholder references in spec values."""
21-
22-
# Placeholder regex pattern (compiled for efficiency)
16+
# Regex pattern for placeholders like {name}: captures name, no nested braces
2317
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."""
3518

36-
# Cache attribute constants
37-
CACHE_ATTR_PREFIX = "_"
38-
"""Prefix for cache attributes on compiled/runtime instances."""
19+
SYNTHETIC_CONST = "__sconst__" # Synthetic module for promoted constants
3920

40-
# Compiled code generation constants
41-
COMPILED_VAR_PREFIX = "__val_"
42-
"""Prefix for temporary variables in compiled code."""
21+
CACHE_ATTR_PREFIX = "_" # Prefix for cache attributes (_name)
4322

44-
COMPILED_ARG_PREFIX = "__arg_"
45-
"""Prefix for argument variables in compiled code."""
23+
COMPILED_VAR_PREFIX = "__val_" # Prefix for temp variables in compiled code
24+
COMPILED_ARG_PREFIX = (
25+
"__arg_" # Prefix for argument variables in compiled code
26+
)

apywire/runtime.py

Lines changed: 19 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import asyncio
1111
import importlib
1212
import re
13+
from operator import itemgetter
1314
from typing import Awaitable, Callable, Protocol, cast, final
1415

1516
from apywire.constants import SYNTHETIC_CONST
@@ -154,20 +155,17 @@ def _separate_args_kwargs(
154155
Returns:
155156
Tuple of (positional_args, keyword_args)
156157
"""
157-
args_list: list[tuple[int, object]] = []
158-
kwargs_dict: dict[str, object] = {}
159-
158+
# Iterate once over data.items() to separate args and kwargs
159+
args_list = []
160+
kwargs_dict = {}
160161
for k, v in data.items():
161162
if isinstance(k, int):
162163
args_list.append((k, v))
163164
else:
164165
kwargs_dict[k] = v
165166

166-
# Sort positional args by index
167-
def _sort_key(item: tuple[int, object]) -> int:
168-
return item[0]
169-
170-
args_list.sort(key=_sort_key)
167+
# Sort positional args by index and extract values
168+
args_list.sort(key=itemgetter(0))
171169
pos_args = [v for _, v in args_list]
172170

173171
return (pos_args, kwargs_dict)
@@ -244,8 +242,6 @@ def _instantiate_impl(self, name: str) -> _RuntimeValue:
244242
# All positional arguments
245243
instance = constructor(*kwargs)
246244
else:
247-
# Should not happen given _ResolvedSpecMapping type,
248-
# but for safety
249245
instance = constructor(kwargs)
250246
except Exception as e:
251247
raise WiringError(
@@ -267,9 +263,7 @@ def _resolve_runtime(
267263
their accessors.
268264
"""
269265
if isinstance(o, _WiredRef):
270-
# Detect when a placeholder reference points to something that
271-
# wasn't defined in the spec. This is an unknown placeholder
272-
# and should raise a friendly error.
266+
# Ensure placeholder was defined in spec
273267
if o.name not in self._values and o.name not in self._parsed:
274268
ctx = f" while instantiating '{context}'" if context else ""
275269
raise UnknownPlaceholderError(
@@ -280,11 +274,11 @@ def _resolve_runtime(
280274
# Call the accessor `self.name()` to get the runtime value.
281275
return cast(object, getattr(self, o.name)())
282276

283-
if isinstance(o, dict):
277+
elif isinstance(o, dict):
284278
return {k: self._resolve_runtime(v, context) for k, v in o.items()}
285-
if isinstance(o, list):
279+
elif isinstance(o, list):
286280
return [self._resolve_runtime(v, context) for v in o]
287-
if isinstance(o, tuple):
281+
elif isinstance(o, tuple):
288282
return tuple(self._resolve_runtime(v, context) for v in o)
289283
return o
290284

@@ -348,11 +342,12 @@ def __init__(self, wiring: WiringRuntime, name: str) -> None:
348342

349343
def __call__(self) -> object:
350344
"""Return the wired object, instantiating it if necessary."""
351-
# Fast path: check if already instantiated and cached in _values
352-
# or as an attribute on the instance.
353-
# We check _values first as it is the canonical storage.
354-
if self._name in self._wiring._values:
345+
# Fast path: EAFP pattern for cache lookup
346+
# Try to get cached value directly (faster than check + get)
347+
try:
355348
return self._wiring._values[self._name]
349+
except KeyError:
350+
pass
356351

357352
# Not cached, so we need to instantiate it.
358353
# We use _instantiate_attr which handles thread safety if enabled.
@@ -381,9 +376,11 @@ def __getattr__(self, name: str) -> Callable[[], Awaitable[object]]:
381376
)
382377

383378
async def _get() -> object:
384-
# If already cached, return immediately
385-
if name in self._wiring._values:
379+
# EAFP: Try cached value first
380+
try:
386381
return self._wiring._values[name]
382+
except KeyError:
383+
pass
387384

388385
# If not cached, run instantiation in executor to avoid blocking
389386
# the event loop.

apywire/threads.py

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,12 @@
2020

2121

2222
class _ThreadLocalState(threading.local):
23-
"""Typed thread-local storage for wiring resolution state.
23+
"""Thread-local storage for wiring resolution state.
2424
25-
This class provides properly typed attributes to avoid mypy Any propagation
26-
when accessing dynamically-set attributes on threading.local().
27-
28-
Attributes are lazily initialized per-thread:
29-
resolving_stack: Stack of attribute names currently being resolved
30-
in this thread (for circular dependency detection).
31-
mode: Current instantiation mode ('optimistic', 'global', or None).
32-
held_locks: List of locks currently held by this thread.
25+
Provides typed attributes initialized per-thread:
26+
- resolving_stack: Stack for circular dependency detection
27+
- mode: Current instantiation mode ('optimistic', 'global', or None)
28+
- held_locks: Locks held by this thread
3329
"""
3430

3531
def __init__(self) -> None:
@@ -94,7 +90,6 @@ def _check_cache(self, name: str) -> tuple[bool, object | None]:
9490
"""
9591
cache_attr = f"{CACHE_ATTR_PREFIX}{name}"
9692

97-
# Check _values dict first (runtime path)
9893
if hasattr(self, "_values"):
9994
values_dict = cast(dict[str, object], getattr(self, "_values"))
10095
if name in values_dict:
@@ -162,9 +157,7 @@ def _instantiate_attr(
162157
found, value = self._check_cache(name)
163158
if found:
164159
return value
165-
# Enter optimistic mode and track held locks to allow
166-
# nested instantiation to detect per-attribute
167-
# conflicts.
160+
# Enter optimistic mode and track held locks
168161
self._local.mode = "optimistic"
169162
held = self._get_held_locks()
170163
held.clear()

apywire/wiring.py

Lines changed: 13 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,6 @@
3535
)
3636

3737

38-
# Marker class for a placeholder reference. Declared before the
39-
# type aliases so `ResolvedValue` can reference it directly.
4038
class _WiredRef:
4139
"""Marker for a value that references another wired attribute.
4240
@@ -49,9 +47,7 @@ def __init__(self, name: str) -> None:
4947
self.name = name
5048

5149

52-
# Spec value types come from the user-provided `Spec` input. They are
53-
# primitives or nested containers and may include placeholder strings
54-
# like "{otherName}".
50+
# User-provided spec values: primitives, containers, or placeholders
5551
_SpecValue: TypeAlias = (
5652
_ConstantValue
5753
| str
@@ -60,9 +56,7 @@ def __init__(self, name: str) -> None:
6056
| dict[str | int, "_SpecValue"]
6157
)
6258

63-
# Resolved values are produced after parsing placeholders; strings of
64-
# the form "{name}" become `_WiredRef` markers and are resolved at
65-
# instantiation.
59+
# After parsing: placeholder strings "{name}" become _WiredRef markers
6660
_ResolvedValue: TypeAlias = (
6761
_ConstantValue
6862
| _WiredRef
@@ -177,8 +171,8 @@ def __init__(
177171
# Has placeholders - classify later
178172
const_with_refs[key] = (value, placeholder_names)
179173

180-
# Separate constants with refs into const-only vs wired-object refs
181-
# handling transitive promotion
174+
# Promote constants to accessors if they reference wired objects:
175+
# Transitive: mark direct refs, then propagate to dependents
182176
const_deps_graph: dict[str, set[str]] = {
183177
key: placeholder_names
184178
for key, (value, placeholder_names) in const_with_refs.items()
@@ -308,11 +302,11 @@ def _resolve(self, obj: _SpecValue) -> _ResolvedValue:
308302
ref_name = self._extract_placeholder_name(obj)
309303
return _WiredRef(ref_name)
310304
return obj
311-
if isinstance(obj, dict):
305+
elif isinstance(obj, dict):
312306
return {k: self._resolve(v) for k, v in obj.items()}
313-
if isinstance(obj, list):
307+
elif isinstance(obj, list):
314308
return [self._resolve(v) for v in obj]
315-
if isinstance(obj, tuple):
309+
elif isinstance(obj, tuple):
316310
return tuple(self._resolve(v) for v in obj)
317311
return obj
318312

@@ -387,11 +381,12 @@ def _topological_sort(
387381
CircularWiringError: If circular dependencies detected
388382
"""
389383
# Calculate in-degree (number of dependencies) for each node
390-
# Only count dependencies that are also in the set being sorted
384+
# Pre-convert to set for efficient intersection
385+
all_nodes = set(dependencies.keys())
391386
in_degree: dict[str, int] = {}
392387
for node, deps in dependencies.items():
393388
# Filter to only dependencies within this set
394-
internal_deps = deps & dependencies.keys()
389+
internal_deps = deps & all_nodes
395390
in_degree[node] = len(internal_deps)
396391

397392
# Start with nodes that have no dependencies (within this set)
@@ -459,13 +454,13 @@ def _resolve_constant(
459454

460455
# Handle embedded placeholders via string interpolation
461456
return self._interpolate_placeholders(value, resolved, "constant")
462-
if isinstance(value, dict):
457+
elif isinstance(value, dict):
463458
return {
464459
k: self._resolve_constant(v, resolved)
465460
for k, v in value.items()
466461
}
467-
if isinstance(value, list):
462+
elif isinstance(value, list):
468463
return [self._resolve_constant(v, resolved) for v in value]
469-
if isinstance(value, tuple):
464+
elif isinstance(value, tuple):
470465
return tuple(self._resolve_constant(v, resolved) for v in value)
471466
return value

0 commit comments

Comments
 (0)