Skip to content

Commit f9e770c

Browse files
committed
Fix: RecursionError 2
1 parent 1c4f2f1 commit f9e770c

2 files changed

Lines changed: 24 additions & 29 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "cli-speeder"
7-
version = "0.1.1"
7+
version = "0.1.2"
88
description = "A dead-simple, zero-dependency lazy import proxy to make Python CLIs start instantly."
99
readme = "README.md"
1010
requires-python = ">=3.8"

src/cli_speeder/core.py

Lines changed: 23 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import importlib
22
import importlib.util
3-
import importlib.abc
3+
import importlib.abc
44
import sys
55
import os
6-
from typing import Any, Callable, Optional, TypeVar, Generic, List
7-
from contextlib import contextmanager
6+
import threading
7+
from typing import Any, Callable, Optional, TypeVar, Generic, List, Set
88

99
_EAGER_MODE = os.environ.get("CLI_SPEEDER_EAGER", "0") == "1"
1010

@@ -88,59 +88,61 @@ def lazy_import(name: str, package: Optional[str] = None) -> Any:
8888
def lazy_from_import(module_name: str, class_name: str, package: Optional[str] = None) -> Any:
8989
return LazyObjectProxy(module_name, class_name, package)
9090

91+
92+
9193
class _LazyFinder(importlib.abc.MetaPathFinder):
9294
"""
9395
A custom importer that intercepts specific module names and
94-
returns a LazyLoader instead of loading them immediately.
96+
returns a LazyLoader.
97+
98+
Uses thread-local storage to prevent recursion during delegation.
9599
"""
96100
def __init__(self, names: List[str]):
97101
self.names = set(names)
98-
self._skip = set() # To prevent recursion
102+
self._local = threading.local()
99103

100104
def find_spec(self, fullname, path, target=None):
101-
# Check if we should intercept this module
105+
# RECURSION CHECK:
106+
# If we are already inside a find_spec call on this thread, ignore this call.
107+
if getattr(self._local, "in_lookup", False):
108+
return None
109+
102110
# We only care if the TOP LEVEL package matches our list
103111
root_pkg = fullname.split(".")[0]
104112
if root_pkg not in self.names:
105113
return None
106-
107-
if fullname in self._skip:
108-
return None
109114

110-
# Prevent recursion: Mark this module as "being looked up"
111-
self._skip.add(fullname)
112-
115+
# DELEGATION (With Guard):
113116
try:
114-
# Find the *real* spec using other finders
115-
# IMPORTANT: We iterate sys.meta_path manually to skip ourselves
117+
self._local.in_lookup = True # LOCK
118+
119+
# Iterate over other finders manually
116120
spec = None
117121
for finder in sys.meta_path:
118122
if finder is self:
119123
continue
120124
try:
121-
# Check if finder has find_spec (some legacy ones don't)
122125
if hasattr(finder, "find_spec"):
123126
spec = finder.find_spec(fullname, path, target)
124127
if spec:
125128
break
126-
except ImportError:
129+
except (ImportError, AttributeError):
127130
continue
128131

129132
if spec is None:
130133
return None
131134

132-
# MAGIC: Wrap the real loader in a LazyLoader
133-
# Only wrap if it has a loader
135+
# WRAP IN LAZY LOADER
136+
# Only wrap if it has a loader and isn't already lazy
134137
if spec.loader and not isinstance(spec.loader, importlib.util.LazyLoader):
135138
spec.loader = importlib.util.LazyLoader(spec.loader)
136139

137140
return spec
138141

139142
finally:
140-
# Always clear the recursion guard
141-
self._skip.discard(fullname)
143+
self._local.in_lookup = False # UNLOCK
142144

143-
# Global reference to avoid adding multiple finders
145+
# Global reference
144146
_INSTALLED_FINDER = None
145147

146148
def speed_up_modules(modules: List[str]):
@@ -152,20 +154,13 @@ def speed_up_modules(modules: List[str]):
152154
"""
153155
global _INSTALLED_FINDER
154156

155-
# Remove numpy/torch from the list if the user accidentally added them
156-
# (Safety mechanism for V2)
157157
unsafe = {"numpy", "torch", "pydantic"}
158158
safe_modules = [m for m in modules if m not in unsafe]
159159

160-
if len(safe_modules) != len(modules):
161-
# We could warn here, but silent safety is better for CLIs
162-
pass
163-
164160
if _INSTALLED_FINDER is None:
165161
_INSTALLED_FINDER = _LazyFinder(safe_modules)
166162
sys.meta_path.insert(0, _INSTALLED_FINDER)
167163
else:
168-
# Just update the existing list
169164
_INSTALLED_FINDER.names.update(safe_modules)
170165

171166

0 commit comments

Comments
 (0)