sphinxcontrib-screenshot 0.1.3__py3-none-any.whl → 0.2.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.

@@ -13,17 +13,25 @@
13
13
  # limitations under the License.
14
14
 
15
15
  import hashlib
16
+ import importlib
17
+ import importlib.metadata
16
18
  import os
19
+ import threading
17
20
  import typing
21
+ import wsgiref.simple_server
18
22
  from concurrent.futures import ThreadPoolExecutor
19
23
  from urllib.parse import urlparse
20
24
 
21
25
  from docutils import nodes
22
26
  from docutils.parsers.rst import directives
23
- from docutils.statemachine import ViewList
27
+ from docutils.parsers.rst.directives.images import Figure
28
+ from playwright._impl._helper import ColorScheme
29
+ from playwright.sync_api import Browser, BrowserContext
24
30
  from playwright.sync_api import TimeoutError as PlaywrightTimeoutError
25
31
  from playwright.sync_api import sync_playwright
32
+ from portpicker import pick_unused_port
26
33
  from sphinx.application import Sphinx
34
+ from sphinx.config import Config
27
35
  from sphinx.util.docutils import SphinxDirective
28
36
 
29
37
  Meta = typing.TypedDict('Meta', {
@@ -32,10 +40,8 @@ Meta = typing.TypedDict('Meta', {
32
40
  'parallel_write_safe': bool
33
41
  })
34
42
 
35
- __version__ = '0.1.3'
36
43
 
37
-
38
- class ScreenshotDirective(SphinxDirective):
44
+ class ScreenshotDirective(SphinxDirective, Figure):
39
45
  """Sphinx Screenshot Dirctive.
40
46
 
41
47
  This directive embeds a screenshot of a webpage.
@@ -48,20 +54,13 @@ class ScreenshotDirective(SphinxDirective):
48
54
  .. screenshot:: http://www.example.com
49
55
  ```
50
56
 
51
- You can also specify the screen size for the screenshot with `width` and
52
- `height` parameters in pixel.
53
-
54
- ```rst
55
- .. screenshot:: http://www.example.com
56
- :width: 1280
57
- :height: 960
58
- ```
59
-
60
- You can include a caption for the screenshot's `figure` directive.
57
+ You can also specify the screen size for the screenshot with
58
+ `viewport-width` and `viewport-height` parameters in pixel.
61
59
 
62
60
  ```rst
63
61
  .. screenshot:: http://www.example.com
64
- :caption: This is a screenshot for www.example.com
62
+ :viewport-width: 1280
63
+ :viewport-height: 960
65
64
  ```
66
65
 
67
66
  You can describe the interaction that you want to have with the webpage
@@ -73,13 +72,6 @@ class ScreenshotDirective(SphinxDirective):
73
72
  document.querySelector('button').click();
74
73
  ```
75
74
 
76
- Use `figclass` option if you want to specify a class name to the image.
77
-
78
- ```rst
79
- .. screenshot:: http://www.example.com
80
- :figclass: foo
81
- ```
82
-
83
75
  It also generates a PDF file when `pdf` option is given, which might be
84
76
  useful when you need scalable image assets.
85
77
 
@@ -90,25 +82,34 @@ class ScreenshotDirective(SphinxDirective):
90
82
  """
91
83
 
92
84
  required_arguments = 1 # URL
93
- has_content = True
94
85
  option_spec = {
95
- 'height': directives.positive_int,
96
- 'width': directives.positive_int,
97
- 'caption': directives.unchanged,
98
- 'figclass': directives.unchanged,
86
+ **Figure.option_spec,
87
+ 'browser': str,
88
+ 'viewport-height': directives.positive_int,
89
+ 'viewport-width': directives.positive_int,
90
+ 'interactions': str,
99
91
  'pdf': directives.flag,
92
+ 'color-scheme': str,
93
+ 'full-page': directives.flag,
94
+ 'context': str,
95
+ 'headers': directives.unchanged,
100
96
  }
101
97
  pool = ThreadPoolExecutor()
102
98
 
103
99
  @staticmethod
104
- def take_screenshot(url: str, width: int, height: int, filepath: str,
105
- init_script: str, interactions: str, generate_pdf: bool):
100
+ def take_screenshot(
101
+ url: str, browser_name: str, viewport_width: int, viewport_height: int,
102
+ filepath: str, init_script: str, interactions: str, generate_pdf: bool,
103
+ color_scheme: ColorScheme, full_page: bool,
104
+ context_builder: typing.Optional[typing.Callable[[Browser, str, str],
105
+ BrowserContext]],
106
+ headers: dict):
106
107
  """Takes a screenshot with Playwright's Chromium browser.
107
108
 
108
109
  Args:
109
110
  url (str): The HTTP/HTTPS URL of the webpage to screenshot.
110
- width (int): The width of the screenshot in pixels.
111
- height (int): The height of the screenshot in pixels.
111
+ viewport_width (int): The width of the screenshot in pixels.
112
+ viewport_height (int): The height of the screenshot in pixels.
112
113
  filepath (str): The path to save the screenshot to.
113
114
  init_script (str): JavaScript code to be evaluated after the document
114
115
  was created but before any of its scripts were run. See more details at
@@ -116,15 +117,33 @@ class ScreenshotDirective(SphinxDirective):
116
117
  interactions (str): JavaScript code to run before taking the screenshot
117
118
  after the page was loaded.
118
119
  generate_pdf (bool): Generate a PDF file along with the screenshot.
120
+ color_scheme (str): The preferred color scheme. Can be 'light' or 'dark'.
121
+ context: A method to build the Playwright context.
119
122
  """
120
123
  with sync_playwright() as playwright:
121
- browser = playwright.chromium.launch()
122
- page = browser.new_page()
124
+ browser = getattr(playwright, browser_name).launch()
125
+
126
+ if context_builder:
127
+ try:
128
+ context = context_builder(browser, url, color_scheme)
129
+ except PlaywrightTimeoutError:
130
+ raise RuntimeError(
131
+ 'Timeout error occured at %s in executing py init script %s' %
132
+ (url, context_builder.__name__))
133
+ else:
134
+ context = browser.new_context(color_scheme=color_scheme)
135
+
136
+ page = context.new_page()
123
137
  page.set_default_timeout(10000)
124
- page.set_viewport_size({'width': width, 'height': height})
138
+ page.set_viewport_size({
139
+ 'width': viewport_width,
140
+ 'height': viewport_height
141
+ })
142
+
125
143
  try:
126
144
  if init_script:
127
145
  page.add_init_script(init_script)
146
+ page.set_extra_http_headers(headers)
128
147
  page.goto(url)
129
148
  page.wait_for_load_state('networkidle')
130
149
 
@@ -135,15 +154,24 @@ class ScreenshotDirective(SphinxDirective):
135
154
  except PlaywrightTimeoutError:
136
155
  raise RuntimeError('Timeout error occured at %s in executing\n%s' %
137
156
  (url, interactions))
138
- page.screenshot(path=filepath)
157
+ page.screenshot(path=filepath, full_page=full_page)
139
158
  if generate_pdf:
140
159
  page.emulate_media(media='screen')
141
160
  root, ext = os.path.splitext(filepath)
142
- page.pdf(width=f'{width}px', height=f'{height}px', path=root + '.pdf')
161
+ page.pdf(
162
+ width=f'{viewport_width}px',
163
+ height=f'{viewport_height}px',
164
+ path=root + '.pdf')
143
165
  page.close()
144
166
  browser.close()
145
167
 
146
- def run(self) -> typing.List[nodes.Node]:
168
+ def evaluate_substitutions(self, text: str) -> str:
169
+ substitutions = self.state.document.substitution_defs
170
+ for key, value in substitutions.items():
171
+ text = text.replace(f"|{key}|", value.astext())
172
+ return text
173
+
174
+ def run(self) -> typing.Sequence[nodes.Node]:
147
175
  screenshot_init_script: str = self.env.config.screenshot_init_script or ''
148
176
 
149
177
  # Ensure the screenshots directory exists
@@ -151,55 +179,147 @@ class ScreenshotDirective(SphinxDirective):
151
179
  os.makedirs(ss_dirpath, exist_ok=True)
152
180
 
153
181
  # Parse parameters
154
- url = self.arguments[0]
155
- height = self.options.get('height', 960)
156
- width = self.options.get('width', 1280)
157
- caption_text = self.options.get('caption', '')
158
- figclass = self.options.get('figclass', '')
182
+ raw_url = self.arguments[0]
183
+ url = self.evaluate_substitutions(raw_url)
184
+ interactions = self.options.get('interactions', '')
185
+ browser = self.options.get('browser',
186
+ self.env.config.screenshot_default_browser)
187
+ viewport_height = self.options.get(
188
+ 'viewport-height', self.env.config.screenshot_default_viewport_height)
189
+ viewport_width = self.options.get(
190
+ 'viewport-width', self.env.config.screenshot_default_viewport_width)
191
+ color_scheme = self.options.get(
192
+ 'color-scheme', self.env.config.screenshot_default_color_scheme)
159
193
  pdf = 'pdf' in self.options
160
- interactions = '\n'.join(self.content)
194
+ full_page = ('full-page' in self.options or
195
+ self.env.config.screenshot_default_full_page)
196
+ context = self.options.get('context', '')
197
+ headers = self.options.get('headers', '')
198
+
199
+ request_headers = {**self.env.config.screenshot_default_headers}
200
+ if headers:
201
+ for header in headers.strip().split("\n"):
202
+ name, value = header.split(" ", 1)
203
+ request_headers[name] = value
161
204
 
162
205
  if urlparse(url).scheme not in {'http', 'https'}:
163
206
  raise RuntimeError(
164
207
  f'Invalid URL: {url}. Only HTTP/HTTPS URLs are supported.')
165
208
 
166
209
  # Generate filename based on hash of parameters
167
- hash_input = f'{url}_{height}_{width}_{interactions}'
210
+ hash_input = "_".join([
211
+ raw_url, browser,
212
+ str(viewport_height),
213
+ str(viewport_width), color_scheme, context, interactions,
214
+ str(full_page)
215
+ ])
168
216
  filename = hashlib.md5(hash_input.encode()).hexdigest() + '.png'
169
217
  filepath = os.path.join(ss_dirpath, filename)
170
218
 
219
+ if context:
220
+ context_builder_path = self.config.screenshot_contexts[context]
221
+ context_builder = resolve_python_method(context_builder_path)
222
+ else:
223
+ context_builder = None
224
+
171
225
  # Check if the file already exists. If not, take a screenshot
172
226
  if not os.path.exists(filepath):
173
- fut = self.pool.submit(ScreenshotDirective.take_screenshot, url, width,
174
- height, filepath, screenshot_init_script,
175
- interactions, pdf)
227
+ fut = self.pool.submit(ScreenshotDirective.take_screenshot, url, browser,
228
+ viewport_width, viewport_height, filepath,
229
+ screenshot_init_script, interactions, pdf,
230
+ color_scheme, full_page, context_builder,
231
+ request_headers)
176
232
  fut.result()
177
233
 
178
234
  # Create image and figure nodes
179
235
  docdir = os.path.dirname(self.env.doc2path(self.env.docname))
180
236
  rel_ss_dirpath = os.path.relpath(ss_dirpath, start=docdir)
181
237
  rel_filepath = os.path.join(rel_ss_dirpath, filename).replace(os.sep, '/')
182
- image_node = nodes.image(uri=rel_filepath)
183
- figure_node = nodes.figure('', image_node)
184
238
 
185
- if figclass:
186
- figure_node['classes'].append(figclass)
239
+ self.arguments[0] = rel_filepath
240
+ return super().run()
241
+
242
+
243
+ app_threads = {}
244
+
245
+
246
+ def resolve_python_method(import_path: str):
247
+ module_path, method_name = import_path.split(":")
248
+ module = importlib.import_module(module_path)
249
+ method = getattr(module, method_name)
250
+ return method
251
+
252
+
253
+ def setup_apps(app: Sphinx, config: Config):
254
+ """Start the WSGI application threads.
255
+
256
+ A new replacement is created for each WSGI app."""
257
+ for wsgi_app_name, wsgi_app_path in config.screenshot_apps.items():
258
+ port = pick_unused_port()
259
+ config.rst_prolog = (
260
+ config.rst_prolog or
261
+ "") + f"\n.. |{wsgi_app_name}| replace:: http://localhost:{port}\n"
262
+ app_builder = resolve_python_method(wsgi_app_path)
263
+ wsgi_app = app_builder(app)
264
+ httpd = wsgiref.simple_server.make_server("localhost", port, wsgi_app)
265
+ thread = threading.Thread(target=httpd.serve_forever)
266
+ thread.start()
267
+ app_threads[wsgi_app_name] = (httpd, thread)
187
268
 
188
- if caption_text:
189
- parsed = nodes.Element()
190
- self.state.nested_parse(
191
- ViewList([caption_text], source=''), self.content_offset, parsed)
192
- figure_node += nodes.caption(parsed[0].source or '', '',
193
- *parsed[0].children)
194
269
 
195
- return [figure_node]
270
+ def teardown_apps(app: Sphinx, exception: typing.Optional[Exception]):
271
+ """Shut down the WSGI application threads."""
272
+ for httpd, thread in app_threads.values():
273
+ httpd.shutdown()
274
+ thread.join()
196
275
 
197
276
 
198
277
  def setup(app: Sphinx) -> Meta:
199
278
  app.add_directive('screenshot', ScreenshotDirective)
200
279
  app.add_config_value('screenshot_init_script', '', 'env')
280
+ app.add_config_value(
281
+ 'screenshot_default_viewport_width',
282
+ 1280,
283
+ 'env',
284
+ description="The default width for screenshots")
285
+ app.add_config_value(
286
+ 'screenshot_default_viewport_height',
287
+ 960,
288
+ 'env',
289
+ description="The default height for screenshots")
290
+ app.add_config_value(
291
+ 'screenshot_default_browser',
292
+ 'chromium',
293
+ 'env',
294
+ description="The default browser for screenshots")
295
+ app.add_config_value(
296
+ 'screenshot_default_full_page',
297
+ False,
298
+ 'env',
299
+ description="Whether to take full page screenshots")
300
+ app.add_config_value(
301
+ 'screenshot_default_color_scheme',
302
+ 'null',
303
+ 'env',
304
+ description="The default color scheme for screenshots")
305
+ app.add_config_value(
306
+ 'screenshot_contexts', {},
307
+ 'env',
308
+ types=[dict[str, str]],
309
+ description="A dict of paths to Playwright context build methods")
310
+ app.add_config_value(
311
+ 'screenshot_default_headers', {},
312
+ 'env',
313
+ description="The default headers to pass in requests")
314
+ app.add_config_value(
315
+ 'screenshot_apps', {},
316
+ 'env',
317
+ types=[dict[str, str]],
318
+ description="A dict of WSGI apps")
319
+ app.connect('config-inited', setup_apps)
320
+ app.connect('build-finished', teardown_apps)
201
321
  return {
202
- 'version': __version__,
322
+ 'version': importlib.metadata.version('sphinxcontrib-screenshot'),
203
323
  'parallel_read_safe': True,
204
324
  'parallel_write_safe': True,
205
325
  }
@@ -1,103 +1,64 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.2
2
2
  Name: sphinxcontrib-screenshot
3
- Version: 0.1.3
4
- Summary: A Shpinx extension to embed webpage screenshots.
5
- Home-page: https://github.com/tushuhei/sphinxcontrib-screenshot/
6
- Author: Shuhei Iitsuka
7
- Author-email: tushuhei@gmail.com
3
+ Version: 0.2.0
4
+ Summary: A Sphinx extension to embed webpage screenshots.
5
+ Author-email: Shuhei Iitsuka <tushuhei@gmail.com>
8
6
  License: Apache-2.0
7
+ Project-URL: repository, https://github.com/tushuhei/sphinxcontrib-screenshot/
9
8
  Classifier: Development Status :: 3 - Alpha
10
9
  Classifier: Operating System :: OS Independent
11
10
  Classifier: License :: OSI Approved :: Apache Software License
12
11
  Classifier: Programming Language :: Python :: 3.9
13
12
  Classifier: Programming Language :: Python :: 3.10
14
13
  Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
15
16
  Requires-Python: >=3.9
16
17
  Description-Content-Type: text/markdown
17
18
  License-File: LICENSE
18
19
  Requires-Dist: playwright
19
20
  Requires-Dist: sphinx
21
+ Requires-Dist: portpicker
20
22
  Provides-Extra: dev
21
23
  Requires-Dist: beautifulsoup4; extra == "dev"
22
24
  Requires-Dist: build; extra == "dev"
23
25
  Requires-Dist: flake8; extra == "dev"
26
+ Requires-Dist: flake8-pyproject; extra == "dev"
24
27
  Requires-Dist: isort; extra == "dev"
25
28
  Requires-Dist: mypy; extra == "dev"
26
29
  Requires-Dist: Pillow; extra == "dev"
30
+ Requires-Dist: pre-commit; extra == "dev"
27
31
  Requires-Dist: pytest; extra == "dev"
32
+ Requires-Dist: pytest-regressions[image]; extra == "dev"
28
33
  Requires-Dist: sphinx[test]; extra == "dev"
29
34
  Requires-Dist: toml; extra == "dev"
35
+ Requires-Dist: tox; extra == "dev"
30
36
  Requires-Dist: twine; extra == "dev"
31
37
  Requires-Dist: types-beautifulsoup4; extra == "dev"
32
38
  Requires-Dist: types-docutils; extra == "dev"
39
+ Requires-Dist: types-portpicker; extra == "dev"
33
40
  Requires-Dist: types-Pillow; extra == "dev"
34
41
  Requires-Dist: types-setuptools; extra == "dev"
42
+ Requires-Dist: user-agents; extra == "dev"
35
43
  Requires-Dist: yapf; extra == "dev"
44
+ Provides-Extra: doc
45
+ Requires-Dist: myst-parser; extra == "doc"
46
+ Requires-Dist: shibuya; extra == "doc"
47
+ Requires-Dist: sphinx; extra == "doc"
36
48
 
37
49
  # sphinxcontrib-screenshot
38
50
 
39
51
  A Sphinx extension to embed website screenshots.
40
52
 
41
- ![Example screenshot](https://raw.githubusercontent.com/tushuhei/sphinxcontrib-screenshot/main/example.png)
42
-
43
- ## Install
44
-
45
- ```bash
46
- pip install sphinxcontrib-screenshot
47
- playwright install
48
- ```
49
-
50
- ## Usage
51
-
52
- Add `sphinxcontrib.screenshot` to your `conf.py`.
53
-
54
- ```py
55
- extensions = ["sphinxcontrib.screenshot"]
56
- ```
57
-
58
- Then use the `screenshot` directive in your Sphinx source file.
59
-
60
- ```rst
61
- .. screenshot:: http://www.example.com
62
- ```
63
-
64
- You can also specify the screen size for the screenshot with `width` and `height` parameters.
65
-
66
- ```rst
67
- .. screenshot:: http://www.example.com
68
- :width: 1280
69
- :height: 960
70
- ```
71
-
72
- You can include a caption for the screenshot's `figure` directive.
73
-
74
53
  ```rst
75
54
  .. screenshot:: http://www.example.com
76
- :caption: This is a screenshot for www.example.com
77
- ```
78
-
79
- You can describe the interaction that you want to have with the webpage before taking a screenshot in JavaScript.
80
-
81
- ```rst
82
- .. screenshot:: http://www.example.com
83
-
84
- document.querySelector('button').click();
85
- ```
86
-
87
- ## Pro tips
88
- `sphinxcontrib-screenshot` supports URLs with the HTTP and HTTPS protocols.
89
- To take screenshots of local files and build the document while running a local server for them, you can use the NPM library [concurrently](https://www.npmjs.com/package/concurrently) in the following way:
90
-
91
- ### Build the document
92
- ```bash
93
- npx --yes concurrently -k --success=first "make html" "python3 -m http.server 3000 --directory=examples"
94
- ```
95
-
96
- ### Watch and build the document
97
- ```bash
98
- npx --yes concurrently -k "make livehtml" "python3 -m http.server 3000 --directory=examples"
55
+ :browser: chromium
56
+ :viewport-width: 1280
57
+ :viewport-height: 960
58
+ :color-scheme: dark
99
59
  ```
100
60
 
61
+ Read more in the [documentation](https://sphinxcontrib-screenshot.readthedocs.io).
101
62
 
102
63
  ## Notes
103
64
 
@@ -0,0 +1,6 @@
1
+ sphinxcontrib/screenshot.py,sha256=03cVsF1d9zuWMsVBwtZU_EQ_GMMsYyPa2a3vBmzC4f8,11342
2
+ sphinxcontrib_screenshot-0.2.0.dist-info/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
3
+ sphinxcontrib_screenshot-0.2.0.dist-info/METADATA,sha256=pwaSBQlzvflzW2BSv7ByJ8GvbwDrYF0xn8bftw3dPC0,2868
4
+ sphinxcontrib_screenshot-0.2.0.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
5
+ sphinxcontrib_screenshot-0.2.0.dist-info/top_level.txt,sha256=VJrV3_vaiKQVgVpR0I1iecxoO0drzGu-M0j40PVP2QQ,14
6
+ sphinxcontrib_screenshot-0.2.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.6.0)
2
+ Generator: setuptools (75.8.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,6 +0,0 @@
1
- sphinxcontrib/screenshot.py,sha256=rTcbf2bx5DiRIGKcA8dL7jbi-GwfrW7-aXU83M5vAlY,6829
2
- sphinxcontrib_screenshot-0.1.3.dist-info/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
3
- sphinxcontrib_screenshot-0.1.3.dist-info/METADATA,sha256=T6kkviJZwZe0ZtYllM4YwhlI9qiKtoTbnI6OAcxlvII,3652
4
- sphinxcontrib_screenshot-0.1.3.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
5
- sphinxcontrib_screenshot-0.1.3.dist-info/top_level.txt,sha256=VJrV3_vaiKQVgVpR0I1iecxoO0drzGu-M0j40PVP2QQ,14
6
- sphinxcontrib_screenshot-0.1.3.dist-info/RECORD,,