sphinxcontrib-screenshot 0.1.4__py3-none-any.whl → 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of sphinxcontrib-screenshot might be problematic. Click here for more details.
- sphinxcontrib/screenshot/__init__.py +552 -0
- sphinxcontrib/screenshot/static/screenshot-theme.css +12 -0
- {sphinxcontrib_screenshot-0.1.4.dist-info → sphinxcontrib_screenshot-0.3.0.dist-info}/METADATA +8 -6
- sphinxcontrib_screenshot-0.3.0.dist-info/RECORD +7 -0
- {sphinxcontrib_screenshot-0.1.4.dist-info → sphinxcontrib_screenshot-0.3.0.dist-info}/WHEEL +1 -1
- sphinxcontrib/screenshot.py +0 -345
- sphinxcontrib_screenshot-0.1.4.dist-info/RECORD +0 -6
- {sphinxcontrib_screenshot-0.1.4.dist-info → sphinxcontrib_screenshot-0.3.0.dist-info/licenses}/LICENSE +0 -0
- {sphinxcontrib_screenshot-0.1.4.dist-info → sphinxcontrib_screenshot-0.3.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,552 @@
|
|
|
1
|
+
# Copyright 2023 Google LLC
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
import hashlib
|
|
16
|
+
import importlib
|
|
17
|
+
import importlib.metadata
|
|
18
|
+
import os
|
|
19
|
+
import threading
|
|
20
|
+
import typing
|
|
21
|
+
import wsgiref.simple_server
|
|
22
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from urllib.parse import urlparse
|
|
25
|
+
|
|
26
|
+
from docutils import nodes
|
|
27
|
+
from docutils.parsers.rst import directives
|
|
28
|
+
from docutils.parsers.rst.directives.images import Figure
|
|
29
|
+
from playwright._impl._helper import ColorScheme
|
|
30
|
+
from playwright.sync_api import Browser, BrowserContext
|
|
31
|
+
from playwright.sync_api import TimeoutError as PlaywrightTimeoutError
|
|
32
|
+
from playwright.sync_api import sync_playwright
|
|
33
|
+
from portpicker import pick_unused_port
|
|
34
|
+
from sphinx.application import Sphinx
|
|
35
|
+
from sphinx.config import Config
|
|
36
|
+
from sphinx.util import logging as sphinx_logging
|
|
37
|
+
from sphinx.util.docutils import SphinxDirective
|
|
38
|
+
from sphinx.util.fileutil import copy_asset
|
|
39
|
+
|
|
40
|
+
logger = sphinx_logging.getLogger(__name__)
|
|
41
|
+
|
|
42
|
+
Meta = typing.TypedDict('Meta', {
|
|
43
|
+
'version': str,
|
|
44
|
+
'parallel_read_safe': bool,
|
|
45
|
+
'parallel_write_safe': bool
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
ContextBuilder = typing.Optional[typing.Callable[[Browser, str, str],
|
|
49
|
+
BrowserContext]]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def parse_expected_status_codes(codes_str: str) -> typing.List[int]:
|
|
53
|
+
"""Parse a comma-separated string of HTTP status codes into a list.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
codes_str: Comma-separated status codes like "200, 201, 302".
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
List of integer status codes.
|
|
60
|
+
"""
|
|
61
|
+
return [int(code.strip()) for code in codes_str.split(',')]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class ScreenshotDirective(SphinxDirective, Figure):
|
|
65
|
+
"""Sphinx Screenshot Dirctive.
|
|
66
|
+
|
|
67
|
+
This directive embeds a screenshot of a webpage.
|
|
68
|
+
|
|
69
|
+
# Example
|
|
70
|
+
|
|
71
|
+
You can simply pass a URL for a webpage that you want to take a screenshot.
|
|
72
|
+
|
|
73
|
+
```rst
|
|
74
|
+
.. screenshot:: http://www.example.com
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
You can also specify the screen size for the screenshot with
|
|
78
|
+
`viewport-width` and `viewport-height` parameters in pixel.
|
|
79
|
+
|
|
80
|
+
```rst
|
|
81
|
+
.. screenshot:: http://www.example.com
|
|
82
|
+
:viewport-width: 1280
|
|
83
|
+
:viewport-height: 960
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
You can describe the interaction that you want to have with the webpage
|
|
87
|
+
before taking a screenshot in JavaScript.
|
|
88
|
+
|
|
89
|
+
```rst
|
|
90
|
+
.. screenshot:: http://www.example.com
|
|
91
|
+
|
|
92
|
+
document.querySelector('button').click();
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
It also generates a PDF file when `pdf` option is given, which might be
|
|
96
|
+
useful when you need scalable image assets.
|
|
97
|
+
|
|
98
|
+
```rst
|
|
99
|
+
.. screenshot:: http://www.example.com
|
|
100
|
+
:pdf:
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
You can automatically generate both light and dark mode screenshots by
|
|
104
|
+
setting `:color-scheme: auto`. This creates two screenshots with appropriate
|
|
105
|
+
CSS classes (`only-light` and `only-dark`) that are automatically shown
|
|
106
|
+
based on the user's theme preference.
|
|
107
|
+
|
|
108
|
+
```rst
|
|
109
|
+
.. screenshot:: http://www.example.com
|
|
110
|
+
:color-scheme: auto
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
You can take a screenshot of a local file using a root-relative path.
|
|
114
|
+
|
|
115
|
+
```rst
|
|
116
|
+
.. screenshot:: /static/example.html
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Or you can use a document-relative path.
|
|
120
|
+
|
|
121
|
+
```rst
|
|
122
|
+
.. screenshot:: ./example.html
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
The `file://` protocol is also supported.
|
|
126
|
+
|
|
127
|
+
```rst
|
|
128
|
+
.. screenshot:: file:///path/to/your/file.html
|
|
129
|
+
```
|
|
130
|
+
"""
|
|
131
|
+
|
|
132
|
+
required_arguments = 1 # URL
|
|
133
|
+
option_spec = {
|
|
134
|
+
**(Figure.option_spec or {}),
|
|
135
|
+
'browser': str,
|
|
136
|
+
'viewport-height': directives.positive_int,
|
|
137
|
+
'viewport-width': directives.positive_int,
|
|
138
|
+
'interactions': str,
|
|
139
|
+
'pdf': directives.flag,
|
|
140
|
+
'color-scheme': str,
|
|
141
|
+
'full-page': directives.flag,
|
|
142
|
+
'context': str,
|
|
143
|
+
'headers': directives.unchanged,
|
|
144
|
+
'locale': str,
|
|
145
|
+
'timezone': str,
|
|
146
|
+
'device-scale-factor': directives.positive_int,
|
|
147
|
+
'status-code': str,
|
|
148
|
+
}
|
|
149
|
+
pool = ThreadPoolExecutor()
|
|
150
|
+
|
|
151
|
+
@staticmethod
|
|
152
|
+
def take_screenshot(url: str,
|
|
153
|
+
browser_name: str,
|
|
154
|
+
viewport_width: int,
|
|
155
|
+
viewport_height: int,
|
|
156
|
+
filepath: str,
|
|
157
|
+
init_script: str,
|
|
158
|
+
interactions: str,
|
|
159
|
+
generate_pdf: bool,
|
|
160
|
+
color_scheme: ColorScheme,
|
|
161
|
+
full_page: bool,
|
|
162
|
+
context_builder: ContextBuilder,
|
|
163
|
+
headers: dict,
|
|
164
|
+
device_scale_factor: int,
|
|
165
|
+
locale: typing.Optional[str],
|
|
166
|
+
timezone: typing.Optional[str],
|
|
167
|
+
expected_status_codes: typing.Optional[str] = None,
|
|
168
|
+
location: typing.Optional[str] = None):
|
|
169
|
+
"""Takes a screenshot with Playwright's Chromium browser.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
url (str): The HTTP/HTTPS URL of the webpage to screenshot.
|
|
173
|
+
browser_name (str): Browser to use ('chromium', 'firefox' or 'webkit').
|
|
174
|
+
viewport_width (int): The width of the screenshot in pixels.
|
|
175
|
+
viewport_height (int): The height of the screenshot in pixels.
|
|
176
|
+
filepath (str): The path to save the screenshot to.
|
|
177
|
+
init_script (str): JavaScript code to be evaluated after the document
|
|
178
|
+
was created but before any of its scripts were run. See more details at
|
|
179
|
+
https://playwright.dev/python/docs/api/class-page#page-add-init-script
|
|
180
|
+
interactions (str): JavaScript code to run before taking the screenshot
|
|
181
|
+
after the page was loaded.
|
|
182
|
+
generate_pdf (bool): Generate a PDF file along with the screenshot.
|
|
183
|
+
color_scheme (str): The preferred color scheme. Can be 'light' or 'dark'.
|
|
184
|
+
full_page (bool): Take a full page screenshot.
|
|
185
|
+
context: A method to build the Playwright context.
|
|
186
|
+
headers (dict): Custom request header.
|
|
187
|
+
device_scale_factor (int): The device scale factor for the screenshot.
|
|
188
|
+
This can be thought of as DPR (device pixel ratio).
|
|
189
|
+
locale (str, optional): User locale for the request.
|
|
190
|
+
timezone (str, optional): User timezone for the request.
|
|
191
|
+
expected_status_codes (str, optional): Expected HTTP status codes.
|
|
192
|
+
Format: comma-separated list of codes (e.g., "200,201,302").
|
|
193
|
+
Defaults to "200,302" (OK and redirect).
|
|
194
|
+
location (str, optional): Document location for warning messages.
|
|
195
|
+
"""
|
|
196
|
+
if expected_status_codes is None:
|
|
197
|
+
expected_status_codes = "200,302"
|
|
198
|
+
|
|
199
|
+
valid_codes = parse_expected_status_codes(expected_status_codes)
|
|
200
|
+
|
|
201
|
+
with sync_playwright() as playwright:
|
|
202
|
+
browser: Browser = getattr(playwright, browser_name).launch()
|
|
203
|
+
|
|
204
|
+
if context_builder:
|
|
205
|
+
try:
|
|
206
|
+
context = context_builder(browser, url, color_scheme)
|
|
207
|
+
except PlaywrightTimeoutError:
|
|
208
|
+
raise RuntimeError(
|
|
209
|
+
'Timeout error occured at %s in executing py init script %s' %
|
|
210
|
+
(url, context_builder.__name__))
|
|
211
|
+
else:
|
|
212
|
+
context = browser.new_context(
|
|
213
|
+
color_scheme=color_scheme,
|
|
214
|
+
locale=locale,
|
|
215
|
+
timezone_id=timezone,
|
|
216
|
+
device_scale_factor=device_scale_factor)
|
|
217
|
+
|
|
218
|
+
page = context.new_page()
|
|
219
|
+
page.set_default_timeout(10000)
|
|
220
|
+
page.set_viewport_size({
|
|
221
|
+
'width': viewport_width,
|
|
222
|
+
'height': viewport_height
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
try:
|
|
226
|
+
if init_script:
|
|
227
|
+
page.add_init_script(init_script)
|
|
228
|
+
page.set_extra_http_headers(headers)
|
|
229
|
+
response = page.goto(url)
|
|
230
|
+
|
|
231
|
+
if response and response.status not in valid_codes:
|
|
232
|
+
logger.warning(
|
|
233
|
+
f'Page {url} returned status code {response.status}, '
|
|
234
|
+
f'expected one of: {expected_status_codes}',
|
|
235
|
+
type='screenshot',
|
|
236
|
+
subtype='status_code',
|
|
237
|
+
location=location)
|
|
238
|
+
|
|
239
|
+
page.wait_for_load_state('networkidle')
|
|
240
|
+
|
|
241
|
+
# Execute interactions
|
|
242
|
+
if interactions:
|
|
243
|
+
page.evaluate(interactions)
|
|
244
|
+
page.wait_for_load_state('networkidle')
|
|
245
|
+
except PlaywrightTimeoutError:
|
|
246
|
+
raise RuntimeError('Timeout error occured at %s in executing\n%s' %
|
|
247
|
+
(url, interactions))
|
|
248
|
+
page.screenshot(path=filepath, full_page=full_page)
|
|
249
|
+
if generate_pdf:
|
|
250
|
+
page.emulate_media(media='screen')
|
|
251
|
+
root, ext = os.path.splitext(filepath)
|
|
252
|
+
page.pdf(
|
|
253
|
+
width=f'{viewport_width}px',
|
|
254
|
+
height=f'{viewport_height}px',
|
|
255
|
+
path=root + '.pdf')
|
|
256
|
+
page.close()
|
|
257
|
+
browser.close()
|
|
258
|
+
|
|
259
|
+
def evaluate_substitutions(self, text: str) -> str:
|
|
260
|
+
substitutions = self.state.document.substitution_defs
|
|
261
|
+
for key, value in substitutions.items():
|
|
262
|
+
text = text.replace(f"|{key}|", value.astext())
|
|
263
|
+
return text
|
|
264
|
+
|
|
265
|
+
def _add_css_class_to_nodes(self, nodes_list: typing.Sequence[nodes.Node],
|
|
266
|
+
css_class: str) -> typing.Sequence[nodes.Node]:
|
|
267
|
+
"""Add a CSS class to image or figure nodes.
|
|
268
|
+
|
|
269
|
+
Args:
|
|
270
|
+
nodes_list: List of docutils nodes to modify.
|
|
271
|
+
css_class: CSS class name to add (e.g., 'only-light' or 'only-dark').
|
|
272
|
+
|
|
273
|
+
Returns:
|
|
274
|
+
The modified list of nodes.
|
|
275
|
+
"""
|
|
276
|
+
for node in nodes_list:
|
|
277
|
+
if isinstance(node, (nodes.image, nodes.figure)):
|
|
278
|
+
existing_classes: list = node.get('classes', [])
|
|
279
|
+
node['classes'] = existing_classes + [css_class]
|
|
280
|
+
if isinstance(node, nodes.figure):
|
|
281
|
+
for child in node.children:
|
|
282
|
+
if isinstance(child, nodes.image):
|
|
283
|
+
existing_classes = child.get('classes', [])
|
|
284
|
+
child['classes'] = existing_classes + [css_class]
|
|
285
|
+
return nodes_list
|
|
286
|
+
|
|
287
|
+
def _generate_single_screenshot(
|
|
288
|
+
self,
|
|
289
|
+
color_scheme: typing.Optional[str] = None
|
|
290
|
+
) -> typing.Sequence[nodes.Node]:
|
|
291
|
+
"""Generate a single screenshot and return the docutils nodes.
|
|
292
|
+
|
|
293
|
+
Args:
|
|
294
|
+
color_scheme: Optional color scheme to override the directive
|
|
295
|
+
option. Used when generating dual-theme screenshots.
|
|
296
|
+
|
|
297
|
+
Returns:
|
|
298
|
+
List of docutils nodes representing the screenshot.
|
|
299
|
+
"""
|
|
300
|
+
screenshot_init_script: str = self.env.config.screenshot_init_script or ''
|
|
301
|
+
docdir = os.path.dirname(self.env.doc2path(self.env.docname))
|
|
302
|
+
|
|
303
|
+
# Ensure the screenshots directory exists
|
|
304
|
+
ss_dirpath = os.path.join(self.env.app.outdir, '_static', 'screenshots')
|
|
305
|
+
os.makedirs(ss_dirpath, exist_ok=True)
|
|
306
|
+
|
|
307
|
+
raw_path = self.arguments[0]
|
|
308
|
+
url_or_filepath = self.evaluate_substitutions(raw_path)
|
|
309
|
+
|
|
310
|
+
# Check if the path is a local file path.
|
|
311
|
+
if urlparse(url_or_filepath).scheme == '':
|
|
312
|
+
# root-relative path
|
|
313
|
+
if url_or_filepath.startswith('/'):
|
|
314
|
+
url_or_filepath = os.path.join(self.env.srcdir,
|
|
315
|
+
url_or_filepath.lstrip('/'))
|
|
316
|
+
# document-relative path
|
|
317
|
+
else:
|
|
318
|
+
url_or_filepath = os.path.join(docdir, url_or_filepath)
|
|
319
|
+
url_or_filepath = "file://" + os.path.normpath(url_or_filepath)
|
|
320
|
+
|
|
321
|
+
if urlparse(url_or_filepath).scheme not in {'http', 'https', 'file'}:
|
|
322
|
+
raise RuntimeError(
|
|
323
|
+
f'Invalid URL: {url_or_filepath}. ' +
|
|
324
|
+
'Only HTTP/HTTPS/FILE URLs or root/document-relative file paths ' +
|
|
325
|
+
'are supported.')
|
|
326
|
+
|
|
327
|
+
interactions = self.options.get('interactions', '')
|
|
328
|
+
browser = self.options.get('browser',
|
|
329
|
+
self.env.config.screenshot_default_browser)
|
|
330
|
+
viewport_height = self.options.get(
|
|
331
|
+
'viewport-height', self.env.config.screenshot_default_viewport_height)
|
|
332
|
+
viewport_width = self.options.get(
|
|
333
|
+
'viewport-width', self.env.config.screenshot_default_viewport_width)
|
|
334
|
+
color_scheme = color_scheme or self.options.get(
|
|
335
|
+
'color-scheme', self.env.config.screenshot_default_color_scheme)
|
|
336
|
+
pdf = 'pdf' in self.options
|
|
337
|
+
full_page = ('full-page' in self.options or
|
|
338
|
+
self.env.config.screenshot_default_full_page)
|
|
339
|
+
locale = self.options.get('locale',
|
|
340
|
+
self.env.config.screenshot_default_locale)
|
|
341
|
+
timezone = self.options.get('timezone',
|
|
342
|
+
self.env.config.screenshot_default_timezone)
|
|
343
|
+
context = self.options.get('context', '')
|
|
344
|
+
headers = self.options.get('headers', '')
|
|
345
|
+
device_scale_factor = self.options.get(
|
|
346
|
+
'device-scale-factor',
|
|
347
|
+
self.env.config.screenshot_default_device_scale_factor)
|
|
348
|
+
status_code = self.options.get('status-code', None)
|
|
349
|
+
request_headers = {**self.env.config.screenshot_default_headers}
|
|
350
|
+
if headers:
|
|
351
|
+
for header in headers.strip().split("\n"):
|
|
352
|
+
name, value = header.split(" ", 1)
|
|
353
|
+
request_headers[name] = value
|
|
354
|
+
|
|
355
|
+
# Generate filename based on hash of parameters
|
|
356
|
+
hash_input = "_".join([
|
|
357
|
+
raw_path, browser,
|
|
358
|
+
str(viewport_height),
|
|
359
|
+
str(viewport_width), color_scheme, context, interactions,
|
|
360
|
+
str(full_page),
|
|
361
|
+
str(device_scale_factor),
|
|
362
|
+
str(status_code or "")
|
|
363
|
+
])
|
|
364
|
+
filename = hashlib.md5(hash_input.encode()).hexdigest() + '.png'
|
|
365
|
+
filepath = os.path.join(ss_dirpath, filename)
|
|
366
|
+
|
|
367
|
+
if context:
|
|
368
|
+
context_builder_path = self.config.screenshot_contexts[context]
|
|
369
|
+
context_builder = resolve_python_method(context_builder_path)
|
|
370
|
+
else:
|
|
371
|
+
context_builder = None
|
|
372
|
+
|
|
373
|
+
# Check if the file already exists. If not, take a screenshot
|
|
374
|
+
if not os.path.exists(filepath):
|
|
375
|
+
fut = self.pool.submit(ScreenshotDirective.take_screenshot,
|
|
376
|
+
url_or_filepath, browser, viewport_width,
|
|
377
|
+
viewport_height, filepath, screenshot_init_script,
|
|
378
|
+
interactions, pdf,
|
|
379
|
+
typing.cast(ColorScheme, color_scheme), full_page,
|
|
380
|
+
context_builder, request_headers,
|
|
381
|
+
device_scale_factor, locale, timezone,
|
|
382
|
+
status_code, self.env.docname)
|
|
383
|
+
fut.result()
|
|
384
|
+
|
|
385
|
+
rel_ss_dirpath = os.path.relpath(ss_dirpath, start=docdir)
|
|
386
|
+
rel_filepath = os.path.join(rel_ss_dirpath, filename).replace(os.sep, '/')
|
|
387
|
+
|
|
388
|
+
self.arguments[0] = rel_filepath
|
|
389
|
+
return super().run()
|
|
390
|
+
|
|
391
|
+
def _generate_dual_theme_screenshots(self) -> typing.Sequence[nodes.Node]:
|
|
392
|
+
"""Generate two screenshots (light and dark mode) with CSS classes.
|
|
393
|
+
|
|
394
|
+
Returns:
|
|
395
|
+
List of docutils nodes containing both light and dark mode screenshots.
|
|
396
|
+
"""
|
|
397
|
+
original_arguments = self.arguments[:]
|
|
398
|
+
original_options = self.options.copy()
|
|
399
|
+
|
|
400
|
+
light_nodes = self._generate_single_screenshot(color_scheme='light')
|
|
401
|
+
light_nodes = self._add_css_class_to_nodes(light_nodes, 'only-light')
|
|
402
|
+
|
|
403
|
+
self.arguments = original_arguments[:]
|
|
404
|
+
self.options = original_options.copy()
|
|
405
|
+
|
|
406
|
+
dark_nodes = self._generate_single_screenshot(color_scheme='dark')
|
|
407
|
+
dark_nodes = self._add_css_class_to_nodes(dark_nodes, 'only-dark')
|
|
408
|
+
|
|
409
|
+
self.arguments = original_arguments
|
|
410
|
+
self.options = original_options
|
|
411
|
+
|
|
412
|
+
return list(light_nodes) + list(dark_nodes)
|
|
413
|
+
|
|
414
|
+
def run(self) -> typing.Sequence[nodes.Node]:
|
|
415
|
+
"""Process the screenshot directive and generate appropriate nodes.
|
|
416
|
+
|
|
417
|
+
Returns:
|
|
418
|
+
List of docutils nodes. If color-scheme is 'auto', returns two sets
|
|
419
|
+
of nodes (one for light mode, one for dark mode). Otherwise returns
|
|
420
|
+
a single screenshot node.
|
|
421
|
+
"""
|
|
422
|
+
color_scheme = self.options.get(
|
|
423
|
+
'color-scheme', self.env.config.screenshot_default_color_scheme)
|
|
424
|
+
|
|
425
|
+
if color_scheme == 'auto':
|
|
426
|
+
return self._generate_dual_theme_screenshots()
|
|
427
|
+
else:
|
|
428
|
+
return self._generate_single_screenshot()
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
app_threads = {}
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def resolve_python_method(import_path: str):
|
|
435
|
+
module_path, method_name = import_path.split(":")
|
|
436
|
+
module = importlib.import_module(module_path)
|
|
437
|
+
method = getattr(module, method_name)
|
|
438
|
+
return method
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def setup_apps(app: Sphinx, config: Config):
|
|
442
|
+
"""Start the WSGI application threads.
|
|
443
|
+
|
|
444
|
+
A new replacement is created for each WSGI app."""
|
|
445
|
+
for wsgi_app_name, wsgi_app_path in config.screenshot_apps.items():
|
|
446
|
+
port = pick_unused_port()
|
|
447
|
+
config.rst_prolog = (
|
|
448
|
+
config.rst_prolog or
|
|
449
|
+
"") + f"\n.. |{wsgi_app_name}| replace:: http://localhost:{port}\n"
|
|
450
|
+
app_builder = resolve_python_method(wsgi_app_path)
|
|
451
|
+
wsgi_app = app_builder(app)
|
|
452
|
+
httpd = wsgiref.simple_server.make_server("localhost", port, wsgi_app)
|
|
453
|
+
thread = threading.Thread(target=httpd.serve_forever)
|
|
454
|
+
thread.start()
|
|
455
|
+
app_threads[wsgi_app_name] = (httpd, thread)
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def teardown_apps(app: Sphinx, exception: typing.Optional[Exception]):
|
|
459
|
+
"""Shut down the WSGI application threads."""
|
|
460
|
+
for httpd, thread in app_threads.values():
|
|
461
|
+
httpd.shutdown()
|
|
462
|
+
thread.join()
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def copy_static_files(app: Sphinx, exception: typing.Optional[Exception]):
|
|
466
|
+
"""Copy static CSS files from the extension to the build output directory.
|
|
467
|
+
|
|
468
|
+
This function is called during the build-finished event to copy the
|
|
469
|
+
screenshot-theme.css file to the output directory.
|
|
470
|
+
|
|
471
|
+
Args:
|
|
472
|
+
app: The Sphinx application instance.
|
|
473
|
+
exception: Exception that occurred during build, if any.
|
|
474
|
+
"""
|
|
475
|
+
if exception is None and app.builder.format == 'html':
|
|
476
|
+
static_source_dir = Path(__file__).parent / 'static'
|
|
477
|
+
static_dest_dir = Path(app.outdir) / '_static' / 'sphinxcontrib-screenshot'
|
|
478
|
+
static_dest_dir.mkdir(parents=True, exist_ok=True)
|
|
479
|
+
|
|
480
|
+
css_source = str(static_source_dir / 'screenshot-theme.css')
|
|
481
|
+
css_dest = str(static_dest_dir)
|
|
482
|
+
copy_asset(css_source, css_dest)
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
def setup(app: Sphinx) -> Meta:
|
|
486
|
+
app.add_directive('screenshot', ScreenshotDirective)
|
|
487
|
+
app.add_config_value('screenshot_init_script', '', 'env')
|
|
488
|
+
app.add_config_value(
|
|
489
|
+
'screenshot_default_viewport_width',
|
|
490
|
+
1280,
|
|
491
|
+
'env',
|
|
492
|
+
description="The default width for screenshots")
|
|
493
|
+
app.add_config_value(
|
|
494
|
+
'screenshot_default_viewport_height',
|
|
495
|
+
960,
|
|
496
|
+
'env',
|
|
497
|
+
description="The default height for screenshots")
|
|
498
|
+
app.add_config_value(
|
|
499
|
+
'screenshot_default_browser',
|
|
500
|
+
'chromium',
|
|
501
|
+
'env',
|
|
502
|
+
description="The default browser for screenshots")
|
|
503
|
+
app.add_config_value(
|
|
504
|
+
'screenshot_default_full_page',
|
|
505
|
+
False,
|
|
506
|
+
'env',
|
|
507
|
+
description="Whether to take full page screenshots")
|
|
508
|
+
app.add_config_value(
|
|
509
|
+
'screenshot_default_color_scheme',
|
|
510
|
+
'null',
|
|
511
|
+
'env',
|
|
512
|
+
description="The default color scheme for screenshots. " +
|
|
513
|
+
"Use 'auto' to generate both light and dark mode screenshots")
|
|
514
|
+
app.add_config_value(
|
|
515
|
+
'screenshot_contexts', {},
|
|
516
|
+
'env',
|
|
517
|
+
types=[dict[str, str]],
|
|
518
|
+
description="A dict of paths to Playwright context build methods")
|
|
519
|
+
app.add_config_value(
|
|
520
|
+
'screenshot_default_headers', {},
|
|
521
|
+
'env',
|
|
522
|
+
description="The default headers to pass in requests")
|
|
523
|
+
app.add_config_value(
|
|
524
|
+
'screenshot_default_device_scale_factor',
|
|
525
|
+
1,
|
|
526
|
+
'env',
|
|
527
|
+
description="The default device scale factor " +
|
|
528
|
+
"a.k.a. DPR (device pixel ratio)")
|
|
529
|
+
app.add_config_value(
|
|
530
|
+
'screenshot_default_locale',
|
|
531
|
+
None,
|
|
532
|
+
'env',
|
|
533
|
+
description="The default locale in requests")
|
|
534
|
+
app.add_config_value(
|
|
535
|
+
'screenshot_default_timezone',
|
|
536
|
+
None,
|
|
537
|
+
'env',
|
|
538
|
+
description="The default timezone in requests")
|
|
539
|
+
app.add_config_value(
|
|
540
|
+
'screenshot_apps', {},
|
|
541
|
+
'env',
|
|
542
|
+
types=[dict[str, str]],
|
|
543
|
+
description="A dict of WSGI apps")
|
|
544
|
+
app.connect('config-inited', setup_apps)
|
|
545
|
+
app.connect('build-finished', teardown_apps)
|
|
546
|
+
app.connect('build-finished', copy_static_files)
|
|
547
|
+
app.add_css_file('sphinxcontrib-screenshot/screenshot-theme.css')
|
|
548
|
+
return {
|
|
549
|
+
'version': importlib.metadata.version('sphinxcontrib-screenshot'),
|
|
550
|
+
'parallel_read_safe': True,
|
|
551
|
+
'parallel_write_safe': True,
|
|
552
|
+
}
|
{sphinxcontrib_screenshot-0.1.4.dist-info → sphinxcontrib_screenshot-0.3.0.dist-info}/METADATA
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: sphinxcontrib-screenshot
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: A Sphinx extension to embed webpage screenshots.
|
|
5
5
|
Author-email: Shuhei Iitsuka <tushuhei@gmail.com>
|
|
6
6
|
License: Apache-2.0
|
|
@@ -8,12 +8,12 @@ Project-URL: repository, https://github.com/tushuhei/sphinxcontrib-screenshot/
|
|
|
8
8
|
Classifier: Development Status :: 3 - Alpha
|
|
9
9
|
Classifier: Operating System :: OS Independent
|
|
10
10
|
Classifier: License :: OSI Approved :: Apache Software License
|
|
11
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
12
11
|
Classifier: Programming Language :: Python :: 3.10
|
|
13
12
|
Classifier: Programming Language :: Python :: 3.11
|
|
14
13
|
Classifier: Programming Language :: Python :: 3.12
|
|
15
14
|
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
-
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
17
|
Description-Content-Type: text/markdown
|
|
18
18
|
License-File: LICENSE
|
|
19
19
|
Requires-Dist: playwright
|
|
@@ -45,6 +45,7 @@ Provides-Extra: doc
|
|
|
45
45
|
Requires-Dist: myst-parser; extra == "doc"
|
|
46
46
|
Requires-Dist: shibuya; extra == "doc"
|
|
47
47
|
Requires-Dist: sphinx; extra == "doc"
|
|
48
|
+
Dynamic: license-file
|
|
48
49
|
|
|
49
50
|
# sphinxcontrib-screenshot
|
|
50
51
|
|
|
@@ -53,9 +54,10 @@ A Sphinx extension to embed website screenshots.
|
|
|
53
54
|
```rst
|
|
54
55
|
.. screenshot:: http://www.example.com
|
|
55
56
|
:browser: chromium
|
|
56
|
-
:width: 1280
|
|
57
|
-
:height: 960
|
|
57
|
+
:viewport-width: 1280
|
|
58
|
+
:viewport-height: 960
|
|
58
59
|
:color-scheme: dark
|
|
60
|
+
:status-code: 200,302
|
|
59
61
|
```
|
|
60
62
|
|
|
61
63
|
Read more in the [documentation](https://sphinxcontrib-screenshot.readthedocs.io).
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
sphinxcontrib/screenshot/__init__.py,sha256=mEfCLA41_698Bn46AtBBi0aBNKdcQUDOU3PlkxqitV4,19806
|
|
2
|
+
sphinxcontrib/screenshot/static/screenshot-theme.css,sha256=c2coCn_dBSK5rzuESy2t_qdnzjZVawe5qcAUqhPgjEI,170
|
|
3
|
+
sphinxcontrib_screenshot-0.3.0.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
|
|
4
|
+
sphinxcontrib_screenshot-0.3.0.dist-info/METADATA,sha256=3m89bDVOcIqto22KJedNE_IguAc6LA8WOUOk4XaFbfU,2916
|
|
5
|
+
sphinxcontrib_screenshot-0.3.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
6
|
+
sphinxcontrib_screenshot-0.3.0.dist-info/top_level.txt,sha256=VJrV3_vaiKQVgVpR0I1iecxoO0drzGu-M0j40PVP2QQ,14
|
|
7
|
+
sphinxcontrib_screenshot-0.3.0.dist-info/RECORD,,
|
sphinxcontrib/screenshot.py
DELETED
|
@@ -1,345 +0,0 @@
|
|
|
1
|
-
# Copyright 2023 Google LLC
|
|
2
|
-
#
|
|
3
|
-
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
-
# you may not use this file except in compliance with the License.
|
|
5
|
-
# You may obtain a copy of the License at
|
|
6
|
-
#
|
|
7
|
-
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
-
#
|
|
9
|
-
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
-
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
-
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
-
# See the License for the specific language governing permissions and
|
|
13
|
-
# limitations under the License.
|
|
14
|
-
|
|
15
|
-
import hashlib
|
|
16
|
-
import importlib
|
|
17
|
-
import importlib.metadata
|
|
18
|
-
import os
|
|
19
|
-
import threading
|
|
20
|
-
import typing
|
|
21
|
-
import wsgiref.simple_server
|
|
22
|
-
from concurrent.futures import ThreadPoolExecutor
|
|
23
|
-
from urllib.parse import urlparse
|
|
24
|
-
|
|
25
|
-
from docutils import nodes
|
|
26
|
-
from docutils.parsers.rst import directives
|
|
27
|
-
from docutils.statemachine import ViewList
|
|
28
|
-
from playwright._impl._helper import ColorScheme
|
|
29
|
-
from playwright.sync_api import Browser, BrowserContext
|
|
30
|
-
from playwright.sync_api import TimeoutError as PlaywrightTimeoutError
|
|
31
|
-
from playwright.sync_api import sync_playwright
|
|
32
|
-
from portpicker import pick_unused_port
|
|
33
|
-
from sphinx.application import Sphinx
|
|
34
|
-
from sphinx.config import Config
|
|
35
|
-
from sphinx.util.docutils import SphinxDirective
|
|
36
|
-
|
|
37
|
-
Meta = typing.TypedDict('Meta', {
|
|
38
|
-
'version': str,
|
|
39
|
-
'parallel_read_safe': bool,
|
|
40
|
-
'parallel_write_safe': bool
|
|
41
|
-
})
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
class ScreenshotDirective(SphinxDirective):
|
|
45
|
-
"""Sphinx Screenshot Dirctive.
|
|
46
|
-
|
|
47
|
-
This directive embeds a screenshot of a webpage.
|
|
48
|
-
|
|
49
|
-
# Example
|
|
50
|
-
|
|
51
|
-
You can simply pass a URL for a webpage that you want to take a screenshot.
|
|
52
|
-
|
|
53
|
-
```rst
|
|
54
|
-
.. screenshot:: http://www.example.com
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
You can also specify the screen size for the screenshot with `width` and
|
|
58
|
-
`height` parameters in pixel.
|
|
59
|
-
|
|
60
|
-
```rst
|
|
61
|
-
.. screenshot:: http://www.example.com
|
|
62
|
-
:width: 1280
|
|
63
|
-
:height: 960
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
You can include a caption for the screenshot's `figure` directive.
|
|
67
|
-
|
|
68
|
-
```rst
|
|
69
|
-
.. screenshot:: http://www.example.com
|
|
70
|
-
:caption: This is a screenshot for www.example.com
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
You can describe the interaction that you want to have with the webpage
|
|
74
|
-
before taking a screenshot in JavaScript.
|
|
75
|
-
|
|
76
|
-
```rst
|
|
77
|
-
.. screenshot:: http://www.example.com
|
|
78
|
-
|
|
79
|
-
document.querySelector('button').click();
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
Use `figclass` option if you want to specify a class name to the image.
|
|
83
|
-
|
|
84
|
-
```rst
|
|
85
|
-
.. screenshot:: http://www.example.com
|
|
86
|
-
:figclass: foo
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
It also generates a PDF file when `pdf` option is given, which might be
|
|
90
|
-
useful when you need scalable image assets.
|
|
91
|
-
|
|
92
|
-
```rst
|
|
93
|
-
.. screenshot:: http://www.example.com
|
|
94
|
-
:pdf:
|
|
95
|
-
```
|
|
96
|
-
"""
|
|
97
|
-
|
|
98
|
-
required_arguments = 1 # URL
|
|
99
|
-
has_content = True
|
|
100
|
-
option_spec = {
|
|
101
|
-
'browser': str,
|
|
102
|
-
'height': directives.positive_int,
|
|
103
|
-
'width': directives.positive_int,
|
|
104
|
-
'caption': directives.unchanged,
|
|
105
|
-
'figclass': directives.unchanged,
|
|
106
|
-
'pdf': directives.flag,
|
|
107
|
-
'color-scheme': str,
|
|
108
|
-
'full-page': directives.flag,
|
|
109
|
-
'context': str,
|
|
110
|
-
'headers': directives.unchanged,
|
|
111
|
-
}
|
|
112
|
-
pool = ThreadPoolExecutor()
|
|
113
|
-
|
|
114
|
-
@staticmethod
|
|
115
|
-
def take_screenshot(
|
|
116
|
-
url: str, browser_name: str, width: int, height: int, filepath: str,
|
|
117
|
-
init_script: str, interactions: str, generate_pdf: bool,
|
|
118
|
-
color_scheme: ColorScheme, full_page: bool,
|
|
119
|
-
context_builder: typing.Optional[typing.Callable[[Browser, str, str],
|
|
120
|
-
BrowserContext]],
|
|
121
|
-
headers: dict):
|
|
122
|
-
"""Takes a screenshot with Playwright's Chromium browser.
|
|
123
|
-
|
|
124
|
-
Args:
|
|
125
|
-
url (str): The HTTP/HTTPS URL of the webpage to screenshot.
|
|
126
|
-
width (int): The width of the screenshot in pixels.
|
|
127
|
-
height (int): The height of the screenshot in pixels.
|
|
128
|
-
filepath (str): The path to save the screenshot to.
|
|
129
|
-
init_script (str): JavaScript code to be evaluated after the document
|
|
130
|
-
was created but before any of its scripts were run. See more details at
|
|
131
|
-
https://playwright.dev/python/docs/api/class-page#page-add-init-script
|
|
132
|
-
interactions (str): JavaScript code to run before taking the screenshot
|
|
133
|
-
after the page was loaded.
|
|
134
|
-
generate_pdf (bool): Generate a PDF file along with the screenshot.
|
|
135
|
-
color_scheme (str): The preferred color scheme. Can be 'light' or 'dark'.
|
|
136
|
-
context: A method to build the Playwright context.
|
|
137
|
-
"""
|
|
138
|
-
with sync_playwright() as playwright:
|
|
139
|
-
browser = getattr(playwright, browser_name).launch()
|
|
140
|
-
|
|
141
|
-
if context_builder:
|
|
142
|
-
try:
|
|
143
|
-
context = context_builder(browser, url, color_scheme)
|
|
144
|
-
except PlaywrightTimeoutError:
|
|
145
|
-
raise RuntimeError(
|
|
146
|
-
'Timeout error occured at %s in executing py init script %s' %
|
|
147
|
-
(url, context_builder.__name__))
|
|
148
|
-
else:
|
|
149
|
-
context = browser.new_context(color_scheme=color_scheme)
|
|
150
|
-
|
|
151
|
-
page = context.new_page()
|
|
152
|
-
page.set_default_timeout(10000)
|
|
153
|
-
page.set_viewport_size({'width': width, 'height': height})
|
|
154
|
-
|
|
155
|
-
try:
|
|
156
|
-
if init_script:
|
|
157
|
-
page.add_init_script(init_script)
|
|
158
|
-
page.set_extra_http_headers(headers)
|
|
159
|
-
page.goto(url)
|
|
160
|
-
page.wait_for_load_state('networkidle')
|
|
161
|
-
|
|
162
|
-
# Execute interactions
|
|
163
|
-
if interactions:
|
|
164
|
-
page.evaluate(interactions)
|
|
165
|
-
page.wait_for_load_state('networkidle')
|
|
166
|
-
except PlaywrightTimeoutError:
|
|
167
|
-
raise RuntimeError('Timeout error occured at %s in executing\n%s' %
|
|
168
|
-
(url, interactions))
|
|
169
|
-
page.screenshot(path=filepath, full_page=full_page)
|
|
170
|
-
if generate_pdf:
|
|
171
|
-
page.emulate_media(media='screen')
|
|
172
|
-
root, ext = os.path.splitext(filepath)
|
|
173
|
-
page.pdf(width=f'{width}px', height=f'{height}px', path=root + '.pdf')
|
|
174
|
-
page.close()
|
|
175
|
-
browser.close()
|
|
176
|
-
|
|
177
|
-
def evaluate_substitutions(self, text: str) -> str:
|
|
178
|
-
substitutions = self.state.document.substitution_defs
|
|
179
|
-
for key, value in substitutions.items():
|
|
180
|
-
text = text.replace(f"|{key}|", value.astext())
|
|
181
|
-
return text
|
|
182
|
-
|
|
183
|
-
def run(self) -> typing.List[nodes.Node]:
|
|
184
|
-
screenshot_init_script: str = self.env.config.screenshot_init_script or ''
|
|
185
|
-
|
|
186
|
-
# Ensure the screenshots directory exists
|
|
187
|
-
ss_dirpath = os.path.join(self.env.app.outdir, '_static', 'screenshots')
|
|
188
|
-
os.makedirs(ss_dirpath, exist_ok=True)
|
|
189
|
-
|
|
190
|
-
# Parse parameters
|
|
191
|
-
raw_url = self.arguments[0]
|
|
192
|
-
url = self.evaluate_substitutions(raw_url)
|
|
193
|
-
browser = self.options.get('browser',
|
|
194
|
-
self.env.config.screenshot_default_browser)
|
|
195
|
-
height = self.options.get('height',
|
|
196
|
-
self.env.config.screenshot_default_height)
|
|
197
|
-
width = self.options.get('width', self.env.config.screenshot_default_width)
|
|
198
|
-
color_scheme = self.options.get(
|
|
199
|
-
'color-scheme', self.env.config.screenshot_default_color_scheme)
|
|
200
|
-
caption_text = self.options.get('caption', '')
|
|
201
|
-
figclass = self.options.get('figclass', '')
|
|
202
|
-
pdf = 'pdf' in self.options
|
|
203
|
-
full_page = ('full-page' in self.options or
|
|
204
|
-
self.env.config.screenshot_default_full_page)
|
|
205
|
-
context = self.options.get('context', '')
|
|
206
|
-
interactions = '\n'.join(self.content)
|
|
207
|
-
headers = self.options.get('headers', '')
|
|
208
|
-
|
|
209
|
-
request_headers = {**self.env.config.screenshot_default_headers}
|
|
210
|
-
if headers:
|
|
211
|
-
for header in headers.strip().split("\n"):
|
|
212
|
-
name, value = header.split(" ", 1)
|
|
213
|
-
request_headers[name] = value
|
|
214
|
-
|
|
215
|
-
if urlparse(url).scheme not in {'http', 'https'}:
|
|
216
|
-
raise RuntimeError(
|
|
217
|
-
f'Invalid URL: {url}. Only HTTP/HTTPS URLs are supported.')
|
|
218
|
-
|
|
219
|
-
# Generate filename based on hash of parameters
|
|
220
|
-
hash_input = "_".join([
|
|
221
|
-
raw_url, browser,
|
|
222
|
-
str(height),
|
|
223
|
-
str(width), color_scheme, context, interactions,
|
|
224
|
-
str(full_page)
|
|
225
|
-
])
|
|
226
|
-
filename = hashlib.md5(hash_input.encode()).hexdigest() + '.png'
|
|
227
|
-
filepath = os.path.join(ss_dirpath, filename)
|
|
228
|
-
|
|
229
|
-
if context:
|
|
230
|
-
context_builder_path = self.config.screenshot_contexts[context]
|
|
231
|
-
context_builder = resolve_python_method(context_builder_path)
|
|
232
|
-
else:
|
|
233
|
-
context_builder = None
|
|
234
|
-
|
|
235
|
-
# Check if the file already exists. If not, take a screenshot
|
|
236
|
-
if not os.path.exists(filepath):
|
|
237
|
-
fut = self.pool.submit(ScreenshotDirective.take_screenshot, url, browser,
|
|
238
|
-
width, height, filepath, screenshot_init_script,
|
|
239
|
-
interactions, pdf, color_scheme, full_page,
|
|
240
|
-
context_builder, request_headers)
|
|
241
|
-
fut.result()
|
|
242
|
-
|
|
243
|
-
# Create image and figure nodes
|
|
244
|
-
docdir = os.path.dirname(self.env.doc2path(self.env.docname))
|
|
245
|
-
rel_ss_dirpath = os.path.relpath(ss_dirpath, start=docdir)
|
|
246
|
-
rel_filepath = os.path.join(rel_ss_dirpath, filename).replace(os.sep, '/')
|
|
247
|
-
image_node = nodes.image(uri=rel_filepath)
|
|
248
|
-
figure_node = nodes.figure('', image_node)
|
|
249
|
-
|
|
250
|
-
if figclass:
|
|
251
|
-
figure_node['classes'].append(figclass)
|
|
252
|
-
|
|
253
|
-
if caption_text:
|
|
254
|
-
parsed = nodes.Element()
|
|
255
|
-
self.state.nested_parse(
|
|
256
|
-
ViewList([caption_text], source=''), self.content_offset, parsed)
|
|
257
|
-
figure_node += nodes.caption(parsed[0].source or '', '',
|
|
258
|
-
*parsed[0].children)
|
|
259
|
-
|
|
260
|
-
return [figure_node]
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
app_threads = {}
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
def resolve_python_method(import_path: str):
|
|
267
|
-
module_path, method_name = import_path.split(":")
|
|
268
|
-
module = importlib.import_module(module_path)
|
|
269
|
-
method = getattr(module, method_name)
|
|
270
|
-
return method
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
def setup_apps(app: Sphinx, config: Config):
|
|
274
|
-
"""Start the WSGI application threads.
|
|
275
|
-
|
|
276
|
-
A new replacement is created for each WSGI app."""
|
|
277
|
-
for wsgi_app_name, wsgi_app_path in config.screenshot_apps.items():
|
|
278
|
-
port = pick_unused_port()
|
|
279
|
-
config.rst_prolog = (
|
|
280
|
-
config.rst_prolog or
|
|
281
|
-
"") + f"\n.. |{wsgi_app_name}| replace:: http://localhost:{port}\n"
|
|
282
|
-
app_builder = resolve_python_method(wsgi_app_path)
|
|
283
|
-
wsgi_app = app_builder(app)
|
|
284
|
-
httpd = wsgiref.simple_server.make_server("localhost", port, wsgi_app)
|
|
285
|
-
thread = threading.Thread(target=httpd.serve_forever)
|
|
286
|
-
thread.start()
|
|
287
|
-
app_threads[wsgi_app_name] = (httpd, thread)
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
def teardown_apps(app: Sphinx, exception: typing.Optional[Exception]):
|
|
291
|
-
"""Shut down the WSGI application threads."""
|
|
292
|
-
for httpd, thread in app_threads.values():
|
|
293
|
-
httpd.shutdown()
|
|
294
|
-
thread.join()
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
def setup(app: Sphinx) -> Meta:
|
|
298
|
-
app.add_directive('screenshot', ScreenshotDirective)
|
|
299
|
-
app.add_config_value('screenshot_init_script', '', 'env')
|
|
300
|
-
app.add_config_value(
|
|
301
|
-
'screenshot_default_width',
|
|
302
|
-
1280,
|
|
303
|
-
'env',
|
|
304
|
-
description="The default width for screenshots")
|
|
305
|
-
app.add_config_value(
|
|
306
|
-
'screenshot_default_height',
|
|
307
|
-
960,
|
|
308
|
-
'env',
|
|
309
|
-
description="The default height for screenshots")
|
|
310
|
-
app.add_config_value(
|
|
311
|
-
'screenshot_default_browser',
|
|
312
|
-
'chromium',
|
|
313
|
-
'env',
|
|
314
|
-
description="The default browser for screenshots")
|
|
315
|
-
app.add_config_value(
|
|
316
|
-
'screenshot_default_full_page',
|
|
317
|
-
False,
|
|
318
|
-
'env',
|
|
319
|
-
description="Whether to take full page screenshots")
|
|
320
|
-
app.add_config_value(
|
|
321
|
-
'screenshot_default_color_scheme',
|
|
322
|
-
'null',
|
|
323
|
-
'env',
|
|
324
|
-
description="The default color scheme for screenshots")
|
|
325
|
-
app.add_config_value(
|
|
326
|
-
'screenshot_contexts', {},
|
|
327
|
-
'env',
|
|
328
|
-
types=[dict[str, str]],
|
|
329
|
-
description="A dict of paths to Playwright context build methods")
|
|
330
|
-
app.add_config_value(
|
|
331
|
-
'screenshot_default_headers', {},
|
|
332
|
-
'env',
|
|
333
|
-
description="The default headers to pass in requests")
|
|
334
|
-
app.add_config_value(
|
|
335
|
-
'screenshot_apps', {},
|
|
336
|
-
'env',
|
|
337
|
-
types=[dict[str, str]],
|
|
338
|
-
description="A dict of WSGI apps")
|
|
339
|
-
app.connect('config-inited', setup_apps)
|
|
340
|
-
app.connect('build-finished', teardown_apps)
|
|
341
|
-
return {
|
|
342
|
-
'version': importlib.metadata.version('sphinxcontrib-screenshot'),
|
|
343
|
-
'parallel_read_safe': True,
|
|
344
|
-
'parallel_write_safe': True,
|
|
345
|
-
}
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
sphinxcontrib/screenshot.py,sha256=eJEF1qfGGo1uwyTovYHXx4Ewz2fXmrxDSsQSdLJ0O-0,11863
|
|
2
|
-
sphinxcontrib_screenshot-0.1.4.dist-info/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
|
|
3
|
-
sphinxcontrib_screenshot-0.1.4.dist-info/METADATA,sha256=XpzmVDGTDfL-AxKUv7jIV3wQ_phEGc-OLxWU-wG7-o0,2850
|
|
4
|
-
sphinxcontrib_screenshot-0.1.4.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
|
5
|
-
sphinxcontrib_screenshot-0.1.4.dist-info/top_level.txt,sha256=VJrV3_vaiKQVgVpR0I1iecxoO0drzGu-M0j40PVP2QQ,14
|
|
6
|
-
sphinxcontrib_screenshot-0.1.4.dist-info/RECORD,,
|
|
File without changes
|
{sphinxcontrib_screenshot-0.1.4.dist-info → sphinxcontrib_screenshot-0.3.0.dist-info}/top_level.txt
RENAMED
|
File without changes
|