config: an out-of-tree replacement point for every resolution-pipeline step (#39134)

This commit is contained in:
Cheng Wan
2026-09-12 17:26:54 -07:00
committed by GitHub
parent a8b5616303
commit 6804eeaabe
8 changed files with 593 additions and 92 deletions
+3 -1
View File
@@ -18,6 +18,7 @@ from sglang.srt.arg_groups.overrides import (
from sglang.srt.environ import envs
from sglang.srt.model_executor.cuda_graph_config import Backend, Phase
from sglang.srt.runtime_context import get_platform
from sglang.srt.utils.common import get_device_memory_capacity
logger = logging.getLogger(__name__)
@@ -37,7 +38,7 @@ def handle_offload_compatibility(server_args: Any) -> None:
)
def handle_gpu_memory_settings(server_args: Any, gpu_mem):
def handle_gpu_memory_settings(server_args: Any):
"""
Configure GPU memory-dependent settings including
chunked_prefill_size, cuda_graph_config[decode].max_bs, and mem_fraction_static.
@@ -68,6 +69,7 @@ def handle_gpu_memory_settings(server_args: Any, gpu_mem):
)
cfg = resolving_view(server_args)
gpu_mem = get_device_memory_capacity(cfg.device)
# A copy, so an earlier declaration keeps the value it recorded.
cuda_graph_config = copy.deepcopy(cfg.cuda_graph_config)
decode_cuda_graph_config = cuda_graph_config.decode
+6 -1
View File
@@ -28,6 +28,7 @@ from sglang.srt.arg_groups.overrides import (
use_mla_backend,
validate_declarations,
)
from sglang.srt.arg_groups.resolution_hooks import run_hook
from sglang.srt.configs.embedding_model_spec import BCGPrefillPolicy
from sglang.srt.configs.linear_attn_model_registry import get_linear_attn_spec_by_arch
from sglang.srt.connector import ConnectorType
@@ -786,7 +787,11 @@ def handle_model_capability_adjustments(server_args: Any):
"_handle_model_capability_adjustments",
prefill_only_disable_kv_cache=True,
)
validate_prefill_only_disable_kv_cache_args(server_args)
# Through the registry, not a bare call: an out-of-tree
# replacement registered at this validator's own pipeline
# position must also win here, at this later re-validation after
# the Hopper/Blackwell no-KV-pool default declares itself.
run_hook(validate_prefill_only_disable_kv_cache_args, server_args)
declare_resolution(
server_args,
"_handle_model_capability_adjustments",
@@ -19,6 +19,7 @@ from sglang.srt.arg_groups.overrides import (
run_post_process_pass,
should_report_expert_balancedness,
)
from sglang.srt.arg_groups.resolution_hooks import run_hook
from sglang.srt.connector import ConnectorType
from sglang.srt.environ import envs
from sglang.srt.model_executor.cuda_graph_config import Backend, Phase, with_phase
@@ -29,7 +30,12 @@ logger = logging.getLogger(__name__)
def handle_context_parallelism(server_args: Any):
validate_prefill_cp_platform(server_args)
# Through the registry, not a bare call: an out-of-tree replacement of
# `validate_prefill_cp_platform` registered at its own (earlier) pipeline
# position must also win here, or a package permitting prefill CP on its
# own qualified HIP/NPU/MUSA build would still hit the original rejection
# at this later, nested call.
run_hook(validate_prefill_cp_platform, server_args)
cfg = resolving_view(server_args)
if parse_connector_type(cfg.model_path) != ConnectorType.INSTANCE:
+83 -80
View File
@@ -18,7 +18,7 @@ from sglang.srt.arg_groups.overrides import (
resolving_view,
run_post_process_pass,
)
from sglang.srt.utils.common import get_device_memory_capacity
from sglang.srt.arg_groups.resolution_hooks import run_hook
def run_resolution_pipeline(server_args: Any) -> None:
@@ -43,6 +43,11 @@ def run_resolution_pipeline(server_args: Any) -> None:
5. Give each handler one clear contract: what state it expects, what it
may mutate, and whether it validates only. Long ordering comments
belong in the helper or signal that the helper should be split.
6. Call each step through ``run_hook(handle_x, server_args, ...)``, not
``handle_x(server_args, ...)`` directly -- see
``arg_groups/resolution_hooks.py``. This is every step's fixed
position in the pipeline either way; registering an override changes
what runs here, never when.
"""
# What the caller asked for, before any handler runs; this plus the
@@ -62,7 +67,7 @@ def run_resolution_pipeline(server_args: Any) -> None:
from sglang.srt.arg_groups.mega_moe_hook import handle_mega_moe
handle_mega_moe(server_args)
run_hook(handle_mega_moe, server_args)
from sglang.srt.arg_groups.serving_hook import (
handle_asr_validation,
handle_crash_dump_env,
@@ -81,17 +86,17 @@ def run_resolution_pipeline(server_args: Any) -> None:
handle_tokenizer_batching,
)
handle_return_hidden_states_mode(server_args)
handle_media_url_security(server_args)
run_hook(handle_return_hidden_states_mode, server_args)
run_hook(handle_media_url_security, server_args)
from sglang.srt.arg_groups.hicache_hook import (
handle_hicache,
handle_hicache_ratio_default,
)
handle_hicache_ratio_default(server_args)
run_hook(handle_hicache_ratio_default, server_args)
from sglang.srt.arg_groups.memory_hook import handle_offload_compatibility
handle_offload_compatibility(server_args)
run_hook(handle_offload_compatibility, server_args)
from sglang.srt.arg_groups.validation_hook import (
default_unset_prefill_decode_interval,
validate_experimental_sgl_marlin,
@@ -99,8 +104,8 @@ def run_resolution_pipeline(server_args: Any) -> None:
validate_sampling_mask_max_tokens,
)
validate_prefill_decode_interval(server_args)
validate_sampling_mask_max_tokens(server_args)
run_hook(validate_prefill_decode_interval, server_args)
run_hook(validate_sampling_mask_max_tokens, server_args)
# Reject an explicitly enabled but incompatible hardware runtime before
# model path resolution, downloads, or the dummy-model short circuit.
@@ -109,8 +114,8 @@ def run_resolution_pipeline(server_args: Any) -> None:
handle_hardware_runtime_validation,
)
validate_prefill_cp_platform(server_args)
handle_hardware_runtime_validation()
run_hook(validate_prefill_cp_platform, server_args)
run_hook(handle_hardware_runtime_validation, server_args)
if cfg.model_path.lower() in ["none", "dummy"]:
return
@@ -119,30 +124,30 @@ def run_resolution_pipeline(server_args: Any) -> None:
handle_model_source_paths,
)
handle_model_source_paths(server_args)
run_hook(handle_model_source_paths, server_args)
# Validate mm_process_config.
handle_multimodal(server_args)
run_hook(handle_multimodal, server_args)
# Validate SSL arguments early.
handle_ssl_validation(server_args)
run_hook(handle_ssl_validation, server_args)
# Validate transcription/ASR-specific server args.
handle_asr_validation(server_args)
run_hook(handle_asr_validation, server_args)
# Handle deprecated arguments.
handle_deprecated_args(server_args)
run_hook(handle_deprecated_args, server_args)
# Handle deprecated environment variables for prefill delayer.
handle_prefill_delayer_env_compat(server_args)
run_hook(handle_prefill_delayer_env_compat, server_args)
# Set missing default values.
handle_missing_default_values(server_args)
run_hook(handle_missing_default_values, server_args)
# expert_pack may replace a raw GGUF input with its generated local
# model metadata before any model-specific handler calls model_config_of.
# It also establishes eager-only invariants before CUDA graph parsing.
from sglang.srt.arg_groups.expert_pack_hook import handle_expert_pack
handle_expert_pack(server_args)
run_hook(handle_expert_pack, server_args)
# Validate PD disaggregation flags before CUDA graph config.
from sglang.srt.arg_groups.pd_disaggregation_hook import (
@@ -150,7 +155,7 @@ def run_resolution_pipeline(server_args: Any) -> None:
handle_pd_disaggregation,
)
handle_pd_disaggregation(server_args)
run_hook(handle_pd_disaggregation, server_args)
from sglang.srt.arg_groups.kv_cache_hook import (
handle_cache_compatibility,
@@ -171,8 +176,8 @@ def run_resolution_pipeline(server_args: Any) -> None:
handle_expert_distribution_metrics,
)
validate_prefill_only_disable_kv_cache_args(server_args)
handle_decode_context_parallelism(server_args)
run_hook(validate_prefill_only_disable_kv_cache_args, server_args)
run_hook(handle_decode_context_parallelism, server_args)
# Model-arch prefill CUDA-graph default must land before cuda-graph
# resolution (the declarative registry materializes too late to affect
@@ -186,16 +191,16 @@ def run_resolution_pipeline(server_args: Any) -> None:
handle_cuda_graph_config,
)
apply_inkling_prefill_cuda_graph_default(server_args)
apply_muse_glimmer_prefill_cuda_graph_max_bs_default(server_args)
run_hook(apply_inkling_prefill_cuda_graph_default, server_args)
run_hook(apply_muse_glimmer_prefill_cuda_graph_max_bs_default, server_args)
# must run before _handle_cuda_graph_config and _handle_data_parallelism
handle_dwdp(server_args)
run_hook(handle_dwdp, server_args)
handle_cuda_graph_config(server_args)
run_hook(handle_cuda_graph_config, server_args)
# Requires the parsed backend and explicit-input locks, and must precede
# handle_gpu_memory_settings so the chunk size feeds memory budgeting.
apply_glm5_chunked_prefill_default(server_args)
run_hook(apply_glm5_chunked_prefill_default, server_args)
# Handle device-specific backends.
from sglang.srt.arg_groups.platform_hook import (
@@ -210,23 +215,21 @@ def run_resolution_pipeline(server_args: Any) -> None:
handle_xpu_backends,
)
handle_hpu_backends(server_args)
handle_cpu_backends(server_args)
handle_npu_backends(server_args)
handle_mps_backends(server_args)
handle_xpu_backends(server_args)
run_hook(handle_hpu_backends, server_args)
run_hook(handle_cpu_backends, server_args)
run_hook(handle_npu_backends, server_args)
run_hook(handle_mps_backends, server_args)
run_hook(handle_xpu_backends, server_args)
# Must precede handle_gpu_memory_settings: its symm-mem prealloc default
# keys off enable_symm_mem.
handle_symm_mem_device_support(server_args)
run_hook(handle_symm_mem_device_support, server_args)
handle_platform_defaults(server_args)
gpu_mem = get_device_memory_capacity(cfg.device)
run_hook(handle_platform_defaults, server_args)
# Handle memory-related, chunked prefill, and CUDA graph batch size configurations.
from sglang.srt.arg_groups.memory_hook import handle_gpu_memory_settings
handle_gpu_memory_settings(server_args, gpu_mem)
run_hook(handle_gpu_memory_settings, server_args)
# Apply model-specific adjustments.
from sglang.srt.arg_groups.model_hook import (
@@ -234,10 +237,10 @@ def run_resolution_pipeline(server_args: Any) -> None:
handle_model_specific_adjustments,
)
handle_model_specific_adjustments(server_args)
default_unset_prefill_decode_interval(server_args)
run_hook(handle_model_specific_adjustments, server_args)
run_hook(default_unset_prefill_decode_interval, server_args)
# After the model overrides: Qwen4-Exp declares the PLE offload default there.
handle_offload_compatibility(server_args)
run_hook(handle_offload_compatibility, server_args)
# Set kernel backends.
run_post_process_pass(server_args, _sampling_backend_default)
@@ -250,49 +253,49 @@ def run_resolution_pipeline(server_args: Any) -> None:
handle_multi_item_scoring,
)
handle_deterministic_inference(server_args)
handle_attention_backend_compatibility(server_args)
run_hook(handle_deterministic_inference, server_args)
run_hook(handle_attention_backend_compatibility, server_args)
# Must run after the attention backend is resolved so the trtllm_mla
# default (auto-selected for DeepseekV3ForCausalLM on sm100) is visible.
disable_prefill_cuda_graph_for_deepseek_trtllm_mla(server_args)
run_hook(disable_prefill_cuda_graph_for_deepseek_trtllm_mla, server_args)
from sglang.srt.arg_groups.mamba_hook import (
handle_int8_mamba_checkpoint,
handle_mamba_backend,
)
handle_mamba_backend(server_args)
handle_int8_mamba_checkpoint(server_args)
handle_linear_attn_backend(server_args)
apply_glm5_prefill_cuda_graph_policy(server_args)
handle_kv4_compatibility(server_args)
handle_mxfp8_kv_cache_compatibility(server_args)
run_hook(handle_mamba_backend, server_args)
run_hook(handle_int8_mamba_checkpoint, server_args)
run_hook(handle_linear_attn_backend, server_args)
run_hook(apply_glm5_prefill_cuda_graph_policy, server_args)
run_hook(handle_kv4_compatibility, server_args)
run_hook(handle_mxfp8_kv_cache_compatibility, server_args)
run_post_process_pass(server_args, _page_size_default)
handle_amd_specifics(server_args)
handle_nccl_pre_warm(server_args)
handle_grammar_backend(server_args)
run_hook(handle_amd_specifics, server_args)
run_hook(handle_nccl_pre_warm, server_args)
run_hook(handle_grammar_backend, server_args)
# Handle multi-item scoring constraints. Must run after the above so
# the final attention backend and chunked_prefill_size are in effect.
handle_multi_item_scoring(server_args)
run_hook(handle_multi_item_scoring, server_args)
# Backend-dependent half of --prefill-only-disable-kv-cache validation.
# Must stay after _handle_attention_backend_compatibility() (above) and
# _handle_multi_item_scoring() so the resolved prefill backend is final;
# the flag/precondition half runs earlier in
# _validate_prefill_only_disable_kv_cache_args().
handle_prefill_only_disable_kv_cache(server_args)
run_hook(handle_prefill_only_disable_kv_cache, server_args)
# Handle Hicache settings.
handle_hicache(server_args)
run_hook(handle_hicache, server_args)
# Handle data parallelism.
handle_data_parallelism(server_args)
run_hook(handle_data_parallelism, server_args)
# Normalize load balancing defaults.
handle_load_balance_method(server_args)
run_hook(handle_load_balance_method, server_args)
# Handle context parallelism.
handle_context_parallelism(server_args)
run_hook(handle_context_parallelism, server_args)
# Handle MoE configurations.
from sglang.srt.arg_groups.moe_hook import (
@@ -303,12 +306,12 @@ def run_resolution_pipeline(server_args: Any) -> None:
validate_deepep_v2_speculative_draft,
)
handle_moe_kernel_config(server_args)
handle_a2a_moe(server_args)
handle_eplb_and_dispatch(server_args)
handle_expert_distribution_metrics(server_args)
handle_elastic_ep(server_args)
validate_experimental_sgl_marlin(server_args)
run_hook(handle_moe_kernel_config, server_args)
run_hook(handle_a2a_moe, server_args)
run_hook(handle_eplb_and_dispatch, server_args)
run_hook(handle_expert_distribution_metrics, server_args)
run_hook(handle_elastic_ep, server_args)
run_hook(validate_experimental_sgl_marlin, server_args)
# Handle pipeline parallelism.
run_post_process_pass(server_args, _pipeline_parallel_overlap_disable)
@@ -317,55 +320,55 @@ def run_resolution_pipeline(server_args: Any) -> None:
from sglang.srt.arg_groups.speculative_hook import handle_speculative_decoding
handle_speculative_decoding(server_args)
run_hook(handle_speculative_decoding, server_args)
# After the speculative hook so speculative_algorithm is final.
from sglang.srt.arg_groups.layernorm_sp_hook import handle_layernorm_sp
handle_layernorm_sp(server_args)
run_hook(handle_layernorm_sp, server_args)
# Validate the CuteDSL A2A token budget now that num_tokens_per_req is final.
validate_cutedsl_a2a_token_budget(server_args)
run_hook(validate_cutedsl_a2a_token_budget, server_args)
# Handle model loading format.
handle_load_format(server_args)
run_hook(handle_load_format, server_args)
# Handle Encoder disaggregation.
handle_encoder_disaggregation(server_args)
run_hook(handle_encoder_disaggregation, server_args)
# Validate tokenizer settings.
handle_tokenizer_batching(server_args)
run_hook(handle_tokenizer_batching, server_args)
# Propagate environment variables.
handle_environment_variables(server_args)
run_hook(handle_environment_variables, server_args)
# Validate cache settings.
handle_cache_compatibility(server_args)
run_hook(handle_cache_compatibility, server_args)
handle_page_major_kv_layout(server_args)
run_hook(handle_page_major_kv_layout, server_args)
handle_unified_memory_pool(server_args)
run_hook(handle_unified_memory_pool, server_args)
# Handle diffusion LLM inference.
from sglang.srt.arg_groups.dllm_hook import handle_dllm_inference
handle_dllm_inference(server_args)
run_hook(handle_dllm_inference, server_args)
# Handle crash dump environment variables (must run before CUDA init).
handle_crash_dump_env(server_args)
run_hook(handle_crash_dump_env, server_args)
# Handle debug utilities.
handle_debug_utils(server_args)
run_hook(handle_debug_utils, server_args)
# Handle any other necessary validations.
handle_other_validations(server_args)
run_hook(handle_other_validations, server_args)
# Model-capability adjustments that legacy code applied at model-load
# time; last declarations of the resolution, mirroring that order.
handle_model_capability_adjustments(server_args)
run_hook(handle_model_capability_adjustments, server_args)
# Validate after all batch-size declarations are visible.
validate_deepep_v2_speculative_draft(server_args)
validate_deepep_v2_dispatch_token_budget(server_args)
run_hook(validate_deepep_v2_speculative_draft, server_args)
run_hook(validate_deepep_v2_dispatch_token_budget, server_args)
server_args._resolution_finished = True
@@ -20,11 +20,13 @@ from sglang.srt.utils.common import is_host_cpu_arm64
logger = logging.getLogger(__name__)
def handle_hardware_runtime_validation():
# This is intentionally independent of `server_args.device`: setting
# SGLANG_USE_MLX opts into the MLX backend and must fail immediately if
# the environment cannot honor that request. With the flag unset,
# use_mlx() remains lazy and does not import MLX.
def handle_hardware_runtime_validation(server_args: Any):
# `server_args` is accepted, not read: every resolution-hook step takes
# it, uniformly, so `run_hook` never has to special-case an arity. The
# check below is intentionally independent of `server_args.device`:
# setting SGLANG_USE_MLX opts into the MLX backend and must fail
# immediately if the environment cannot honor that request. With the
# flag unset, use_mlx() remains lazy and does not import MLX.
use_mlx()
@@ -0,0 +1,204 @@
"""Out-of-tree replacement for a named step of the resolution pipeline.
`run_resolution_pipeline` calls its steps by name, hardcoded, with no
per-step dispatch through `self` -- there is nothing on `ServerArgs` left to
subclass in order to change how one step decides. This is the replacement
for that: a decorator that wraps whatever currently runs under a given name,
and a dispatcher the pipeline calls instead of the bare function. The name
itself is never spelled out at the call site -- `run_hook` reads it off the
function it was handed -- so there is exactly one place a step's name is
written by hand: the whitelist below, and whatever a downstream registrant
passes to the decorator.
Whitelisted names only, the same discipline `Arg(resolvable=True)` uses for
declarable fields: this project has already been bitten once by an
unqualified name collision in this exact pipeline (`_parse_cuda_graph_config`
and `_handle_cuda_graph_config` merged under one rename and the dispatcher
called itself). A name that is not on the list fails loudly at import time,
not silently at the call site three modules away.
Every step takes exactly `(server_args)`, `handle_hardware_runtime_validation`
included -- it does not read `server_args` (see the comment at its
definition), but it takes the parameter anyway so `run_hook` never has to
special-case an arity. An override's own signature always matches: `def
mine(server_args, previous)`.
"""
from __future__ import annotations
from typing import Any, Callable, Dict, FrozenSet, List
# Every step this pipeline runs by name that a downstream package may replace.
# Add a name here only alongside the call site's own switch to `run_hook` --
# an entry with no matching `run_hook(...)` call is a name nothing will ever
# look up. `test_every_whitelisted_hook_has_a_call_site` in
# test_resolution_hook_registry.py holds the two directions of this equal by
# construction, so this list cannot drift from `pipeline.py` silently.
#
# `handle_offload_compatibility` runs at two different points in the pipeline
# (once before model-specific adjustments, once after -- see the comments at
# its call sites). An override registered for it applies at both, identically;
# there is no way to target "just the second call" through this mechanism,
# because the name is all `run_hook` has to key on.
_OVERRIDABLE_HOOKS: FrozenSet[str] = frozenset(
{
"handle_mega_moe",
"handle_return_hidden_states_mode",
"handle_media_url_security",
"handle_hicache_ratio_default",
"handle_offload_compatibility",
"validate_prefill_decode_interval",
"default_unset_prefill_decode_interval",
"validate_sampling_mask_max_tokens",
"validate_prefill_cp_platform",
"handle_hardware_runtime_validation",
"handle_model_source_paths",
"handle_multimodal",
"handle_ssl_validation",
"handle_asr_validation",
"handle_deprecated_args",
"handle_prefill_delayer_env_compat",
"handle_missing_default_values",
"handle_expert_pack",
"handle_pd_disaggregation",
"validate_prefill_only_disable_kv_cache_args",
"handle_decode_context_parallelism",
"apply_inkling_prefill_cuda_graph_default",
"apply_muse_glimmer_prefill_cuda_graph_max_bs_default",
"handle_dwdp",
"handle_cuda_graph_config",
"apply_glm5_chunked_prefill_default",
"handle_hpu_backends",
"handle_cpu_backends",
"handle_npu_backends",
"handle_mps_backends",
"handle_xpu_backends",
"handle_symm_mem_device_support",
"handle_platform_defaults",
"handle_gpu_memory_settings",
"handle_model_specific_adjustments",
"handle_deterministic_inference",
"handle_attention_backend_compatibility",
"disable_prefill_cuda_graph_for_deepseek_trtllm_mla",
"handle_mamba_backend",
"handle_int8_mamba_checkpoint",
"handle_linear_attn_backend",
"apply_glm5_prefill_cuda_graph_policy",
"handle_kv4_compatibility",
"handle_mxfp8_kv_cache_compatibility",
"handle_amd_specifics",
"handle_nccl_pre_warm",
"handle_grammar_backend",
"handle_multi_item_scoring",
"handle_prefill_only_disable_kv_cache",
"handle_hicache",
"handle_data_parallelism",
"handle_load_balance_method",
"handle_context_parallelism",
"handle_moe_kernel_config",
"handle_a2a_moe",
"handle_eplb_and_dispatch",
"handle_expert_distribution_metrics",
"handle_elastic_ep",
"validate_experimental_sgl_marlin",
"handle_speculative_decoding",
"handle_layernorm_sp",
"validate_cutedsl_a2a_token_budget",
"handle_load_format",
"handle_encoder_disaggregation",
"handle_tokenizer_batching",
"handle_environment_variables",
"handle_cache_compatibility",
"handle_page_major_kv_layout",
"handle_unified_memory_pool",
"handle_dllm_inference",
"handle_crash_dump_env",
"handle_debug_utils",
"handle_other_validations",
"handle_model_capability_adjustments",
"validate_deepep_v2_speculative_draft",
"validate_deepep_v2_dispatch_token_budget",
}
)
# name -> registered overrides, oldest first. Each takes `(server_args,
# previous)`, where `previous` is the callable it wraps -- the built-in on
# the first registration, the previous registrant's own wrapper on every one
# after. Process-global as a `dict[str, list]` so a test isolates it the way
# test_model_overrides.py isolates `_MODEL_OVERRIDE_FNS`:
# `patch.dict(..., clear=True)`.
_HOOKS: Dict[str, List[Callable[[Any, Callable[[Any], None]], None]]] = {}
def register_resolution_hook(name: str):
"""Replace (or wrap) the pipeline step named ``name``.
The decorated function is called as ``fn(server_args, previous)``.
``previous`` is a plain ``server_args -> None`` callable: the built-in
step on the first registration for this name, or the previous
registrant's own wrapper on every registration after that. Call it to
run what would have run without this override -- the `super().handle_x()`
shape, expressed as an explicit argument instead of a method-resolution
lookup, because there is no class hierarchy here for `super()` to walk.
Not calling it is a full replacement.
Registering twice for the same name does not replace the first
registration; it wraps it. The **last** registration is outermost --
runs first, and decides whether/when its `previous` (everything
registered before it, down to the built-in) runs at all. Two downstream
packages that both target the same name compose in whichever order they
happened to import in; if that order matters to you, make one of them
import the other first.
This changes *what* runs at the step's existing position in the
pipeline, never *when*: the call site in `pipeline.py` is unmoved, so
every other step keeps the order it already had. A wrapped step's own
declarations reach `resolution_result` the same way any declaration
does -- only readers from this position onward see them; a step that
already ran and read the old value before this one's `previous` (or the
built-in) declared its replacement has already made its decision on it.
"""
if name not in _OVERRIDABLE_HOOKS:
raise ValueError(
f"{name!r} is not an overridable resolution hook; the "
f"overridable set is {sorted(_OVERRIDABLE_HOOKS)}. A new entry "
"needs a matching `run_hook(...)` call at the step's site in "
"pipeline.py, not just a name here."
)
def decorator(fn):
_HOOKS.setdefault(name, []).append(fn)
return fn
return decorator
def run_hook(builtin: Callable[[Any], None], server_args: Any) -> None:
"""Run ``builtin`` -- the registered chain if anything overrode it under
its name, ``builtin`` directly otherwise.
The name is ``builtin.__name__``, not a second argument: the call site
already has the function in scope (a function-local import, same as
every other step), and spelling the name out again next to it is the
exact "two copies that can silently disagree" shape this project keeps
removing elsewhere. ``builtin`` must therefore be a plain, named
function -- every real call site is -- not a lambda or a bound method.
Called from the step's fixed position in `run_resolution_pipeline`. The
chain is rebuilt from the registry on every call rather than cached at
registration time, because at registration time (import time, before any
`ServerArgs` exists) there is no `server_args` yet and the first
registrant's `previous` cannot be bound to anything real until a call
actually happens.
"""
step = builtin
for fn in _HOOKS.get(builtin.__name__, ()):
step = _bind(fn, step)
step(server_args)
def _bind(fn, previous):
def wrapped(server_args):
fn(server_args, previous)
return wrapped