Coverage for src/lektor_ng/context.py: 83%
149 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-08-03 16:04 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-08-03 16:04 +0000
1from contextlib import contextmanager
3from jinja2 import Undefined
4from werkzeug.local import LocalProxy, LocalStack
6from lektor_ng.reporter import reporter
8_ctx_stack = LocalStack()
11def url_to(*args, **kwargs):
12 """Calculates a URL to another record."""
13 ctx = get_ctx()
14 if ctx is None:
15 raise RuntimeError("No context found")
16 return ctx.url_to(*args, **kwargs)
19def get_asset_url(asset):
20 """Calculates the asset URL relative to the current record."""
21 ctx = get_ctx()
22 if ctx is None:
23 raise RuntimeError("No context found")
24 asset = ctx.pad.get_asset(asset)
25 if asset is None:
26 return Undefined("Asset not found")
27 return ctx.get_asset_url(asset)
30@LocalProxy
31def site_proxy():
32 """Returns the current pad."""
33 ctx = get_ctx()
34 if ctx is None:
35 return Undefined(hint="Cannot access the site from here", name="site")
36 return ctx.pad
39@LocalProxy
40def config_proxy():
41 """Returns the current config."""
42 return site_proxy.db.config
45def get_ctx():
46 """Returns the current context."""
47 return _ctx_stack.top
50def get_locale(default="en_US"):
51 """Returns the current locale."""
52 ctx = get_ctx()
53 if ctx is not None:
54 rv = ctx.locale
55 if rv is not None:
56 return rv
57 return ctx.pad.db.config.site_locale
58 return default
61class Context:
62 """The context is a thread local object that provides the system with
63 general information about in which state it is. The context is created
64 whenever a source is processed and can be accessed by template engine and
65 other things.
67 It's considered read and write and also accumulates changes that happen
68 during processing of the object.
69 """
71 def __init__(self, artifact=None, pad=None):
72 if pad is None:
73 if artifact is None:
74 raise TypeError("Either artifact or pad is needed to construct a context.")
75 pad = artifact.build_state.pad
77 if artifact is not None:
78 self.artifact = artifact
79 self.source = artifact.source_obj
80 self.build_state = self.artifact.build_state
81 else:
82 self.artifact = None
83 self.source = None
84 self.build_state = None
86 self.exc_info = None
88 self.pad = pad
90 # Processing information
91 self.referenced_dependencies = set()
92 self.referenced_virtual_dependencies = set()
93 self.sub_artifacts = []
95 self.flow_block_render_stack = []
97 self._forced_base_url = None
98 self._resolving_url = False
100 # General cache system where other things can put their temporary
101 # stuff in.
102 self.cache = {}
104 self._dependency_collectors = []
106 @property
107 def env(self):
108 """The environment of the context."""
109 return self.pad.db.env
111 @property
112 def record(self):
113 """If the source is a record it will be available here."""
114 rv = self.source
115 if rv is not None and rv.source_classification == "record":
116 return rv
117 return None
119 @property
120 def locale(self):
121 """Returns the current locale if it's available, otherwise `None`.
122 This does not fall back to the site locale.
123 """
124 source = self.source
125 if source is not None:
126 alt_cfg = self.pad.db.config["ALTERNATIVES"].get(source.alt)
127 if alt_cfg:
128 return alt_cfg["locale"]
129 return None
131 def push(self):
132 _ctx_stack.push(self)
134 @staticmethod
135 def pop():
136 _ctx_stack.pop()
138 def __enter__(self):
139 self.push()
140 return self
142 def __exit__(self, exc_type, exc_value, tb):
143 self.pop()
145 @property
146 def base_url(self):
147 """The URL path for the current context."""
148 if self._forced_base_url:
149 return self._forced_base_url
150 if self.source is not None:
151 return self.source.url_path
152 return "/"
154 def url_to(
155 self,
156 path,
157 alt=None,
158 absolute=None,
159 external=None,
160 resolve=None,
161 strict_resolve=None,
162 ):
163 """Returns a URL to another path."""
164 if self.source is None:
165 raise RuntimeError("Can only generate paths to other pages if the context has a source document set.")
166 return self.source.url_to(
167 path,
168 alt=alt,
169 base_url=self.base_url,
170 absolute=absolute,
171 external=external,
172 resolve=resolve,
173 strict_resolve=strict_resolve,
174 )
176 def get_asset_url(self, asset):
177 """Calculates the asset URL relative to the current record."""
178 if self.source is None:
179 raise RuntimeError("Can only generate paths to assets if the context has a source document set.")
180 asset_url = self.source.url_to("!" + asset.url_path)
181 info = self.build_state.get_file_info(asset.source_filename)
182 self.record_dependency(asset.source_filename)
183 return f"{asset_url}?h={info.checksum[:8]}"
185 def sub_artifact(self, *args, **kwargs):
186 """Decorator version of :func:`add_sub_artifact`."""
188 def decorator(f):
189 self.add_sub_artifact(*args, build_func=f, **kwargs)
190 return f
192 return decorator
194 def add_sub_artifact(
195 self,
196 artifact_name,
197 build_func=None,
198 sources=None,
199 source_obj=None,
200 config_hash=None,
201 ):
202 """Sometimes it can happen that while building an artifact another
203 artifact needs building. This function is generally used to record
204 this request.
205 """
206 if self.build_state is None:
207 raise TypeError(
208 "The context does not have a build state which means that artifact declaration is not possible."
209 )
210 aft = self.build_state.new_artifact(
211 artifact_name=artifact_name,
212 sources=sources,
213 source_obj=source_obj,
214 config_hash=config_hash,
215 )
216 self.sub_artifacts.append((aft, build_func))
217 reporter.report_sub_artifact(aft)
219 def record_dependency(self, filename, affects_url=None):
220 """Records a dependency from processing.
222 If ``affects_url`` is set to ``False`` the dependency will be ignored if
223 we are in the process of resolving a URL.
224 """
225 if self._resolving_url and affects_url is False:
226 return
227 self.referenced_dependencies.add(filename)
228 for coll in self._dependency_collectors:
229 coll(filename)
231 def record_virtual_dependency(self, virtual_source):
232 """Records a dependency from processing."""
233 self.referenced_virtual_dependencies.add(virtual_source)
234 for coll in self._dependency_collectors:
235 coll(virtual_source)
237 @contextmanager
238 def gather_dependencies(self, func):
239 """For the duration of the `with` block the provided function will be
240 invoked for all dependencies encountered.
241 """
242 self._dependency_collectors.append(func)
243 try:
244 yield
245 finally:
246 self._dependency_collectors.pop()
248 @contextmanager
249 def changed_base_url(self, value):
250 """Temporarily overrides the URL path of the context."""
251 old = self._forced_base_url
252 self._forced_base_url = value
253 try:
254 yield
255 finally:
256 self._forced_base_url = old
259@contextmanager
260def ignore_url_unaffecting_dependencies(value=True):
261 """Ignore dependencies which do not affect URL resolution within context."""
262 ctx = get_ctx()
263 if ctx is not None:
264 old = ctx._resolving_url
265 ctx._resolving_url = value
266 try:
267 yield
268 finally:
269 if ctx is not None:
270 ctx._resolving_url = old