├── .coveragerc ├── .gitignore ├── .noserc ├── .travis.yml ├── CHANGES.md ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.md ├── appveyor.yml ├── docs ├── Makefile ├── _static │ └── custom.css ├── _templates │ └── layout.html ├── changes.md ├── conf.py ├── index.md ├── make.bat ├── reference.md └── server.py ├── dodo.py ├── mypy.ini ├── pyppeteer ├── __init__.py ├── browser.py ├── chromium_downloader.py ├── command.py ├── connection.py ├── coverage.py ├── dialog.py ├── element_handle.py ├── emulation_manager.py ├── errors.py ├── execution_context.py ├── frame_manager.py ├── helper.py ├── input.py ├── launcher.py ├── multimap.py ├── navigator_watcher.py ├── network_manager.py ├── options.py ├── page.py ├── target.py ├── tracing.py ├── us_keyboard_layout.py ├── util.py └── worker.py ├── requirements-dev.txt ├── requirements-docs.txt ├── requirements-test.txt ├── setup.py ├── spell.txt ├── tests ├── __init__.py ├── base.py ├── blank_800x600.png ├── closeme.py ├── dumpio.py ├── file-to-upload.txt ├── frame_utils.py ├── server.py ├── static │ ├── beforeunload.html │ ├── button.html │ ├── cached │ │ ├── one-style.css │ │ └── one-style.html │ ├── checkbox.html │ ├── csp.html │ ├── csscoverage │ │ ├── involved.html │ │ ├── media.html │ │ ├── multiple.html │ │ ├── simple.html │ │ ├── sourceurl.html │ │ ├── stylesheet1.css │ │ ├── stylesheet2.css │ │ └── unused.html │ ├── detect-touch.html │ ├── digits │ │ ├── 0.png │ │ ├── 1.png │ │ ├── 2.png │ │ ├── 3.png │ │ ├── 4.png │ │ ├── 5.png │ │ ├── 6.png │ │ ├── 7.png │ │ ├── 8.png │ │ └── 9.png │ ├── error.html │ ├── es6 │ │ ├── es6import.js │ │ ├── es6module.js │ │ └── es6pathimport.js │ ├── fileupload.html │ ├── frame-204.html │ ├── frame.html │ ├── grid.html │ ├── historyapi.html │ ├── huge-image.png │ ├── huge-page.html │ ├── injectedfile.js │ ├── injectedstyle.css │ ├── jscoverage │ │ ├── eval.html │ │ ├── involved.html │ │ ├── multiple.html │ │ ├── ranges.html │ │ ├── script1.js │ │ ├── script2.js │ │ ├── simple.html │ │ ├── sourceurl.html │ │ └── unused.html │ ├── keyboard.html │ ├── mobile.html │ ├── modernizr.js │ ├── mouse-helper.js │ ├── nested-frames.html │ ├── offscreenbuttons.html │ ├── one-frame.html │ ├── one-style.css │ ├── one-style.html │ ├── popup │ │ ├── popup.html │ │ └── window-open.html │ ├── resetcss.html │ ├── script.js │ ├── scrollable.html │ ├── select.html │ ├── self-request.html │ ├── serviceworkers │ │ ├── empty │ │ │ ├── sw.html │ │ │ └── sw.js │ │ └── fetch │ │ │ ├── style.css │ │ │ ├── sw.html │ │ │ └── sw.js │ ├── shadow.html │ ├── simple-extension │ │ ├── index.js │ │ └── manifest.json │ ├── simple.json │ ├── style.css │ ├── sw.js │ ├── temperable.html │ ├── textarea.html │ ├── touches.html │ ├── two-frames.html │ ├── worker │ │ ├── worker.html │ │ └── worker.js │ └── wrappedlink.html ├── test_abnormal_crash.py ├── test_browser.py ├── test_browser_context.py ├── test_connection.py ├── test_coverage.py ├── test_dialog.py ├── test_element_handle.py ├── test_execution_context.py ├── test_frame.py ├── test_input.py ├── test_launcher.py ├── test_misc.py ├── test_network.py ├── test_page.py ├── test_pyppeteer.py ├── test_screenshot.py ├── test_target.py ├── test_tracing.py ├── test_worker.py └── utils.py └── tox.ini /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit=setup.py 3 | source=pyppeteer,tests 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # Virtualenv 27 | env/ 28 | venv/ 29 | bin/ 30 | include/ 31 | lib/ 32 | lib64 33 | lib64/ 34 | man/ 35 | pyvenv.cfg 36 | 37 | # PyInstaller 38 | # Usually these files are written by a python script from a template 39 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 40 | *.manifest 41 | *.spec 42 | 43 | # Installer logs 44 | pip-log.txt 45 | pip-delete-this-directory.txt 46 | 47 | # Unit test / coverage reports 48 | htmlcov/ 49 | .tox/ 50 | .coverage 51 | .coverage.* 52 | .cache 53 | .doit.db.* 54 | .mypy_cache 55 | nosetests.xml 56 | coverage.xml 57 | *,cover 58 | .hypothesis/ 59 | .pytest_cache/ 60 | 61 | # Translations 62 | *.mo 63 | *.pot 64 | 65 | # Django stuff: 66 | *.log 67 | 68 | # Sphinx documentation 69 | docs/_build/ 70 | 71 | # PyBuilder 72 | target/ 73 | 74 | # pyenv python configuration file 75 | .python-version 76 | 77 | # pycharm file 78 | .idea/ 79 | 80 | ###### direnv ###### 81 | .direnv 82 | .envrc 83 | 84 | ###### zsh-autoenv ###### 85 | .autoenv.zsh 86 | .autoenv_leave.zsh 87 | 88 | # test files 89 | trace.json 90 | -------------------------------------------------------------------------------- /.noserc: -------------------------------------------------------------------------------- 1 | [nosetests] 2 | logging-level=INFO 3 | # no-path-adjustment=true 4 | # with-coverage=true 5 | # cover-package=pyppeteer 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # Config file for automatic testing at travis-ci.org 2 | # This file will be regenerated if you run travis_pypi_setup.py 3 | 4 | language: python 5 | 6 | matrix: 7 | include: 8 | - dist: xenial 9 | python: 3.5 10 | env: TOXENV="py35" 11 | - dist: xenial 12 | python: 3.6 13 | env: TOXENV="py36" 14 | - dist: xenial 15 | python: 3.7 16 | env: TOXENV="py37,doit,codecov" 17 | - os: osx 18 | osx_image: xcode10.2 # Python 3.7.2 running on macOS 10.14.3 19 | language: shell 20 | env: TOXENV="py37" 21 | before_install: python3 --version 22 | - os: windows 23 | language: shell 24 | before_install: choco install python 25 | env: 26 | - PATH=/c/Python37:/c/Python37/Scripts:$PATH 27 | TOXENV="py37" 28 | allow_failures: 29 | - os: windows # windows build is unstable on Travis 30 | 31 | 32 | install: 33 | - pip3 install tox 34 | 35 | script: 36 | - tox 37 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | History 2 | ======= 3 | 4 | ## Version 0.0.26 (next version) 5 | 6 | * Add `$PYPPETEER_NO_PROGRESS_BAR` environment variable 7 | * `pyppeteer.defaultArgs` now accepts that help infer chromium command-line flags. 8 | * `pyppeteer.launch()` argument `ignoreDefaultArgs` now accepts a list of flags to ignore. 9 | * `Page.type()` now supports typing emoji 10 | * `Page.pdf()` accepts a new argument `preferCSSPageSize` 11 | * Add new option `defaultViewport` to `launch()` and `connect()` 12 | * Add `BrowserContext.pages()` method 13 | 14 | ## Version 0.0.25 (2018-09-27) 15 | 16 | * Fix miss-spelled methods and functions 17 | * Change `Browser.isIncognite` to `Browser.isIncognito` 18 | * Change `Browser.createIncogniteBrowserContext` to `Browser.createIncognitoBrowserContext` 19 | * Change `chromium_excutable` to `chromium_executable` 20 | * Remove `craete` function in `page.py` 21 | 22 | ## Version 0.0.24 (2018-09-12) 23 | 24 | Catch up puppeteer v1.6.0 25 | 26 | * Add `ElementHandle.isIntersectingViewport()` 27 | * Add `reportAnonymousScript` option to `Coverage.startJSCoverage()` 28 | * Add `Page.waitForRequest` and `Page.waitForResponse` methods 29 | * Now possible to attach to extension background pages with `Target.page()` 30 | * Improved reliability of clicking with `Page.click()` and `ElementHandle.click()` 31 | 32 | ## Version 0.0.23 (2018-09-10) 33 | 34 | Catch up puppeteer v1.5.0 35 | 36 | * Add `BrowserContext` class 37 | * Add `Worker` class 38 | * Change `CDPSession.send` to a normal function which returns awaitable value 39 | * Add `Page.isClosed` method 40 | * Add `ElementHandle.querySelectorAllEval` and `ElementHandle.JJeval` 41 | * Add `Target.opener` 42 | * Add `Request.isNavigationRequest` 43 | 44 | ## Version 0.0.22 (2018-09-06) 45 | 46 | Catch up puppeteer v1.4.0 47 | 48 | * Add `pyppeteer.DEBUG` variable 49 | * Add `Page.browser` 50 | * Add `Target.browser` 51 | * Add `ElementHandle.querySelectorEval` and `ElementHandle.Jeval` 52 | * Add `runBeforeUnload` option to `Page.close` method 53 | * Change `Page.querySelectorEval` to raise `ElementHandleError` when element which matches `selector` is not found 54 | * Report 'Log' domain entries as 'console' events 55 | * Fix `Page.goto` to return response when page pushes new state 56 | * (OS X) Suppress long log when extracting chromium 57 | 58 | 59 | ## Version 0.0.21 (2018-08-21) 60 | 61 | Catch up puppeteer v1.3.0 62 | 63 | * Add `pyppeteer-install` command 64 | * Add `autoClose` option to `launch` function 65 | * Add `loop` option to `launch` function (experimental) 66 | * Add `Page.setBypassCSP` method 67 | * `Page.tracing.stop` returns result data 68 | * Rename `documentloaded` to `domcontentloaded` on `waitUntil` option 69 | * Fix `slowMo` option 70 | * Fix anchor navigation 71 | * Fix to return response via redirects 72 | * Continue to find WS URL while process is alive 73 | 74 | 75 | ## Version 0.0.20 (2018-08-11) 76 | 77 | * Run on msys/cygwin, anyway 78 | * Raise error correctly when connection failed (PR#91) 79 | * Change browser download location and temporary user data directory to: 80 | * If `$PYPPETEER_HOME` environment variable is defined, use this location 81 | * Otherwise, use platform dependent locations, based on [appdirs](https://pypi.org/project/appdirs/): 82 | * `'C:\Users\\AppData\Local\pyppeteer'` (Windows) 83 | * `'/Users//Library/Application Support/pyppeteer'` (OS X) 84 | * `'/home//.local/share/pyppeteer'` (Linux) 85 | * or in `'$XDG_DATA_HOME/pyppeteer'` if `$XDG_DATA_HOME` is defined 86 | 87 | * Introduce `$PYPPETEER_CHROMIUM_REVISION` 88 | * Introduce `$PYPPETEER_HOME` 89 | * Add `logLevel` option to `launch` and `connect` functions 90 | * Add page `close` event 91 | * Add `ElementHandle.boxModel` method 92 | * Add an option to disable timeout for `waitFor` functions 93 | 94 | 95 | ## Version 0.0.19 (2018-07-05) 96 | 97 | Catch up puppeteer v1.2.0 98 | 99 | * Add `ElementHandle.contentFrame` method 100 | * Add `Request.redirectChain` method 101 | * `Page.addScriptTag` accepts a new option `type` 102 | 103 | 104 | ## Version 0.0.18 (2018-07-04) 105 | 106 | Catch up puppeteer v1.1.1 107 | 108 | * Add `Page.waitForXPath` and `Frame.waitForXPath` 109 | * `Page.waitFor` accepts xpath string which starts with `//` 110 | * Add `Response.fromCache` and `Response.fromServiceWorker` 111 | * Add `SecurityDetails` class and `response.securityDetails` 112 | * Add `Page.setCacheEnabled` method 113 | * Add `ExecutionContext.frame` 114 | * Add `dumpio` option to `launch` function 115 | * Add `slowMo` option to `connect` function 116 | * `launcher.connect` can be access from package top 117 | * `from pyppeteer import connect` is now valid 118 | * Add `Frame.evaluateHandle` 119 | * Add `Page.Events.DOMContentLoaded` 120 | 121 | 122 | ## Version 0.0.17 (2018-04-02) 123 | 124 | * Mark as alpha 125 | 126 | * Gracefully terminate browser process 127 | * `Request.method` and `Request.postData` return `None` if no data 128 | * Change `Target.url` and `Target.type` to properties 129 | * Change `Dialog.message` and `Dialog.defaultValue` to properties 130 | * Fix: properly emit `Browser.targetChanged` events 131 | * Fix: properly emit `Browser.targetDestroyed` events 132 | 133 | 134 | ## Version 0.0.16 (2018-03-23) 135 | 136 | * BugFix: Skip SIGHUP option on windows (windows does not support this signal) 137 | 138 | 139 | ## Version 0.0.15 (2018-03-22) 140 | 141 | Catch up puppeteer v1.0.0 142 | 143 | * Support `raf` and `mutation` polling for `waitFor*` methods 144 | * Add `Page.coverage` to support JS and CSS coverage 145 | * Add XPath support with `Page.xpath`, `Frame.xpath`, and `ElementHandle.xpath` 146 | * Add `Target.createCDPSession` to work with raw Devtools Protocol 147 | * Change `Frame.executionContext` from property to coroutine 148 | * Add `ignoreDefaultArgs` option to `pyppeteer.launch` 149 | * Add `handleSIGINT`/`handleSIGTERM`/`handleSIGHUP` options to `pyppeteer.launch` 150 | * Add `Page.setDefaultNavigationTimeout` method 151 | * `Page.waitFor*` methods accept `JSHandle` as argument 152 | * Implement `Frame.content` and `Frame.setContent` methods 153 | * `page.tracing.start` accepts custom tracing categories option 154 | * Add `Browser.process` property 155 | * Add `Request.frame` property 156 | 157 | 158 | ## Version 0.0.14 (2018-03-14) 159 | 160 | * Read WS endpoint from web interface instead of stdout 161 | * Pass environment variables of python process to chrome by default 162 | * Do not limit size of websocket frames 163 | 164 | * BugFix: 165 | * `Keyboard.type` 166 | * `Page.Events.Metrics` 167 | 168 | ## Version 0.0.13 (2018-03-10) 169 | 170 | Catch up puppeteer v0.13.0 171 | 172 | * `pyppeteer.launch()` is now **coroutine** 173 | * Implement `connect` function 174 | * `PYPPETEER_DOWNLOAD_HOST` env variable specifies host part of URL to download chromium 175 | * Rename `setRequestInterceptionEnable` to `setRequestInterception` 176 | * Rename `Page.getMetrics` to `Page.metrics` 177 | * Implement `Browser.pages` to access all pages 178 | * Add `Target` class and some new method on Browser 179 | * Add `ElementHandle.querySelector` and `ElementHandle.querySelectorAll` 180 | * Refactor NavigatorWatcher 181 | * add `documentloaded`, `networkidle0`, and `networkidle2` options 182 | * `Request.abort` accepts error code 183 | * `addScriptTag` and `addStyleTag` return `ElementHandle` 184 | * Add `force_expr` option to `evaluate` method 185 | * `Page.select` returns selected values 186 | * Add `pyppeteer.version` and `pyppeteer.version_info` 187 | 188 | * BugFix: 189 | * Do not change original options dictionary 190 | * `Page.frames` 191 | * `Page.queryObjects` 192 | * `Page.exposeFunction` 193 | * Request interception 194 | * Console API 195 | * websocket error on closing browser (#24) 196 | 197 | ## Version 0.0.12 (2018-03-01) 198 | 199 | * BugFix (#33) 200 | 201 | ## Version 0.0.11 (2018-03-01) 202 | 203 | Catch up puppeteer v0.12.0 204 | 205 | * Remove `ElementHandle.evaluate` 206 | * Remove `ElementHandle.attribute` 207 | * Deprecate `Page.plainText` 208 | * Deprecate `Page.injectFile` 209 | * Add `Page.querySelectorAllEval` 210 | * Add `Page.select` and `Page.type` 211 | * Add `ElementHandle.boundingBox` and `ElementHandle.screenshot` 212 | * Add `ElementHandle.focus`, `ElementHandle.type`, and `ElementHandle.press` 213 | * Add `getMetrics` method 214 | * Add `offlineMode` 215 | 216 | ## Version 0.0.10 (2018-02-27) 217 | 218 | * Enable to import `launch` from package root 219 | * Change `browser.close` to coroutine function 220 | * Catch up puppeteer v0.11.0 221 | 222 | ### Version 0.0.9 (2017-09-09) 223 | 224 | * Delete temporary user data directory when browser closed 225 | * Fix bug to fail extracting zip on mac 226 | 227 | ### Version 0.0.8 (2017-09-03) 228 | 229 | * Change chromium revision 230 | * Support steps option of `Mouse.move()` 231 | * Experimentally supports python 3.5 by py-backwards 232 | 233 | ### Version 0.0.7 (2017-09-03) 234 | 235 | * Catch up puppeteer v0.10.2 236 | * Add `Page.querySelectorEval` (`Page.$eval` in puppeteer) 237 | * Deprecate `ElementHandle.attribute` 238 | * Add `Touchscreen` class and implement `Page.tap` and `ElementHandle.tap` 239 | 240 | ### Version 0.0.6 (2017-09-02) 241 | 242 | * Accept keyword arguments for options 243 | * Faster polling on `waitFor*` functions 244 | * Fix bugs 245 | 246 | ### Version 0.0.5 (2017-08-30) 247 | 248 | * Implement pdf printing 249 | * Implement `waitFor*` functions 250 | 251 | ### Version 0.0.4 (2017-08-30) 252 | 253 | * Register PyPI 254 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | MIT License 3 | 4 | Copyright (c) 2017, Hiroyuki Takagi 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 11 | 12 | This software includes the work that is distributed in the Apache License 2.0. 13 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include LICENSE 3 | include CHANGES.md 4 | 5 | recursive-include tests * 6 | recursive-exclude * __pycache__ 7 | recursive-exclude * *.py[co] 8 | 9 | recursive-include docs *.rst conf.py Makefile *.jpg *.png *.gif *.js *.css *.html 10 | prune docs/_build 11 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: clean clean-test clean-pyc clean-build docs help 2 | .DEFAULT_GOAL := help 3 | define BROWSER_PYSCRIPT 4 | import os, webbrowser, sys 5 | try: 6 | from urllib import pathname2url 7 | except: 8 | from urllib.request import pathname2url 9 | 10 | webbrowser.open("file://" + pathname2url(os.path.abspath(sys.argv[1]))) 11 | endef 12 | export BROWSER_PYSCRIPT 13 | 14 | define PRINT_HELP_PYSCRIPT 15 | import re, sys 16 | 17 | for line in sys.stdin: 18 | match = re.match(r'^([a-zA-Z_-]+):.*?## (.*)$$', line) 19 | if match: 20 | target, help = match.groups() 21 | print("%-20s %s" % (target, help)) 22 | endef 23 | export PRINT_HELP_PYSCRIPT 24 | BROWSER := python -c "$$BROWSER_PYSCRIPT" 25 | 26 | help: 27 | @python -c "$$PRINT_HELP_PYSCRIPT" < $(MAKEFILE_LIST) 28 | 29 | clean: clean-build clean-pyc clean-test ## remove all build, test, coverage and Python artifacts 30 | 31 | clean-build: ## remove build artifacts 32 | @echo "Remove build files (build/, dist/, .egg*, ...)." 33 | @rm -fr build/ 34 | @rm -fr dist/ 35 | @rm -fr .eggs/ 36 | @find . -name '*.egg-info' -exec rm -fr {} + 37 | @find . -name '*.egg' -exec rm -f {} + 38 | 39 | clean-pyc: ## remove Python file artifacts 40 | @echo "Remove python files (*.py[co], __pycache__, ...)." 41 | @find . -name '*.pyc' -exec rm -f {} + 42 | @find . -name '*.pyo' -exec rm -f {} + 43 | @find . -name '*~' -exec rm -f {} + 44 | @find . -name '__pycache__' -exec rm -fr {} + 45 | 46 | clean-test: ## remove test and coverage artifacts 47 | @echo "Remove test/coverage files (.coverage, htmlcov/)." 48 | @rm -f .coverage 49 | @rm -fr htmlcov/ 50 | 51 | .PHONY: green 52 | green: ## run green test 53 | @echo "Run green." 54 | @cd maint && \ 55 | green -c ../.green ../pyppeteer 56 | 57 | green-single: ## run green with a single process 58 | @echo "Run green with a single process." 59 | @cd maint && \ 60 | green -s 1 -c ../.green ../pyppeteer 61 | 62 | green-cov: # run green and calculate coverage 63 | @echo "Run green with coverage." 64 | @cd maint && \ 65 | green -r -c ../.green ../pyppeteer 66 | 67 | .PHONY: flake8 68 | flake8: ## run flake8 syntax check 69 | flake8 setup.py pyppeteer 70 | 71 | .PHONY: mypy 72 | mypy: ## run mypy type check 73 | mypy pyppeteer 74 | 75 | .PHONY: pydocstyle 76 | pydocstyle: ## run pydocstyle check 77 | pydocstyle pyppeteer 78 | 79 | # -n option is better but type hints refs are not found 80 | .PHONY: docs 81 | docs: ## build document 82 | @echo "Sphinx build start." 83 | @cd docs && \ 84 | sphinx-build -q -E -W -j auto -b html . _build/html && \ 85 | cd ../ 86 | @echo "Sphinx build done." 87 | 88 | .PHONY: sphinx 89 | sphinx: ## run document build server 90 | @echo "### Sphinx Build Server Start ###" 91 | @python docs/server.py 92 | 93 | .PHONY: spell 94 | spell: ## run spell check on comments and docstring 95 | @pylint --disable all --enable spelling --spelling-dict en_US --spelling-private-dict-file spell.txt pyppeteer 96 | 97 | .PHONY: check 98 | check: ## run flake8, mypy, pydocstyle, sphinx-build 99 | @doit --verbosity 1 --process 4 --parallel-type thread 100 | 101 | .PHONY: test 102 | test: check green-cov ## run style check and test 103 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Pyppeteer 2 | ========= 3 | 4 | Pyppeteer has moved to [pyppeteer/pyppeteer](https://github.com/pyppeteer/pyppeteer) 5 | ==================================================================================== 6 | 7 | --- 8 | 9 | [![PyPI](https://img.shields.io/pypi/v/pyppeteer.svg)](https://pypi.python.org/pypi/pyppeteer) 10 | [![PyPI version](https://img.shields.io/pypi/pyversions/pyppeteer.svg)](https://pypi.python.org/pypi/pyppeteer) 11 | [![Documentation](https://img.shields.io/badge/docs-latest-brightgreen.svg)](https://miyakogi.github.io/pyppeteer) 12 | [![Travis status](https://travis-ci.org/miyakogi/pyppeteer.svg)](https://travis-ci.org/miyakogi/pyppeteer) 13 | [![AppVeyor status](https://ci.appveyor.com/api/projects/status/nb53tkg9po8v1blk?svg=true)](https://ci.appveyor.com/project/miyakogi/pyppeteer) 14 | [![codecov](https://codecov.io/gh/miyakogi/pyppeteer/branch/master/graph/badge.svg)](https://codecov.io/gh/miyakogi/pyppeteer) 15 | 16 | Unofficial Python port of 17 | [puppeteer](https://github.com/GoogleChrome/puppeteer) JavaScript (headless) 18 | chrome/chromium browser automation library. 19 | 20 | * Free software: MIT license (including the work distributed under the Apache 2.0 license) 21 | * Documentation: https://miyakogi.github.io/pyppeteer 22 | 23 | ## Installation 24 | 25 | Pyppeteer requires python 3.6+. 26 | (experimentally supports python 3.5) 27 | 28 | Install by pip from PyPI: 29 | 30 | ``` 31 | python3 -m pip install pyppeteer 32 | ``` 33 | 34 | Or install latest version from [github](https://github.com/miyakogi/pyppeteer): 35 | 36 | ``` 37 | python3 -m pip install -U git+https://github.com/miyakogi/pyppeteer.git@dev 38 | ``` 39 | 40 | ## Usage 41 | 42 | > **Note**: When you run pyppeteer first time, it downloads a recent version of Chromium (~100MB). 43 | > If you don't prefer this behavior, run `pyppeteer-install` command before running scripts which uses pyppeteer. 44 | 45 | **Example**: open web page and take a screenshot. 46 | 47 | ```py 48 | import asyncio 49 | from pyppeteer import launch 50 | 51 | async def main(): 52 | browser = await launch() 53 | page = await browser.newPage() 54 | await page.goto('http://example.com') 55 | await page.screenshot({'path': 'example.png'}) 56 | await browser.close() 57 | 58 | asyncio.get_event_loop().run_until_complete(main()) 59 | ``` 60 | 61 | **Example**: evaluate script on the page. 62 | 63 | ```py 64 | import asyncio 65 | from pyppeteer import launch 66 | 67 | async def main(): 68 | browser = await launch() 69 | page = await browser.newPage() 70 | await page.goto('http://example.com') 71 | await page.screenshot({'path': 'example.png'}) 72 | 73 | dimensions = await page.evaluate('''() => { 74 | return { 75 | width: document.documentElement.clientWidth, 76 | height: document.documentElement.clientHeight, 77 | deviceScaleFactor: window.devicePixelRatio, 78 | } 79 | }''') 80 | 81 | print(dimensions) 82 | # >>> {'width': 800, 'height': 600, 'deviceScaleFactor': 1} 83 | await browser.close() 84 | 85 | asyncio.get_event_loop().run_until_complete(main()) 86 | ``` 87 | 88 | Pyppeteer has almost same API as puppeteer. 89 | More APIs are listed in the 90 | [document](https://miyakogi.github.io/pyppeteer/reference.html). 91 | 92 | [Puppeteer's document](https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#) 93 | and [troubleshooting](https://github.com/GoogleChrome/puppeteer/blob/master/docs/troubleshooting.md) are also useful for pyppeteer users. 94 | 95 | ## Differences between puppeteer and pyppeteer 96 | 97 | Pyppeteer is to be as similar as puppeteer, but some differences between python 98 | and JavaScript make it difficult. 99 | 100 | These are differences between puppeteer and pyppeteer. 101 | 102 | ### Keyword arguments for options 103 | 104 | Puppeteer uses object (dictionary in python) for passing options to 105 | functions/methods. Pyppeteer accepts both dictionary and keyword arguments for 106 | options. 107 | 108 | Dictionary style option (similar to puppeteer): 109 | 110 | ```python 111 | browser = await launch({'headless': True}) 112 | ``` 113 | 114 | Keyword argument style option (more pythonic, isn't it?): 115 | 116 | ```python 117 | browser = await launch(headless=True) 118 | ``` 119 | 120 | ### Element selector method name (`$` -> `querySelector`) 121 | 122 | In python, `$` is not usable for method name. 123 | So pyppeteer uses 124 | `Page.querySelector()`/`Page.querySelectorAll()`/`Page.xpath()` instead of 125 | `Page.$()`/`Page.$$()`/`Page.$x()`. Pyppeteer also has shorthands for these 126 | methods, `Page.J()`, `Page.JJ()`, and `Page.Jx()`. 127 | 128 | ### Arguments of `Page.evaluate()` and `Page.querySelectorEval()` 129 | 130 | Puppeteer's version of `evaluate()` takes JavaScript raw function or string of 131 | JavaScript expression, but pyppeteer takes string of JavaScript. JavaScript 132 | strings can be function or expression. Pyppeteer tries to automatically detect 133 | the string is function or expression, but sometimes it fails. If expression 134 | string is treated as function and error is raised, add `force_expr=True` option, 135 | which force pyppeteer to treat the string as expression. 136 | 137 | Example to get page content: 138 | 139 | ```python 140 | content = await page.evaluate('document.body.textContent', force_expr=True) 141 | ``` 142 | 143 | Example to get element's inner text: 144 | 145 | ```python 146 | element = await page.querySelector('h1') 147 | title = await page.evaluate('(element) => element.textContent', element) 148 | ``` 149 | 150 | ## Future Plan 151 | 152 | 1. Catch up development of puppeteer 153 | * Not intend to add original API which puppeteer does not have 154 | 155 | ## Credits 156 | 157 | This package was created with [Cookiecutter](https://github.com/audreyr/cookiecutter) and the [audreyr/cookiecutter-pypackage](https://github.com/audreyr/cookiecutter-pypackage) project template. 158 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | install: 2 | - C:\Python37\python.exe -V 3 | - C:\Python37\python.exe -m pip install -U pip setuptools 4 | - C:\Python37\python.exe -m pip install tox 5 | 6 | test_script: 7 | - C:\Python37\python.exe -m tox -e py37 8 | 9 | build: off 10 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | 49 | clean: 50 | rm -rf $(BUILDDIR)/* 51 | 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | dirhtml: 58 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 59 | @echo 60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 61 | 62 | singlehtml: 63 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 64 | @echo 65 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 66 | 67 | pickle: 68 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 69 | @echo 70 | @echo "Build finished; now you can process the pickle files." 71 | 72 | json: 73 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 74 | @echo 75 | @echo "Build finished; now you can process the JSON files." 76 | 77 | htmlhelp: 78 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 79 | @echo 80 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 81 | ".hhp project file in $(BUILDDIR)/htmlhelp." 82 | 83 | qthelp: 84 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 85 | @echo 86 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 87 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 88 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/pyppeteer.qhcp" 89 | @echo "To view the help file:" 90 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/pyppeteer.qhc" 91 | 92 | devhelp: 93 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 94 | @echo 95 | @echo "Build finished." 96 | @echo "To view the help file:" 97 | @echo "# mkdir -p $$HOME/.local/share/devhelp/pyppeteer" 98 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/pyppeteer" 99 | @echo "# devhelp" 100 | 101 | epub: 102 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 103 | @echo 104 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 105 | 106 | latex: 107 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 108 | @echo 109 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 110 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 111 | "(use \`make latexpdf' here to do that automatically)." 112 | 113 | latexpdf: 114 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 115 | @echo "Running LaTeX files through pdflatex..." 116 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 117 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 118 | 119 | latexpdfja: 120 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 121 | @echo "Running LaTeX files through platex and dvipdfmx..." 122 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 123 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 124 | 125 | text: 126 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 127 | @echo 128 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 129 | 130 | man: 131 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 132 | @echo 133 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 134 | 135 | texinfo: 136 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 137 | @echo 138 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 139 | @echo "Run \`make' in that directory to run these through makeinfo" \ 140 | "(use \`make info' here to do that automatically)." 141 | 142 | info: 143 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 144 | @echo "Running Texinfo files through makeinfo..." 145 | make -C $(BUILDDIR)/texinfo info 146 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 147 | 148 | gettext: 149 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 150 | @echo 151 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 152 | 153 | changes: 154 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 155 | @echo 156 | @echo "The overview file is in $(BUILDDIR)/changes." 157 | 158 | linkcheck: 159 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 160 | @echo 161 | @echo "Link check complete; look for any errors in the above output " \ 162 | "or in $(BUILDDIR)/linkcheck/output.txt." 163 | 164 | doctest: 165 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 166 | @echo "Testing of doctests in the sources finished, look at the " \ 167 | "results in $(BUILDDIR)/doctest/output.txt." 168 | 169 | xml: 170 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 171 | @echo 172 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 173 | 174 | pseudoxml: 175 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 176 | @echo 177 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 178 | -------------------------------------------------------------------------------- /docs/_static/custom.css: -------------------------------------------------------------------------------- 1 | h1.logo { 2 | font-family: "Raleway"; 3 | font-weight: 500; 4 | } 5 | 6 | a.headerlink { 7 | color: rgba(0, 0, 0, 0.1); 8 | } 9 | 10 | div.sphinxsidebarwrapper p.blurb { 11 | font-family: Lato, sans-serif; 12 | } 13 | 14 | div.sphinxsidebar li.toctree-l1 { 15 | font-family: Lato, sans-serif; 16 | } 17 | 18 | body { 19 | background-color: #fafafa 20 | } 21 | 22 | .search-btn { 23 | padding: 0 1em; 24 | font-family: Lato, sans-serif; 25 | font-weight: normal; 26 | line-height: normal; 27 | align-self: stretch; 28 | } 29 | -------------------------------------------------------------------------------- /docs/_templates/layout.html: -------------------------------------------------------------------------------- 1 | {% extends 'alabaster/layout.html' %} 2 | {% block extrahead %} 3 | 4 | 5 | 6 | 7 | 8 | 9 | {{ super() }} 10 | {% endblock %} 11 | -------------------------------------------------------------------------------- /docs/changes.md: -------------------------------------------------------------------------------- 1 | .. mdinclude:: ../CHANGES.md 2 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # pyppeteer documentation build configuration file, created by 5 | # sphinx-quickstart on Tue Jul 9 22:26:36 2013. 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | 16 | import sys 17 | import os 18 | 19 | # If extensions (or modules to document with autodoc) are in another 20 | # directory, add these directories to sys.path here. If the directory is 21 | # relative to the documentation root, use os.path.abspath to make it 22 | # absolute, like shown here. 23 | # sys.path.insert(0, os.path.abspath('.')) 24 | 25 | # Get the project root dir, which is the parent dir of this 26 | cwd = os.getcwd() 27 | project_root = os.path.dirname(cwd) 28 | 29 | # Insert the project root dir as the first element in the PYTHONPATH. 30 | # This lets us ensure that the source package is imported, and that its 31 | # version is used. 32 | sys.path.insert(0, project_root) 33 | 34 | import pyppeteer 35 | 36 | # -- General configuration --------------------------------------------- 37 | 38 | # If your documentation needs a minimal Sphinx version, state it here. 39 | # needs_sphinx = '1.0' 40 | 41 | # Add any Sphinx extension module names here, as strings. They can be 42 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 43 | extensions = [ 44 | 'sphinx.ext.autodoc', 45 | 'sphinx.ext.githubpages', 46 | 'sphinx.ext.viewcode', 47 | # 'sphinx_autodoc_typehints', 48 | 'sphinxcontrib.asyncio', 49 | 'm2r', 50 | ] 51 | 52 | primary_domain = 'py' 53 | default_role = 'py:obj' 54 | # autodoc_member_order = 'bysource' 55 | # include class' and __init__'s docstring 56 | # autoclass_content = 'both' 57 | # autodoc_docstring_signature = False 58 | autodoc_default_flags = ['show-inheritance'] 59 | 60 | suppress_warnings = ['image.nonlocal_uri'] 61 | 62 | # Add any paths that contain templates here, relative to this directory. 63 | templates_path = ['_templates'] 64 | 65 | # The suffix(es) of source filenames. 66 | # You can specify multiple suffix as a list of string: 67 | source_suffix = ['.rst', '.md'] 68 | 69 | # The encoding of source files. 70 | # source_encoding = 'utf-8-sig' 71 | 72 | # The master toctree document. 73 | master_doc = 'index' 74 | 75 | # General information about the project. 76 | project = 'Pyppeteer' 77 | copyright = "2017, Hiroyuki Takagi" 78 | 79 | # The version info for the project you're documenting, acts as replacement 80 | # for |version| and |release|, also used in various other places throughout 81 | # the built documents. 82 | # 83 | # The short X.Y version. 84 | version = pyppeteer.__version__ 85 | # The full version, including alpha/beta/rc tags. 86 | release = pyppeteer.__version__ 87 | 88 | # The language for content autogenerated by Sphinx. Refer to documentation 89 | # for a list of supported languages. 90 | # language = None 91 | 92 | # There are two options for replacing |today|: either, you set today to 93 | # some non-false value, then it is used: 94 | # today = '' 95 | # Else, today_fmt is used as the format for a strftime call. 96 | # today_fmt = '%B %d, %Y' 97 | 98 | # List of patterns, relative to source directory, that match files and 99 | # directories to ignore when looking for source files. 100 | exclude_patterns = ['_build'] 101 | 102 | # The reST default role (used for this markup: `text`) to use for all 103 | # documents. 104 | # default_role = None 105 | 106 | # If true, '()' will be appended to :func: etc. cross-reference text. 107 | # add_function_parentheses = True 108 | 109 | # If true, the current module name will be prepended to all description 110 | # unit titles (such as .. function::). 111 | # add_module_names = True 112 | 113 | # If true, sectionauthor and moduleauthor directives will be shown in the 114 | # output. They are ignored by default. 115 | # show_authors = False 116 | 117 | # The name of the Pygments (syntax highlighting) style to use. 118 | pygments_style = 'sphinx' 119 | 120 | # A list of ignored prefixes for module index sorting. 121 | # modindex_common_prefix = [] 122 | 123 | # If true, keep warnings as "system message" paragraphs in the built 124 | # documents. 125 | # keep_warnings = False 126 | 127 | 128 | # -- Options for HTML output ------------------------------------------- 129 | 130 | # The theme to use for HTML and HTML Help pages. See the documentation for 131 | # a list of builtin themes. 132 | html_theme = 'alabaster' 133 | 134 | # Theme options are theme-specific and customize the look and feel of a 135 | # theme further. For a list of options available for each theme, see the 136 | # documentation. 137 | html_theme_options = { 138 | 'description': ('Headless chrome/chromium automation library ' 139 | '(unofficial port of puppeteer)'), 140 | 'github_user': 'miyakogi', 141 | 'github_repo': 'pyppeteer', 142 | 'github_banner': True, 143 | 'github_type': 'mark', 144 | 'github_count': False, 145 | 'font_family': '"Charis SIL", "Noto Serif", serif', 146 | 'head_font_family': 'Lato, sans-serif', 147 | 'code_font_family': '"Code new roman", "Ubuntu Mono", monospace', 148 | 'code_font_size': '1rem', 149 | } 150 | 151 | # Add any paths that contain custom themes here, relative to this directory. 152 | # html_theme_path = [] 153 | 154 | # The name for this set of Sphinx documents. If None, it defaults to 155 | # " v documentation". 156 | # html_title = None 157 | 158 | # A shorter title for the navigation bar. Default is the same as 159 | # html_title. 160 | # html_short_title = None 161 | 162 | # The name of an image file (relative to this directory) to place at the 163 | # top of the sidebar. 164 | # html_logo = None 165 | 166 | # The name of an image file (within the static path) to use as favicon 167 | # of the docs. This file should be a Windows icon file (.ico) being 168 | # 16x16 or 32x32 pixels large. 169 | # html_favicon = None 170 | 171 | # Add any paths that contain custom static files (such as style sheets) 172 | # here, relative to this directory. They are copied after the builtin 173 | # static files, so a file named "default.css" will overwrite the builtin 174 | # "default.css". 175 | html_static_path = ['_static'] 176 | 177 | # If not '', a 'Last updated on:' timestamp is inserted at every page 178 | # bottom, using the given strftime format. 179 | # html_last_updated_fmt = '%b %d, %Y' 180 | 181 | # If true, SmartyPants will be used to convert quotes and dashes to 182 | # typographically correct entities. 183 | # html_use_smartypants = True 184 | 185 | # Custom sidebar templates, maps document names to template names. 186 | html_sidebars = { 187 | '**': [ 188 | 'about.html', 189 | 'navigation.html', 190 | 'relations.html', 191 | 'searchbox.html', 192 | ] 193 | } 194 | 195 | # Additional templates that should be rendered to pages, maps page names 196 | # to template names. 197 | # html_additional_pages = {} 198 | 199 | # If false, no module index is generated. 200 | # html_domain_indices = True 201 | 202 | # If false, no index is generated. 203 | # html_use_index = True 204 | 205 | # If true, the index is split into individual pages for each letter. 206 | # html_split_index = False 207 | 208 | # If true, links to the reST sources are added to the pages. 209 | # html_show_sourcelink = True 210 | 211 | # If true, "Created using Sphinx" is shown in the HTML footer. 212 | # Default is True. 213 | # html_show_sphinx = True 214 | 215 | # If true, "(C) Copyright ..." is shown in the HTML footer. 216 | # Default is True. 217 | # html_show_copyright = True 218 | 219 | # If true, an OpenSearch description file will be output, and all pages 220 | # will contain a tag referring to it. The value of this option 221 | # must be the base URL from which the finished HTML is served. 222 | # html_use_opensearch = '' 223 | 224 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 225 | # html_file_suffix = None 226 | 227 | # Output file base name for HTML help builder. 228 | htmlhelp_basename = 'pyppeteerdoc' 229 | 230 | # -- Options for LaTeX output ------------------------------------------ 231 | 232 | latex_elements = { 233 | # The paper size ('letterpaper' or 'a4paper'). 234 | # 'papersize': 'letterpaper', 235 | 236 | # The font size ('10pt', '11pt' or '12pt'). 237 | # 'pointsize': '10pt', 238 | 239 | # Additional stuff for the LaTeX preamble. 240 | # 'preamble': '', 241 | } 242 | 243 | # Grouping the document tree into LaTeX files. List of tuples 244 | # (source start file, target name, title, author, documentclass 245 | # [howto/manual]). 246 | latex_documents = [ 247 | ('index', 'pyppeteer.tex', 248 | 'pyppeteer Documentation', 249 | 'Hiroyuki Takagi', 'manual'), 250 | ] 251 | 252 | # The name of an image file (relative to this directory) to place at 253 | # the top of the title page. 254 | # latex_logo = None 255 | 256 | # For "manual" documents, if this is true, then toplevel headings 257 | # are parts, not chapters. 258 | # latex_use_parts = False 259 | 260 | # If true, show page references after internal links. 261 | # latex_show_pagerefs = False 262 | 263 | # If true, show URL addresses after external links. 264 | # latex_show_urls = False 265 | 266 | # Documents to append as an appendix to all manuals. 267 | # latex_appendices = [] 268 | 269 | # If false, no module index is generated. 270 | # latex_domain_indices = True 271 | 272 | 273 | # -- Options for manual page output ------------------------------------ 274 | 275 | # One entry per manual page. List of tuples 276 | # (source start file, name, description, authors, manual section). 277 | man_pages = [ 278 | ('index', 'pyppeteer', 279 | 'pyppeteer Documentation', 280 | ['Hiroyuki Takagi'], 1) 281 | ] 282 | 283 | # If true, show URL addresses after external links. 284 | # man_show_urls = False 285 | 286 | 287 | # -- Options for Texinfo output ---------------------------------------- 288 | 289 | # Grouping the document tree into Texinfo files. List of tuples 290 | # (source start file, target name, title, author, 291 | # dir menu entry, description, category) 292 | texinfo_documents = [ 293 | ('index', 'pyppeteer', 294 | 'pyppeteer Documentation', 295 | 'Hiroyuki Takagi', 296 | 'pyppeteer', 297 | 'One line description of project.', 298 | 'Miscellaneous'), 299 | ] 300 | 301 | # Documents to append as an appendix to all manuals. 302 | # texinfo_appendices = [] 303 | 304 | # If false, no module index is generated. 305 | # texinfo_domain_indices = True 306 | 307 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 308 | # texinfo_show_urls = 'footnote' 309 | 310 | # If true, do not generate a @detailmenu in the "Top" node's menu. 311 | # texinfo_no_detailmenu = False 312 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | Pyppeteer's documentation 2 | ========================= 3 | 4 | .. mdinclude:: ../README.md 5 | 6 | 7 | Contents 8 | -------- 9 | 10 | .. toctree:: 11 | :maxdepth: 2 12 | 13 | reference 14 | changes 15 | 16 | Indices and tables 17 | ================== 18 | 19 | * :ref:`genindex` 20 | * :ref:`modindex` 21 | * :ref:`search` 22 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. xml to make Docutils-native XML files 37 | echo. pseudoxml to make pseudoxml-XML files for display purposes 38 | echo. linkcheck to check all external links for integrity 39 | echo. doctest to run all doctests embedded in the documentation if enabled 40 | goto end 41 | ) 42 | 43 | if "%1" == "clean" ( 44 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 45 | del /q /s %BUILDDIR%\* 46 | goto end 47 | ) 48 | 49 | 50 | %SPHINXBUILD% 2> nul 51 | if errorlevel 9009 ( 52 | echo. 53 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 54 | echo.installed, then set the SPHINXBUILD environment variable to point 55 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 56 | echo.may add the Sphinx directory to PATH. 57 | echo. 58 | echo.If you don't have Sphinx installed, grab it from 59 | echo.http://sphinx-doc.org/ 60 | exit /b 1 61 | ) 62 | 63 | if "%1" == "html" ( 64 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 65 | if errorlevel 1 exit /b 1 66 | echo. 67 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 68 | goto end 69 | ) 70 | 71 | if "%1" == "dirhtml" ( 72 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 73 | if errorlevel 1 exit /b 1 74 | echo. 75 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 76 | goto end 77 | ) 78 | 79 | if "%1" == "singlehtml" ( 80 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 81 | if errorlevel 1 exit /b 1 82 | echo. 83 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 84 | goto end 85 | ) 86 | 87 | if "%1" == "pickle" ( 88 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 89 | if errorlevel 1 exit /b 1 90 | echo. 91 | echo.Build finished; now you can process the pickle files. 92 | goto end 93 | ) 94 | 95 | if "%1" == "json" ( 96 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 97 | if errorlevel 1 exit /b 1 98 | echo. 99 | echo.Build finished; now you can process the JSON files. 100 | goto end 101 | ) 102 | 103 | if "%1" == "htmlhelp" ( 104 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 105 | if errorlevel 1 exit /b 1 106 | echo. 107 | echo.Build finished; now you can run HTML Help Workshop with the ^ 108 | .hhp project file in %BUILDDIR%/htmlhelp. 109 | goto end 110 | ) 111 | 112 | if "%1" == "qthelp" ( 113 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 114 | if errorlevel 1 exit /b 1 115 | echo. 116 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 117 | .qhcp project file in %BUILDDIR%/qthelp, like this: 118 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\pyppeteer.qhcp 119 | echo.To view the help file: 120 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\pyppeteer.ghc 121 | goto end 122 | ) 123 | 124 | if "%1" == "devhelp" ( 125 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished. 129 | goto end 130 | ) 131 | 132 | if "%1" == "epub" ( 133 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 134 | if errorlevel 1 exit /b 1 135 | echo. 136 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 137 | goto end 138 | ) 139 | 140 | if "%1" == "latex" ( 141 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 142 | if errorlevel 1 exit /b 1 143 | echo. 144 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 145 | goto end 146 | ) 147 | 148 | if "%1" == "latexpdf" ( 149 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 150 | cd %BUILDDIR%/latex 151 | make all-pdf 152 | cd %BUILDDIR%/.. 153 | echo. 154 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 155 | goto end 156 | ) 157 | 158 | if "%1" == "latexpdfja" ( 159 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 160 | cd %BUILDDIR%/latex 161 | make all-pdf-ja 162 | cd %BUILDDIR%/.. 163 | echo. 164 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 165 | goto end 166 | ) 167 | 168 | if "%1" == "text" ( 169 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 170 | if errorlevel 1 exit /b 1 171 | echo. 172 | echo.Build finished. The text files are in %BUILDDIR%/text. 173 | goto end 174 | ) 175 | 176 | if "%1" == "man" ( 177 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 178 | if errorlevel 1 exit /b 1 179 | echo. 180 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 181 | goto end 182 | ) 183 | 184 | if "%1" == "texinfo" ( 185 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 186 | if errorlevel 1 exit /b 1 187 | echo. 188 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 189 | goto end 190 | ) 191 | 192 | if "%1" == "gettext" ( 193 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 194 | if errorlevel 1 exit /b 1 195 | echo. 196 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 197 | goto end 198 | ) 199 | 200 | if "%1" == "changes" ( 201 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 202 | if errorlevel 1 exit /b 1 203 | echo. 204 | echo.The overview file is in %BUILDDIR%/changes. 205 | goto end 206 | ) 207 | 208 | if "%1" == "linkcheck" ( 209 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 210 | if errorlevel 1 exit /b 1 211 | echo. 212 | echo.Link check complete; look for any errors in the above output ^ 213 | or in %BUILDDIR%/linkcheck/output.txt. 214 | goto end 215 | ) 216 | 217 | if "%1" == "doctest" ( 218 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 219 | if errorlevel 1 exit /b 1 220 | echo. 221 | echo.Testing of doctests in the sources finished, look at the ^ 222 | results in %BUILDDIR%/doctest/output.txt. 223 | goto end 224 | ) 225 | 226 | if "%1" == "xml" ( 227 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 228 | if errorlevel 1 exit /b 1 229 | echo. 230 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 231 | goto end 232 | ) 233 | 234 | if "%1" == "pseudoxml" ( 235 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 236 | if errorlevel 1 exit /b 1 237 | echo. 238 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 239 | goto end 240 | ) 241 | 242 | :end 243 | -------------------------------------------------------------------------------- /docs/reference.md: -------------------------------------------------------------------------------- 1 | API Reference 2 | ============= 3 | 4 | Commands 5 | -------- 6 | 7 | * ``pyppeteer-install``: Download and install chromium for pyppeteer. 8 | 9 | Environment Variables 10 | --------------------- 11 | 12 | * ``$PYPPETEER_HOME``: Specify the directory to be used by pyppeteer. 13 | Pyppeteer uses this directory for extracting downloaded Chromium, and for 14 | making temporary user data directory. 15 | Default location depends on platform: 16 | * Windows: `C:\Users\\AppData\Local\pyppeteer` 17 | * OS X: `/Users//Library/Application Support/pyppeteer` 18 | * Linux: `/home//.local/share/pyppeteer` 19 | * or in `$XDG_DATA_HOME/pyppeteer` if `$XDG_DATA_HOME` is defined. 20 | 21 | Details see [appdirs](https://pypi.org/project/appdirs/)'s `user_data_dir`. 22 | 23 | * ``$PYPPETEER_DOWNLOAD_HOST``: Overwrite host part of URL that is used to 24 | download Chromium. Defaults to ``https://storage.googleapis.com``. 25 | 26 | * ``$PYPPETEER_CHROMIUM_REVISION``: Specify a certain version of chromium you'd 27 | like pyppeteer to use. Default value can be checked by 28 | ``pyppeteer.__chromium_revision__``. 29 | 30 | * ``$PYPPETEER_NO_PROGRESS_BAR``: Suppress showing progress bar in chromium 31 | download process. Acceptable values are ``1`` or ``true`` (case-insensitive). 32 | 33 | 34 | Pyppeteer Main Module 35 | --------------------- 36 | 37 | .. currentmodule:: pyppeteer 38 | 39 | .. autofunction:: launch 40 | .. autofunction:: connect 41 | .. autofunction:: defaultArgs 42 | .. autofunction:: executablePath 43 | 44 | Browser Class 45 | ------------- 46 | 47 | .. currentmodule:: pyppeteer.browser 48 | 49 | .. autoclass:: pyppeteer.browser.Browser 50 | :members: 51 | :exclude-members: create 52 | 53 | BrowserContext Class 54 | -------------------- 55 | 56 | .. currentmodule:: pyppeteer.browser 57 | 58 | .. autoclass:: pyppeteer.browser.BrowserContext 59 | :members: 60 | 61 | Page Class 62 | ---------- 63 | 64 | .. currentmodule:: pyppeteer.page 65 | 66 | .. autoclass:: pyppeteer.page.Page 67 | :members: 68 | :exclude-members: create 69 | 70 | Worker Class 71 | ------------ 72 | 73 | .. currentmodule:: pyppeteer.worker 74 | 75 | .. autoclass:: pyppeteer.worker.Worker 76 | :members: 77 | 78 | Keyboard Class 79 | -------------- 80 | 81 | .. currentmodule:: pyppeteer.input 82 | 83 | .. autoclass:: pyppeteer.input.Keyboard 84 | :members: 85 | 86 | Mouse Class 87 | ----------- 88 | 89 | .. currentmodule:: pyppeteer.input 90 | 91 | .. autoclass:: pyppeteer.input.Mouse 92 | :members: 93 | 94 | Tracing Class 95 | ------------- 96 | 97 | .. currentmodule:: pyppeteer.tracing 98 | 99 | .. autoclass:: pyppeteer.tracing.Tracing 100 | :members: 101 | 102 | Dialog Class 103 | ------------ 104 | 105 | .. currentmodule:: pyppeteer.dialog 106 | 107 | .. autoclass:: pyppeteer.dialog.Dialog 108 | :members: 109 | 110 | ConsoleMessage Class 111 | -------------------- 112 | 113 | .. currentmodule:: pyppeteer.page 114 | 115 | .. autoclass:: pyppeteer.page.ConsoleMessage 116 | :members: 117 | 118 | Frame Class 119 | ----------- 120 | 121 | .. currentmodule:: pyppeteer.frame 122 | 123 | .. autoclass:: pyppeteer.frame_manager.Frame 124 | :members: 125 | 126 | ExecutionContext Class 127 | ---------------------- 128 | 129 | .. currentmodule:: pyppeteer.execution_context 130 | 131 | .. autoclass:: pyppeteer.execution_context.ExecutionContext 132 | :members: 133 | 134 | JSHandle Class 135 | -------------- 136 | 137 | .. autoclass:: pyppeteer.execution_context.JSHandle 138 | :members: 139 | 140 | ElementHandle Class 141 | ------------------- 142 | 143 | .. currentmodule:: pyppeteer.element_handle 144 | 145 | .. autoclass:: pyppeteer.element_handle.ElementHandle 146 | :members: 147 | 148 | Request Class 149 | ------------- 150 | 151 | .. currentmodule:: pyppeteer.network_manager 152 | 153 | .. autoclass:: pyppeteer.network_manager.Request 154 | :members: 155 | 156 | Response Class 157 | -------------- 158 | 159 | .. currentmodule:: pyppeteer.network_manager 160 | 161 | .. autoclass:: pyppeteer.network_manager.Response 162 | :members: 163 | 164 | Target Class 165 | ------------ 166 | 167 | .. currentmodule:: pyppeteer.target 168 | 169 | .. autoclass:: pyppeteer.target.Target 170 | :members: 171 | 172 | CDPSession Class 173 | ---------------- 174 | 175 | .. currentmodule:: pyppeteer.connection 176 | 177 | .. autoclass:: pyppeteer.connection.CDPSession 178 | :members: 179 | 180 | Coverage Class 181 | -------------- 182 | 183 | .. currentmodule:: pyppeteer.coverage 184 | 185 | .. autoclass:: pyppeteer.coverage.Coverage 186 | :members: 187 | 188 | Debugging 189 | --------- 190 | 191 | For debugging, you can set `logLevel` option to `logging.DEBUG` for 192 | :func:`pyppeteer.launcher.launch` and :func:`pyppeteer.launcher.connect` 193 | functions. However, this option prints too many logs including SEND/RECV 194 | messages of pyppeteer. In order to only show suppressed error messages, you 195 | should set ``pyppeteer.DEBUG`` to ``True``. 196 | 197 | Example: 198 | 199 | ```python 200 | import asyncio 201 | import pyppeteer 202 | from pyppeteer import launch 203 | 204 | pyppeteer.DEBUG = True # print suppressed errors as error log 205 | 206 | async def main(): 207 | browser = await launch() 208 | ... # do something 209 | 210 | asyncio.get_event_loop().run_until_complete(main()) 211 | ``` 212 | -------------------------------------------------------------------------------- /docs/server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | from os import path 5 | import subprocess 6 | 7 | from livereload import Server 8 | from livereload import watcher 9 | 10 | watcher.pyinotify = None # disable pyinotify 11 | 12 | docsdir = path.dirname(path.abspath(__file__)) 13 | builddir = path.join(docsdir, '_build') 14 | build_cmd = [ 15 | 'sphinx-build', '-q', '-j', 'auto', '-b', 'html', 16 | '-d', path.join(builddir, 'doctrees'), 17 | docsdir, path.join(builddir, 'html'), 18 | ] 19 | 20 | 21 | def cmd() -> None: 22 | print('=== Sphinx Build Start ===') 23 | subprocess.run(build_cmd, cwd=docsdir) 24 | print('=== Sphinx Build done ===') 25 | 26 | 27 | # subprocess.run(['make', 'clean'], cwd=docsdir) 28 | cmd() 29 | server = Server() 30 | 31 | 32 | def docs(p: str) -> str: 33 | return path.join(docsdir, p) 34 | 35 | 36 | # Watch documents 37 | server.watch(docs('*.py'), cmd, delay=1) 38 | server.watch(docs('*.md'), cmd, delay=1) 39 | server.watch(docs('../*.md'), cmd, delay=1) 40 | server.watch(docs('*.md'), cmd, delay=1) 41 | server.watch(docs('*/*.md'), cmd, delay=1) 42 | server.watch(docs('*/*/*.md'), cmd, delay=1) 43 | 44 | # Watch template/style 45 | server.watch(docs('_templates/*.html'), cmd, delay=1) 46 | server.watch(docs('_static/*.css'), cmd, delay=1) 47 | server.watch(docs('_static/*.js'), cmd, delay=1) 48 | 49 | # Watch package 50 | server.watch(docs('../pyppeteer/*.py'), cmd, delay=1) 51 | server.watch(docs('../pyppeteer/*/*.py'), cmd, delay=1) 52 | server.watch(docs('../pyppeteer/*/*/*.py'), cmd, delay=1) 53 | 54 | server.serve(port=8889, root=docs('_build/html'), debug=True, restart_delay=1) 55 | -------------------------------------------------------------------------------- /dodo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | """Doit task definitions.""" 5 | 6 | import multiprocessing 7 | 8 | from doit.action import CmdAction 9 | 10 | cores = multiprocessing.cpu_count() 11 | DOIT_CONFIG = { 12 | 'default_tasks': [ 13 | 'check', 14 | ], 15 | 'continue': True, 16 | 'verbosity': 1, 17 | 'num_process': cores, 18 | } 19 | 20 | 21 | def task_flake8(): 22 | """Run flake8 check.""" 23 | return { 24 | 'actions': ['flake8 setup.py pyppeteer tests'], 25 | } 26 | 27 | 28 | def task_mypy(): 29 | """Run mypy check.""" 30 | return { 31 | 'actions': ['mypy pyppeteer'], 32 | } 33 | 34 | 35 | def task_pydocstyle(): 36 | """Run docstyle check.""" 37 | return { 38 | 'actions': ['pydocstyle pyppeteer'], 39 | } 40 | 41 | 42 | def task_docs(): 43 | """Build sphinx document.""" 44 | return { 45 | 'actions': [ 46 | 'sphinx-build -q -W -E -j auto -b html docs docs/_build/html', 47 | ], 48 | } 49 | 50 | 51 | def task_readme(): 52 | """Check long description for package.""" 53 | return { 54 | 'actions': ['python setup.py check -r -s'], 55 | } 56 | 57 | 58 | def task_spell(): 59 | """Check spelling of comments and docstrings.""" 60 | return { 61 | 'actions': [ 62 | 'pylint --disable all --enable spelling --spelling-dict en_US ' 63 | '--spelling-private-dict-file spell.txt pyppeteer' 64 | ], 65 | } 66 | 67 | 68 | def task_check(): 69 | """Run flake8/mypy/pydocstyle/docs/readme tasks.""" 70 | return { 71 | 'actions': None, 72 | 'task_dep': ['flake8', 'mypy', 'pydocstyle', 'docs', 'readme'] 73 | } 74 | 75 | 76 | def task_test(): 77 | """Run pytest.""" 78 | return { 79 | 'actions': [CmdAction( 80 | 'pytest -n {}'.format(cores // 2), 81 | buffering=1, 82 | )], 83 | 'verbosity': 2, 84 | } 85 | 86 | 87 | def task_all(): 88 | """Run all tests and checks.""" 89 | return { 90 | 'actions': None, 91 | 'task_dep': ['test', 'check'], 92 | } 93 | -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | strict_optional = true 3 | disallow_untyped_defs = true 4 | disallow_untyped_calls = true 5 | follow_imports = silent 6 | ignore_missing_imports = true 7 | mypy_path = out 8 | 9 | [mypy-*/tests/*,*/docs/*,dodo,setup] 10 | ignore_errors = true 11 | -------------------------------------------------------------------------------- /pyppeteer/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | """Meta data for pyppeteer.""" 5 | 6 | import logging 7 | import os 8 | 9 | from appdirs import AppDirs 10 | 11 | __author__ = """Hiroyuki Takagi""" 12 | __email__ = 'miyako.dev@gmail.com' 13 | __version__ = '0.0.25' 14 | __chromium_revision__ = '588429' 15 | __base_puppeteer_version__ = 'v1.6.0' 16 | __pyppeteer_home__ = os.environ.get( 17 | 'PYPPETEER_HOME', AppDirs('pyppeteer').user_data_dir) # type: str 18 | DEBUG = False 19 | 20 | # Setup root logger 21 | _logger = logging.getLogger('pyppeteer') 22 | _log_handler = logging.StreamHandler() 23 | _fmt = '[{levelname[0]}:{name}] {msg}' 24 | _formatter = logging.Formatter(fmt=_fmt, style='{') 25 | _log_handler.setFormatter(_formatter) 26 | _log_handler.setLevel(logging.DEBUG) 27 | _logger.addHandler(_log_handler) 28 | _logger.propagate = False 29 | 30 | from pyppeteer.launcher import connect, launch, executablePath # noqa: E402 31 | from pyppeteer.launcher import defaultArgs # noqa: E402 32 | 33 | version = __version__ 34 | version_info = tuple(int(i) for i in version.split('.')) 35 | 36 | __all__ = [ 37 | 'connect', 38 | 'launch', 39 | 'executablePath', 40 | 'defaultArgs', 41 | 'version', 42 | 'version_info', 43 | ] 44 | -------------------------------------------------------------------------------- /pyppeteer/chromium_downloader.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | """Chromium download module.""" 5 | 6 | from io import BytesIO 7 | import logging 8 | import os 9 | from pathlib import Path 10 | import stat 11 | import sys 12 | from zipfile import ZipFile 13 | 14 | import urllib3 15 | from tqdm import tqdm 16 | 17 | from pyppeteer import __chromium_revision__, __pyppeteer_home__ 18 | 19 | logger = logging.getLogger(__name__) 20 | 21 | DOWNLOADS_FOLDER = Path(__pyppeteer_home__) / 'local-chromium' 22 | DEFAULT_DOWNLOAD_HOST = 'https://storage.googleapis.com' 23 | DOWNLOAD_HOST = os.environ.get( 24 | 'PYPPETEER_DOWNLOAD_HOST', DEFAULT_DOWNLOAD_HOST) 25 | BASE_URL = f'{DOWNLOAD_HOST}/chromium-browser-snapshots' 26 | 27 | REVISION = os.environ.get( 28 | 'PYPPETEER_CHROMIUM_REVISION', __chromium_revision__) 29 | 30 | NO_PROGRESS_BAR = os.environ.get('PYPPETEER_NO_PROGRESS_BAR', '') 31 | if NO_PROGRESS_BAR.lower() in ('1', 'true'): 32 | NO_PROGRESS_BAR = True # type: ignore 33 | 34 | downloadURLs = { 35 | 'linux': f'{BASE_URL}/Linux_x64/{REVISION}/chrome-linux.zip', 36 | 'mac': f'{BASE_URL}/Mac/{REVISION}/chrome-mac.zip', 37 | 'win32': f'{BASE_URL}/Win/{REVISION}/chrome-win32.zip', 38 | 'win64': f'{BASE_URL}/Win_x64/{REVISION}/chrome-win32.zip', 39 | } 40 | 41 | chromiumExecutable = { 42 | 'linux': DOWNLOADS_FOLDER / REVISION / 'chrome-linux' / 'chrome', 43 | 'mac': (DOWNLOADS_FOLDER / REVISION / 'chrome-mac' / 'Chromium.app' / 44 | 'Contents' / 'MacOS' / 'Chromium'), 45 | 'win32': DOWNLOADS_FOLDER / REVISION / 'chrome-win32' / 'chrome.exe', 46 | 'win64': DOWNLOADS_FOLDER / REVISION / 'chrome-win32' / 'chrome.exe', 47 | } 48 | 49 | 50 | def current_platform() -> str: 51 | """Get current platform name by short string.""" 52 | if sys.platform.startswith('linux'): 53 | return 'linux' 54 | elif sys.platform.startswith('darwin'): 55 | return 'mac' 56 | elif (sys.platform.startswith('win') or 57 | sys.platform.startswith('msys') or 58 | sys.platform.startswith('cyg')): 59 | if sys.maxsize > 2 ** 31 - 1: 60 | return 'win64' 61 | return 'win32' 62 | raise OSError('Unsupported platform: ' + sys.platform) 63 | 64 | 65 | def get_url() -> str: 66 | """Get chromium download url.""" 67 | return downloadURLs[current_platform()] 68 | 69 | 70 | def download_zip(url: str) -> BytesIO: 71 | """Download data from url.""" 72 | logger.warning('start chromium download.\n' 73 | 'Download may take a few minutes.') 74 | 75 | # disable warnings so that we don't need a cert. 76 | # see https://urllib3.readthedocs.io/en/latest/advanced-usage.html for more 77 | urllib3.disable_warnings() 78 | 79 | with urllib3.PoolManager() as http: 80 | # Get data from url. 81 | # set preload_content=False means using stream later. 82 | data = http.request('GET', url, preload_content=False) 83 | 84 | try: 85 | total_length = int(data.headers['content-length']) 86 | except (KeyError, ValueError, AttributeError): 87 | total_length = 0 88 | 89 | process_bar = tqdm( 90 | total=total_length, 91 | file=os.devnull if NO_PROGRESS_BAR else None, 92 | ) 93 | 94 | # 10 * 1024 95 | _data = BytesIO() 96 | for chunk in data.stream(10240): 97 | _data.write(chunk) 98 | process_bar.update(len(chunk)) 99 | process_bar.close() 100 | 101 | logger.warning('\nchromium download done.') 102 | return _data 103 | 104 | 105 | def extract_zip(data: BytesIO, path: Path) -> None: 106 | """Extract zipped data to path.""" 107 | # On mac zipfile module cannot extract correctly, so use unzip instead. 108 | if current_platform() == 'mac': 109 | import subprocess 110 | import shutil 111 | zip_path = path / 'chrome.zip' 112 | if not path.exists(): 113 | path.mkdir(parents=True) 114 | with zip_path.open('wb') as f: 115 | f.write(data.getvalue()) 116 | if not shutil.which('unzip'): 117 | raise OSError('Failed to automatically extract chromium.' 118 | f'Please unzip {zip_path} manually.') 119 | proc = subprocess.run( 120 | ['unzip', str(zip_path)], 121 | cwd=str(path), 122 | stdout=subprocess.PIPE, 123 | stderr=subprocess.STDOUT, 124 | ) 125 | if proc.returncode != 0: 126 | logger.error(proc.stdout.decode()) 127 | raise OSError(f'Failed to unzip {zip_path}.') 128 | if chromium_executable().exists() and zip_path.exists(): 129 | zip_path.unlink() 130 | else: 131 | with ZipFile(data) as zf: 132 | zf.extractall(str(path)) 133 | exec_path = chromium_executable() 134 | if not exec_path.exists(): 135 | raise IOError('Failed to extract chromium.') 136 | exec_path.chmod(exec_path.stat().st_mode | stat.S_IXOTH | stat.S_IXGRP | 137 | stat.S_IXUSR) 138 | logger.warning(f'chromium extracted to: {path}') 139 | 140 | 141 | def download_chromium() -> None: 142 | """Download and extract chromium.""" 143 | extract_zip(download_zip(get_url()), DOWNLOADS_FOLDER / REVISION) 144 | 145 | 146 | def chromium_excutable() -> Path: 147 | """[Deprecated] miss-spelled function. 148 | 149 | Use `chromium_executable` instead. 150 | """ 151 | logger.warning( 152 | '`chromium_excutable` function is deprecated. ' 153 | 'Use `chromium_executable instead.' 154 | ) 155 | return chromium_executable() 156 | 157 | 158 | def chromium_executable() -> Path: 159 | """Get path of the chromium executable.""" 160 | return chromiumExecutable[current_platform()] 161 | 162 | 163 | def check_chromium() -> bool: 164 | """Check if chromium is placed at correct path.""" 165 | return chromium_executable().exists() 166 | -------------------------------------------------------------------------------- /pyppeteer/command.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | """Commands for Pyppeteer.""" 5 | 6 | import logging 7 | 8 | from pyppeteer.chromium_downloader import check_chromium, download_chromium 9 | 10 | 11 | def install() -> None: 12 | """Download chromium if not install.""" 13 | if not check_chromium(): 14 | download_chromium() 15 | else: 16 | logging.getLogger(__name__).warning('chromium is already installed.') 17 | -------------------------------------------------------------------------------- /pyppeteer/dialog.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | """Dialog module.""" 5 | 6 | from types import SimpleNamespace 7 | 8 | from pyppeteer.connection import CDPSession 9 | 10 | 11 | class Dialog(object): 12 | """Dialog class. 13 | 14 | Dialog objects are dispatched by page via the ``dialog`` event. 15 | 16 | An example of using ``Dialog`` class: 17 | 18 | .. code:: 19 | 20 | browser = await launch() 21 | page = await browser.newPage() 22 | 23 | async def close_dialog(dialog): 24 | print(dialog.message) 25 | await dialog.dismiss() 26 | await browser.close() 27 | 28 | page.on( 29 | 'dialog', 30 | lambda dialog: asyncio.ensure_future(close_dialog(dialog)) 31 | ) 32 | await page.evaluate('() => alert("1")') 33 | """ 34 | 35 | Type = SimpleNamespace( 36 | Alert='alert', 37 | BeforeUnload='beforeunload', 38 | Confirm='confirm', 39 | Prompt='prompt', 40 | ) 41 | 42 | def __init__(self, client: CDPSession, type: str, message: str, 43 | defaultValue: str = '') -> None: 44 | self._client = client 45 | self._type = type 46 | self._message = message 47 | self._handled = False 48 | self._defaultValue = defaultValue 49 | 50 | @property 51 | def type(self) -> str: 52 | """Get dialog type. 53 | 54 | One of ``alert``, ``beforeunload``, ``confirm``, or ``prompt``. 55 | """ 56 | return self._type 57 | 58 | @property 59 | def message(self) -> str: 60 | """Get dialog message.""" 61 | return self._message 62 | 63 | @property 64 | def defaultValue(self) -> str: 65 | """If dialog is prompt, get default prompt value. 66 | 67 | If dialog is not prompt, return empty string (``''``). 68 | """ 69 | return self._defaultValue 70 | 71 | async def accept(self, promptText: str = '') -> None: 72 | """Accept the dialog. 73 | 74 | * ``promptText`` (str): A text to enter in prompt. If the dialog's type 75 | is not prompt, this does not cause any effect. 76 | """ 77 | self._handled = True 78 | await self._client.send('Page.handleJavaScriptDialog', { 79 | 'accept': True, 80 | 'promptText': promptText, 81 | }) 82 | 83 | async def dismiss(self) -> None: 84 | """Dismiss the dialog.""" 85 | self._handled = True 86 | await self._client.send('Page.handleJavaScriptDialog', { 87 | 'accept': False, 88 | }) 89 | -------------------------------------------------------------------------------- /pyppeteer/emulation_manager.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | """Emulation Manager module.""" 5 | 6 | from pyppeteer import helper 7 | from pyppeteer.connection import CDPSession 8 | 9 | 10 | class EmulationManager(object): 11 | """EmulationManager class.""" 12 | 13 | def __init__(self, client: CDPSession) -> None: 14 | """Make new emulation manager.""" 15 | self._client = client 16 | self._emulatingMobile = False 17 | self._hasTouch = False 18 | 19 | async def emulateViewport(self, viewport: dict) -> bool: 20 | """Evaluate viewport.""" 21 | options = dict() 22 | mobile = viewport.get('isMobile', False) 23 | options['mobile'] = mobile 24 | if 'width' in viewport: 25 | options['width'] = helper.get_positive_int(viewport, 'width') 26 | if 'height' in viewport: 27 | options['height'] = helper.get_positive_int(viewport, 'height') 28 | 29 | options['deviceScaleFactor'] = viewport.get('deviceScaleFactor', 1) 30 | if viewport.get('isLandscape'): 31 | options['screenOrientation'] = {'angle': 90, 32 | 'type': 'landscapePrimary'} 33 | else: 34 | options['screenOrientation'] = {'angle': 0, 35 | 'type': 'portraitPrimary'} 36 | hasTouch = viewport.get('hasTouch', False) 37 | 38 | await self._client.send('Emulation.setDeviceMetricsOverride', options) 39 | await self._client.send('Emulation.setTouchEmulationEnabled', { 40 | 'enabled': hasTouch, 41 | 'configuration': 'mobile' if mobile else 'desktop' 42 | }) 43 | 44 | reloadNeeded = (self._emulatingMobile != mobile or 45 | self._hasTouch != hasTouch) 46 | 47 | self._emulatingMobile = mobile 48 | self._hasTouch = hasTouch 49 | return reloadNeeded 50 | -------------------------------------------------------------------------------- /pyppeteer/errors.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | """Exceptions for pyppeteer package.""" 5 | 6 | import asyncio 7 | 8 | 9 | class PyppeteerError(Exception): # noqa: D204 10 | """Base exception for pyppeteer.""" 11 | pass 12 | 13 | 14 | class BrowserError(PyppeteerError): # noqa: D204 15 | """Exception raised from browser.""" 16 | pass 17 | 18 | 19 | class ElementHandleError(PyppeteerError): # noqa: D204 20 | """ElementHandle related exception.""" 21 | pass 22 | 23 | 24 | class NetworkError(PyppeteerError): # noqa: D204 25 | """Network/Protocol related exception.""" 26 | pass 27 | 28 | 29 | class PageError(PyppeteerError): # noqa: D204 30 | """Page/Frame related exception.""" 31 | pass 32 | 33 | 34 | class TimeoutError(asyncio.TimeoutError): # noqa: D204 35 | """Timeout Error class.""" 36 | pass 37 | -------------------------------------------------------------------------------- /pyppeteer/execution_context.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | """Execution Context Module.""" 5 | 6 | import logging 7 | import math 8 | import re 9 | from typing import Any, Dict, Optional, TYPE_CHECKING 10 | 11 | from pyppeteer import helper 12 | from pyppeteer.connection import CDPSession 13 | from pyppeteer.errors import ElementHandleError, NetworkError 14 | from pyppeteer.helper import debugError 15 | 16 | if TYPE_CHECKING: 17 | from pyppeteer.element_handle import ElementHandle # noqa: F401 18 | from pyppeteer.frame_manager import Frame # noqa: F401 19 | 20 | logger = logging.getLogger(__name__) 21 | 22 | EVALUATION_SCRIPT_URL = '__pyppeteer_evaluation_script__' 23 | SOURCE_URL_REGEX = re.compile( 24 | r'^[\040\t]*//[@#] sourceURL=\s*(\S*?)\s*$', 25 | re.MULTILINE, 26 | ) 27 | 28 | 29 | class ExecutionContext(object): 30 | """Execution Context class.""" 31 | 32 | def __init__(self, client: CDPSession, contextPayload: Dict, 33 | objectHandleFactory: Any, frame: 'Frame' = None) -> None: 34 | self._client = client 35 | self._frame = frame 36 | self._contextId = contextPayload.get('id') 37 | 38 | auxData = contextPayload.get('auxData', {'isDefault': False}) 39 | self._isDefault = bool(auxData.get('isDefault')) 40 | self._objectHandleFactory = objectHandleFactory 41 | 42 | @property 43 | def frame(self) -> Optional['Frame']: 44 | """Return frame associated with this execution context.""" 45 | return self._frame 46 | 47 | async def evaluate(self, pageFunction: str, *args: Any, 48 | force_expr: bool = False) -> Any: 49 | """Execute ``pageFunction`` on this context. 50 | 51 | Details see :meth:`pyppeteer.page.Page.evaluate`. 52 | """ 53 | handle = await self.evaluateHandle( 54 | pageFunction, *args, force_expr=force_expr) 55 | try: 56 | result = await handle.jsonValue() 57 | except NetworkError as e: 58 | if 'Object reference chain is too long' in e.args[0]: 59 | return 60 | if 'Object couldn\'t be returned by value' in e.args[0]: 61 | return 62 | raise 63 | await handle.dispose() 64 | return result 65 | 66 | async def evaluateHandle(self, pageFunction: str, *args: Any, # noqa: C901 67 | force_expr: bool = False) -> 'JSHandle': 68 | """Execute ``pageFunction`` on this context. 69 | 70 | Details see :meth:`pyppeteer.page.Page.evaluateHandle`. 71 | """ 72 | suffix = f'//# sourceURL={EVALUATION_SCRIPT_URL}' 73 | 74 | if force_expr or (not args and not helper.is_jsfunc(pageFunction)): 75 | try: 76 | if SOURCE_URL_REGEX.match(pageFunction): 77 | expressionWithSourceUrl = pageFunction 78 | else: 79 | expressionWithSourceUrl = f'{pageFunction}\n{suffix}' 80 | _obj = await self._client.send('Runtime.evaluate', { 81 | 'expression': expressionWithSourceUrl, 82 | 'contextId': self._contextId, 83 | 'returnByValue': False, 84 | 'awaitPromise': True, 85 | 'userGesture': True, 86 | }) 87 | except Exception as e: 88 | _rewriteError(e) 89 | 90 | exceptionDetails = _obj.get('exceptionDetails') 91 | if exceptionDetails: 92 | raise ElementHandleError( 93 | 'Evaluation failed: {}'.format( 94 | helper.getExceptionMessage(exceptionDetails))) 95 | remoteObject = _obj.get('result') 96 | return self._objectHandleFactory(remoteObject) 97 | 98 | try: 99 | _obj = await self._client.send('Runtime.callFunctionOn', { 100 | 'functionDeclaration': f'{pageFunction}\n{suffix}\n', 101 | 'executionContextId': self._contextId, 102 | 'arguments': [self._convertArgument(arg) for arg in args], 103 | 'returnByValue': False, 104 | 'awaitPromise': True, 105 | 'userGesture': True, 106 | }) 107 | except Exception as e: 108 | _rewriteError(e) 109 | 110 | exceptionDetails = _obj.get('exceptionDetails') 111 | if exceptionDetails: 112 | raise ElementHandleError('Evaluation failed: {}'.format( 113 | helper.getExceptionMessage(exceptionDetails))) 114 | remoteObject = _obj.get('result') 115 | return self._objectHandleFactory(remoteObject) 116 | 117 | def _convertArgument(self, arg: Any) -> Dict: # noqa: C901 118 | if arg == math.inf: 119 | return {'unserializableValue': 'Infinity'} 120 | if arg == -math.inf: 121 | return {'unserializableValue': '-Infinity'} 122 | objectHandle = arg if isinstance(arg, JSHandle) else None 123 | if objectHandle: 124 | if objectHandle._context != self: 125 | raise ElementHandleError('JSHandles can be evaluated only in the context they were created!') # noqa: E501 126 | if objectHandle._disposed: 127 | raise ElementHandleError('JSHandle is disposed!') 128 | if objectHandle._remoteObject.get('unserializableValue'): 129 | return {'unserializableValue': objectHandle._remoteObject.get('unserializableValue')} # noqa: E501 130 | if not objectHandle._remoteObject.get('objectId'): 131 | return {'value': objectHandle._remoteObject.get('value')} 132 | return {'objectId': objectHandle._remoteObject.get('objectId')} 133 | return {'value': arg} 134 | 135 | async def queryObjects(self, prototypeHandle: 'JSHandle') -> 'JSHandle': 136 | """Send query. 137 | 138 | Details see :meth:`pyppeteer.page.Page.queryObjects`. 139 | """ 140 | if prototypeHandle._disposed: 141 | raise ElementHandleError('Prototype JSHandle is disposed!') 142 | if not prototypeHandle._remoteObject.get('objectId'): 143 | raise ElementHandleError( 144 | 'Prototype JSHandle must not be referencing primitive value') 145 | response = await self._client.send('Runtime.queryObjects', { 146 | 'prototypeObjectId': prototypeHandle._remoteObject['objectId'], 147 | }) 148 | return self._objectHandleFactory(response.get('objects')) 149 | 150 | 151 | class JSHandle(object): 152 | """JSHandle class. 153 | 154 | JSHandle represents an in-page JavaScript object. JSHandle can be created 155 | with the :meth:`~pyppeteer.page.Page.evaluateHandle` method. 156 | """ 157 | 158 | def __init__(self, context: ExecutionContext, client: CDPSession, 159 | remoteObject: Dict) -> None: 160 | self._context = context 161 | self._client = client 162 | self._remoteObject = remoteObject 163 | self._disposed = False 164 | 165 | @property 166 | def executionContext(self) -> ExecutionContext: 167 | """Get execution context of this handle.""" 168 | return self._context 169 | 170 | async def getProperty(self, propertyName: str) -> 'JSHandle': 171 | """Get property value of ``propertyName``.""" 172 | objectHandle = await self._context.evaluateHandle( 173 | '''(object, propertyName) => { 174 | const result = {__proto__: null}; 175 | result[propertyName] = object[propertyName]; 176 | return result; 177 | }''', self, propertyName) 178 | properties = await objectHandle.getProperties() 179 | result = properties[propertyName] 180 | await objectHandle.dispose() 181 | return result 182 | 183 | async def getProperties(self) -> Dict[str, 'JSHandle']: 184 | """Get all properties of this handle.""" 185 | response = await self._client.send('Runtime.getProperties', { 186 | 'objectId': self._remoteObject.get('objectId', ''), 187 | 'ownProperties': True, 188 | }) 189 | result = dict() 190 | for prop in response['result']: 191 | if not prop.get('enumerable'): 192 | continue 193 | result[prop.get('name')] = self._context._objectHandleFactory( 194 | prop.get('value')) 195 | return result 196 | 197 | async def jsonValue(self) -> Dict: 198 | """Get Jsonized value of this object.""" 199 | objectId = self._remoteObject.get('objectId') 200 | if objectId: 201 | response = await self._client.send('Runtime.callFunctionOn', { 202 | 'functionDeclaration': 'function() { return this; }', 203 | 'objectId': objectId, 204 | 'returnByValue': True, 205 | 'awaitPromise': True, 206 | }) 207 | return helper.valueFromRemoteObject(response['result']) 208 | return helper.valueFromRemoteObject(self._remoteObject) 209 | 210 | def asElement(self) -> Optional['ElementHandle']: 211 | """Return either null or the object handle itself.""" 212 | return None 213 | 214 | async def dispose(self) -> None: 215 | """Stop referencing the handle.""" 216 | if self._disposed: 217 | return 218 | self._disposed = True 219 | try: 220 | await helper.releaseObject(self._client, self._remoteObject) 221 | except Exception as e: 222 | debugError(logger, e) 223 | 224 | def toString(self) -> str: 225 | """Get string representation.""" 226 | if self._remoteObject.get('objectId'): 227 | _type = (self._remoteObject.get('subtype') or 228 | self._remoteObject.get('type')) 229 | return f'JSHandle@{_type}' 230 | return 'JSHandle:{}'.format( 231 | helper.valueFromRemoteObject(self._remoteObject)) 232 | 233 | 234 | def _rewriteError(error: Exception) -> None: 235 | if error.args[0].endswith('Cannot find context with specified id'): 236 | msg = 'Execution context was destroyed, most likely because of a navigation.' # noqa: E501 237 | raise type(error)(msg) 238 | raise error 239 | -------------------------------------------------------------------------------- /pyppeteer/helper.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | """Helper functions.""" 5 | 6 | import asyncio 7 | import json 8 | import logging 9 | import math 10 | from typing import Any, Awaitable, Callable, Dict, List 11 | 12 | from pyee import EventEmitter 13 | 14 | import pyppeteer 15 | from pyppeteer.connection import CDPSession 16 | from pyppeteer.errors import ElementHandleError, TimeoutError 17 | 18 | logger = logging.getLogger(__name__) 19 | 20 | 21 | def debugError(_logger: logging.Logger, msg: Any) -> None: 22 | """Log error messages.""" 23 | if pyppeteer.DEBUG: 24 | _logger.error(msg) 25 | else: 26 | _logger.debug(msg) 27 | 28 | 29 | def evaluationString(fun: str, *args: Any) -> str: 30 | """Convert function and arguments to str.""" 31 | _args = ', '.join([ 32 | json.dumps('undefined' if arg is None else arg) for arg in args 33 | ]) 34 | expr = f'({fun})({_args})' 35 | return expr 36 | 37 | 38 | def getExceptionMessage(exceptionDetails: dict) -> str: 39 | """Get exception message from `exceptionDetails` object.""" 40 | exception = exceptionDetails.get('exception') 41 | if exception: 42 | return exception.get('description') or exception.get('value') 43 | message = exceptionDetails.get('text', '') 44 | stackTrace = exceptionDetails.get('stackTrace', dict()) 45 | if stackTrace: 46 | for callframe in stackTrace.get('callFrames'): 47 | location = ( 48 | str(callframe.get('url', '')) + ':' + 49 | str(callframe.get('lineNumber', '')) + ':' + 50 | str(callframe.get('columnNumber')) 51 | ) 52 | functionName = callframe.get('functionName', '') 53 | message = message + f'\n at {functionName} ({location})' 54 | return message 55 | 56 | 57 | def addEventListener(emitter: EventEmitter, eventName: str, handler: Callable 58 | ) -> Dict[str, Any]: 59 | """Add handler to the emitter and return emitter/handler.""" 60 | emitter.on(eventName, handler) 61 | return {'emitter': emitter, 'eventName': eventName, 'handler': handler} 62 | 63 | 64 | def removeEventListeners(listeners: List[dict]) -> None: 65 | """Remove listeners from emitter.""" 66 | for listener in listeners: 67 | emitter = listener['emitter'] 68 | eventName = listener['eventName'] 69 | handler = listener['handler'] 70 | emitter.remove_listener(eventName, handler) 71 | listeners.clear() 72 | 73 | 74 | unserializableValueMap = { 75 | '-0': -0, 76 | 'NaN': None, 77 | None: None, 78 | 'Infinity': math.inf, 79 | '-Infinity': -math.inf, 80 | } 81 | 82 | 83 | def valueFromRemoteObject(remoteObject: Dict) -> Any: 84 | """Serialize value of remote object.""" 85 | if remoteObject.get('objectId'): 86 | raise ElementHandleError('Cannot extract value when objectId is given') 87 | value = remoteObject.get('unserializableValue') 88 | if value: 89 | if value == '-0': 90 | return -0 91 | elif value == 'NaN': 92 | return None 93 | elif value == 'Infinity': 94 | return math.inf 95 | elif value == '-Infinity': 96 | return -math.inf 97 | else: 98 | raise ElementHandleError( 99 | 'Unsupported unserializable value: {}'.format(value)) 100 | return remoteObject.get('value') 101 | 102 | 103 | def releaseObject(client: CDPSession, remoteObject: dict 104 | ) -> Awaitable: 105 | """Release remote object.""" 106 | objectId = remoteObject.get('objectId') 107 | fut_none = client._loop.create_future() 108 | fut_none.set_result(None) 109 | if not objectId: 110 | return fut_none 111 | try: 112 | return client.send('Runtime.releaseObject', { 113 | 'objectId': objectId 114 | }) 115 | except Exception as e: 116 | # Exceptions might happen in case of a page been navigated or closed. 117 | # Swallow these since they are harmless and we don't leak anything in this case. # noqa 118 | debugError(logger, e) 119 | return fut_none 120 | 121 | 122 | def waitForEvent(emitter: EventEmitter, eventName: str, # noqa: C901 123 | predicate: Callable[[Any], bool], timeout: float, 124 | loop: asyncio.AbstractEventLoop) -> Awaitable: 125 | """Wait for an event emitted from the emitter.""" 126 | promise = loop.create_future() 127 | 128 | def resolveCallback(target: Any) -> None: 129 | promise.set_result(target) 130 | 131 | def rejectCallback(exception: Exception) -> None: 132 | promise.set_exception(exception) 133 | 134 | async def timeoutTimer() -> None: 135 | await asyncio.sleep(timeout / 1000) 136 | rejectCallback( 137 | TimeoutError('Timeout exceeded while waiting for event')) 138 | 139 | def _listener(target: Any) -> None: 140 | if not predicate(target): 141 | return 142 | cleanup() 143 | resolveCallback(target) 144 | 145 | listener = addEventListener(emitter, eventName, _listener) 146 | if timeout: 147 | eventTimeout = loop.create_task(timeoutTimer()) 148 | 149 | def cleanup() -> None: 150 | removeEventListeners([listener]) 151 | if timeout: 152 | eventTimeout.cancel() 153 | 154 | return promise 155 | 156 | 157 | def get_positive_int(obj: dict, name: str) -> int: 158 | """Get and check the value of name in obj is positive integer.""" 159 | value = obj[name] 160 | if not isinstance(value, int): 161 | raise TypeError( 162 | f'{name} must be integer: {type(value)}') 163 | elif value < 0: 164 | raise ValueError( 165 | f'{name} must be positive integer: {value}') 166 | return value 167 | 168 | 169 | def is_jsfunc(func: str) -> bool: # not in puppeteer 170 | """Heuristically check function or expression.""" 171 | func = func.strip() 172 | if func.startswith('function') or func.startswith('async '): 173 | return True 174 | elif '=>' in func: 175 | return True 176 | return False 177 | -------------------------------------------------------------------------------- /pyppeteer/multimap.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | """Multimap module.""" 5 | 6 | from collections import OrderedDict 7 | from typing import Any, List, Optional 8 | 9 | 10 | class Multimap(object): 11 | """Multimap class.""" 12 | 13 | def __init__(self) -> None: 14 | """Make new multimap.""" 15 | # maybe defaultdict(set) is better 16 | self._map: OrderedDict[Optional[str], List[Any]] = OrderedDict() 17 | 18 | def set(self, key: Optional[str], value: Any) -> None: 19 | """Set value.""" 20 | _set = self._map.get(key) 21 | if not _set: 22 | _set = list() 23 | self._map[key] = _set 24 | if value not in _set: 25 | _set.append(value) 26 | 27 | def get(self, key: Optional[str]) -> List[Any]: 28 | """Get values.""" 29 | return self._map.get(key, list()) 30 | 31 | def has(self, key: Optional[str]) -> bool: 32 | """Check key is in this map.""" 33 | return key in self._map 34 | 35 | def hasValue(self, key: Optional[str], value: Any) -> bool: 36 | """Check value is in this map.""" 37 | _set = self._map.get(key, list()) 38 | return value in _set 39 | 40 | def size(self) -> int: 41 | """Length of this map.""" 42 | return len(self._map) 43 | 44 | def delete(self, key: Optional[str], value: Any) -> bool: 45 | """Delete value from key.""" 46 | values = self.get(key) 47 | result = value in values 48 | if result: 49 | values.remove(value) 50 | if len(values) == 0: 51 | self._map.pop(key) 52 | return result 53 | 54 | def deleteAll(self, key: Optional[str]) -> None: 55 | """Delete all value of the key.""" 56 | self._map.pop(key, None) 57 | 58 | def firstValue(self, key: Optional[str]) -> Any: 59 | """Get first value of the key.""" 60 | _set = self._map.get(key) 61 | if not _set: 62 | return None 63 | return _set[0] 64 | 65 | def firstKey(self) -> Optional[str]: 66 | """Get first key.""" 67 | return next(iter(self._map.keys())) 68 | 69 | def valuesArray(self) -> List[Any]: 70 | """Get all values as list.""" 71 | result: List[Any] = list() 72 | for values in self._map.values(): 73 | result.extend(values) 74 | return result 75 | 76 | def clear(self) -> None: 77 | """Clear all entries of this map.""" 78 | self._map.clear() 79 | -------------------------------------------------------------------------------- /pyppeteer/navigator_watcher.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | """Navigator Watcher module.""" 5 | 6 | import asyncio 7 | import concurrent.futures 8 | from typing import Any, Awaitable, Dict, List, Union 9 | 10 | from pyppeteer import helper 11 | from pyppeteer.errors import TimeoutError 12 | from pyppeteer.frame_manager import FrameManager, Frame 13 | from pyppeteer.util import merge_dict 14 | 15 | 16 | class NavigatorWatcher: 17 | """NavigatorWatcher class.""" 18 | 19 | def __init__(self, frameManager: FrameManager, frame: Frame, timeout: int, 20 | options: Dict = None, **kwargs: Any) -> None: 21 | """Make new navigator watcher.""" 22 | options = merge_dict(options, kwargs) 23 | self._validate_options(options) 24 | self._frameManager = frameManager 25 | self._frame = frame 26 | self._initialLoaderId = frame._loaderId 27 | self._timeout = timeout 28 | self._hasSameDocumentNavigation = False 29 | self._eventListeners = [ 30 | helper.addEventListener( 31 | self._frameManager, 32 | FrameManager.Events.LifecycleEvent, 33 | self._checkLifecycleComplete, 34 | ), 35 | helper.addEventListener( 36 | self._frameManager, 37 | FrameManager.Events.FrameNavigatedWithinDocument, 38 | self._navigatedWithinDocument, 39 | ), 40 | helper.addEventListener( 41 | self._frameManager, 42 | FrameManager.Events.FrameDetached, 43 | self._checkLifecycleComplete, 44 | ), 45 | ] 46 | self._loop = self._frameManager._client._loop 47 | self._lifecycleCompletePromise = self._loop.create_future() 48 | 49 | self._navigationPromise = self._loop.create_task(asyncio.wait([ 50 | self._lifecycleCompletePromise, 51 | self._createTimeoutPromise(), 52 | ], return_when=concurrent.futures.FIRST_COMPLETED)) 53 | self._navigationPromise.add_done_callback( 54 | lambda fut: self._cleanup()) 55 | 56 | def _validate_options(self, options: Dict) -> None: # noqa: C901 57 | if 'networkIdleTimeout' in options: 58 | raise ValueError( 59 | '`networkIdleTimeout` option is no longer supported.') 60 | if 'networkIdleInflight' in options: 61 | raise ValueError( 62 | '`networkIdleInflight` option is no longer supported.') 63 | if options.get('waitUntil') == 'networkidle': 64 | raise ValueError( 65 | '`networkidle` option is no logner supported. ' 66 | 'Use `networkidle2` instead.') 67 | if options.get('waitUntil') == 'documentloaded': 68 | import logging 69 | logging.getLogger(__name__).warning( 70 | '`documentloaded` option is no longer supported. ' 71 | 'Use `domcontentloaded` instead.') 72 | _waitUntil = options.get('waitUntil', 'load') 73 | if isinstance(_waitUntil, list): 74 | waitUntil = _waitUntil 75 | elif isinstance(_waitUntil, str): 76 | waitUntil = [_waitUntil] 77 | else: 78 | raise TypeError( 79 | '`waitUntil` option should be str or list of str, ' 80 | f'but got type {type(_waitUntil)}' 81 | ) 82 | self._expectedLifecycle: List[str] = [] 83 | for value in waitUntil: 84 | protocolEvent = pyppeteerToProtocolLifecycle.get(value) 85 | if protocolEvent is None: 86 | raise ValueError( 87 | f'Unknown value for options.waitUntil: {value}') 88 | self._expectedLifecycle.append(protocolEvent) 89 | 90 | def _createTimeoutPromise(self) -> Awaitable[None]: 91 | self._maximumTimer = self._loop.create_future() 92 | if self._timeout: 93 | errorMessage = f'Navigation Timeout Exceeded: {self._timeout} ms exceeded.' # noqa: E501 94 | 95 | async def _timeout_func() -> None: 96 | await asyncio.sleep(self._timeout / 1000) 97 | self._maximumTimer.set_exception(TimeoutError(errorMessage)) 98 | 99 | self._timeout_timer: Union[asyncio.Task, asyncio.Future] = self._loop.create_task(_timeout_func()) # noqa: E501 100 | else: 101 | self._timeout_timer = self._loop.create_future() 102 | return self._maximumTimer 103 | 104 | def navigationPromise(self) -> Any: 105 | """Return navigation promise.""" 106 | return self._navigationPromise 107 | 108 | def _navigatedWithinDocument(self, frame: Frame = None) -> None: 109 | if frame != self._frame: 110 | return 111 | self._hasSameDocumentNavigation = True 112 | self._checkLifecycleComplete() 113 | 114 | def _checkLifecycleComplete(self, frame: Frame = None) -> None: 115 | if (self._frame._loaderId == self._initialLoaderId and 116 | not self._hasSameDocumentNavigation): 117 | return 118 | if not self._checkLifecycle(self._frame, self._expectedLifecycle): 119 | return 120 | 121 | if not self._lifecycleCompletePromise.done(): 122 | self._lifecycleCompletePromise.set_result(None) 123 | 124 | def _checkLifecycle(self, frame: Frame, expectedLifecycle: List[str] 125 | ) -> bool: 126 | for event in expectedLifecycle: 127 | if event not in frame._lifecycleEvents: 128 | return False 129 | for child in frame.childFrames: 130 | if not self._checkLifecycle(child, expectedLifecycle): 131 | return False 132 | return True 133 | 134 | def cancel(self) -> None: 135 | """Cancel navigation.""" 136 | self._cleanup() 137 | 138 | def _cleanup(self) -> None: 139 | helper.removeEventListeners(self._eventListeners) 140 | self._lifecycleCompletePromise.cancel() 141 | self._maximumTimer.cancel() 142 | self._timeout_timer.cancel() 143 | 144 | 145 | pyppeteerToProtocolLifecycle = { 146 | 'load': 'load', 147 | 'domcontentloaded': 'DOMContentLoaded', 148 | 'documentloaded': 'DOMContentLoaded', 149 | 'networkidle0': 'networkIdle', 150 | 'networkidle2': 'networkAlmostIdle', 151 | } 152 | -------------------------------------------------------------------------------- /pyppeteer/options.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | """Options module.""" 5 | 6 | from argparse import Namespace 7 | 8 | config = Namespace() 9 | -------------------------------------------------------------------------------- /pyppeteer/target.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | """Target module.""" 5 | 6 | import asyncio 7 | from typing import Any, Callable, Coroutine, Dict, List, Optional 8 | from typing import TYPE_CHECKING 9 | 10 | from pyppeteer.connection import CDPSession 11 | from pyppeteer.page import Page 12 | 13 | if TYPE_CHECKING: 14 | from pyppeteer.browser import Browser, BrowserContext # noqa: F401 15 | 16 | 17 | class Target(object): 18 | """Browser's target class.""" 19 | 20 | def __init__(self, targetInfo: Dict, browserContext: 'BrowserContext', 21 | sessionFactory: Callable[[], Coroutine[Any, Any, CDPSession]], 22 | ignoreHTTPSErrors: bool, defaultViewport: Optional[Dict], 23 | screenshotTaskQueue: List, loop: asyncio.AbstractEventLoop 24 | ) -> None: 25 | self._targetInfo = targetInfo 26 | self._browserContext = browserContext 27 | self._targetId = targetInfo.get('targetId', '') 28 | self._sessionFactory = sessionFactory 29 | self._ignoreHTTPSErrors = ignoreHTTPSErrors 30 | self._defaultViewport = defaultViewport 31 | self._screenshotTaskQueue = screenshotTaskQueue 32 | self._loop = loop 33 | self._page: Optional[Page] = None 34 | 35 | self._initializedPromise = self._loop.create_future() 36 | self._isClosedPromise = self._loop.create_future() 37 | self._isInitialized = (self._targetInfo['type'] != 'page' 38 | or self._targetInfo['url'] != '') 39 | if self._isInitialized: 40 | self._initializedCallback(True) 41 | 42 | def _initializedCallback(self, bl: bool) -> None: 43 | # TODO: this may cause error on page close 44 | if self._initializedPromise.done(): 45 | self._initializedPromise = self._loop.create_future() 46 | self._initializedPromise.set_result(bl) 47 | 48 | def _closedCallback(self) -> None: 49 | self._isClosedPromise.set_result(None) 50 | 51 | async def createCDPSession(self) -> CDPSession: 52 | """Create a Chrome Devtools Protocol session attached to the target.""" 53 | return await self._sessionFactory() 54 | 55 | async def page(self) -> Optional[Page]: 56 | """Get page of this target. 57 | 58 | If the target is not of type "page" or "background_page", return 59 | ``None``. 60 | """ 61 | if (self._targetInfo['type'] in ['page', 'background_page'] and 62 | self._page is None): 63 | client = await self._sessionFactory() 64 | new_page = await Page.create( 65 | client, self, 66 | self._ignoreHTTPSErrors, 67 | self._defaultViewport, 68 | self._screenshotTaskQueue, 69 | ) 70 | self._page = new_page 71 | return new_page 72 | return self._page 73 | 74 | @property 75 | def url(self) -> str: 76 | """Get url of this target.""" 77 | return self._targetInfo['url'] 78 | 79 | @property 80 | def type(self) -> str: 81 | """Get type of this target. 82 | 83 | Type can be ``'page'``, ``'background_page'``, ``'service_worker'``, 84 | ``'browser'``, or ``'other'``. 85 | """ 86 | _type = self._targetInfo['type'] 87 | if _type in ['page', 'background_page', 'service_worker', 'browser']: 88 | return _type 89 | return 'other' 90 | 91 | @property 92 | def browser(self) -> 'Browser': 93 | """Get the browser the target belongs to.""" 94 | return self._browserContext.browser 95 | 96 | @property 97 | def browserContext(self) -> 'BrowserContext': 98 | """Return the browser context the target belongs to.""" 99 | return self._browserContext 100 | 101 | @property 102 | def opener(self) -> Optional['Target']: 103 | """Get the target that opened this target. 104 | 105 | Top-level targets return ``None``. 106 | """ 107 | openerId = self._targetInfo.get('openerId') 108 | if openerId is None: 109 | return None 110 | return self.browser._targets.get(openerId) 111 | 112 | def _targetInfoChanged(self, targetInfo: Dict) -> None: 113 | self._targetInfo = targetInfo 114 | 115 | if not self._isInitialized and (self._targetInfo['type'] != 'page' or 116 | self._targetInfo['url'] != ''): 117 | self._isInitialized = True 118 | self._initializedCallback(True) 119 | return 120 | -------------------------------------------------------------------------------- /pyppeteer/tracing.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | """Tracing module.""" 5 | 6 | from pathlib import Path 7 | from typing import Any 8 | 9 | from pyppeteer.connection import CDPSession 10 | from pyppeteer.util import merge_dict 11 | 12 | 13 | class Tracing(object): 14 | """Tracing class. 15 | 16 | You can use :meth:`start` and :meth:`stop` to create a trace file which can 17 | be opened in Chrome DevTools or 18 | `timeline viewer `_. 19 | 20 | .. code:: 21 | 22 | await page.tracing.start({'path': 'trace.json'}) 23 | await page.goto('https://www.google.com') 24 | await page.tracing.stop() 25 | """ 26 | 27 | def __init__(self, client: CDPSession) -> None: 28 | self._client = client 29 | self._recording = False 30 | self._path = '' 31 | 32 | async def start(self, options: dict = None, **kwargs: Any) -> None: 33 | """Start tracing. 34 | 35 | Only one trace can be active at a time per browser. 36 | 37 | This method accepts the following options: 38 | 39 | * ``path`` (str): A path to write the trace file to. 40 | * ``screenshots`` (bool): Capture screenshots in the trace. 41 | * ``categories`` (List[str]): Specify custom categories to use instead 42 | of default. 43 | """ 44 | options = merge_dict(options, kwargs) 45 | defaultCategories = [ 46 | '-*', 'devtools.timeline', 'v8.execute', 47 | 'disabled-by-default-devtools.timeline', 48 | 'disabled-by-default-devtools.timeline.frame', 'toplevel', 49 | 'blink.console', 'blink.user_timing', 'latencyInfo', 50 | 'disabled-by-default-devtools.timeline.stack', 51 | 'disabled-by-default-v8.cpu_profiler', 52 | 'disabled-by-default-v8.cpu_profiler.hires', 53 | ] 54 | categoriesArray = options.get('categories', defaultCategories) 55 | 56 | if 'screenshots' in options: 57 | categoriesArray.append('disabled-by-default-devtools.screenshot') 58 | 59 | self._path = options.get('path', '') 60 | self._recording = True 61 | await self._client.send('Tracing.start', { 62 | 'transferMode': 'ReturnAsStream', 63 | 'categories': ','.join(categoriesArray), 64 | }) 65 | 66 | async def stop(self) -> str: 67 | """Stop tracing. 68 | 69 | :return: trace data as string. 70 | """ 71 | contentPromise = self._client._loop.create_future() 72 | self._client.once( 73 | 'Tracing.tracingComplete', 74 | lambda event: self._client._loop.create_task( 75 | self._readStream(event.get('stream'), self._path) 76 | ).add_done_callback( 77 | lambda fut: contentPromise.set_result(fut.result()) 78 | ) 79 | ) 80 | await self._client.send('Tracing.end') 81 | self._recording = False 82 | return await contentPromise 83 | 84 | async def _readStream(self, handle: str, path: str) -> str: 85 | # might be better to return as bytes 86 | eof = False 87 | bufs = [] 88 | while not eof: 89 | response = await self._client.send('IO.read', {'handle': handle}) 90 | eof = response.get('eof', False) 91 | bufs.append(response.get('data', '')) 92 | await self._client.send('IO.close', {'handle': handle}) 93 | 94 | result = ''.join(bufs) 95 | if path: 96 | file = Path(path) 97 | with file.open('w') as f: 98 | f.write(result) 99 | return result 100 | -------------------------------------------------------------------------------- /pyppeteer/util.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | """Utility functions.""" 5 | 6 | import gc 7 | import socket 8 | from typing import Dict, Optional 9 | 10 | from pyppeteer.chromium_downloader import check_chromium, chromium_executable 11 | from pyppeteer.chromium_downloader import download_chromium 12 | 13 | __all__ = [ 14 | 'check_chromium', 15 | 'chromium_executable', 16 | 'download_chromium', 17 | 'get_free_port', 18 | 'merge_dict', 19 | ] 20 | 21 | 22 | def get_free_port() -> int: 23 | """Get free port.""" 24 | sock = socket.socket() 25 | sock.bind(('localhost', 0)) 26 | port = sock.getsockname()[1] 27 | sock.close() 28 | del sock 29 | gc.collect() 30 | return port 31 | 32 | 33 | def merge_dict(dict1: Optional[Dict], dict2: Optional[Dict]) -> Dict: 34 | """Merge two dictionaries into new one.""" 35 | new_dict = {} 36 | if dict1: 37 | new_dict.update(dict1) 38 | if dict2: 39 | new_dict.update(dict2) 40 | return new_dict 41 | -------------------------------------------------------------------------------- /pyppeteer/worker.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | """Worker module.""" 5 | 6 | import logging 7 | from typing import Any, Callable, Dict, List, TYPE_CHECKING 8 | 9 | from pyee import EventEmitter 10 | 11 | from pyppeteer.execution_context import ExecutionContext, JSHandle 12 | from pyppeteer.helper import debugError 13 | 14 | if TYPE_CHECKING: 15 | from pyppeteer.connection import CDPSession # noqa: F401 16 | 17 | logger = logging.getLogger(__name__) 18 | 19 | 20 | class Worker(EventEmitter): 21 | """The Worker class represents a WebWorker. 22 | 23 | The events `workercreated` and `workerdestroyed` are emitted on the page 24 | object to signal the worker lifecycle. 25 | 26 | .. code:: 27 | 28 | page.on('workercreated', lambda worker: print('Worker created:', worker.url)) 29 | """ # noqa: E501 30 | 31 | def __init__(self, client: 'CDPSession', url: str, # noqa: C901 32 | consoleAPICalled: Callable[[str, List[JSHandle]], None], 33 | exceptionThrown: Callable[[Dict], None] 34 | ) -> None: 35 | super().__init__() 36 | self._client = client 37 | self._url = url 38 | self._loop = client._loop 39 | self._executionContextPromise = self._loop.create_future() 40 | 41 | def jsHandleFactory(remoteObject: Dict) -> JSHandle: 42 | return None # type: ignore 43 | 44 | def onExecutionContentCreated(event: Dict) -> None: 45 | nonlocal jsHandleFactory 46 | 47 | def jsHandleFactory(remoteObject: Dict) -> JSHandle: 48 | return JSHandle(executionContext, client, remoteObject) 49 | 50 | executionContext = ExecutionContext( 51 | client, event['context'], jsHandleFactory) 52 | self._executionContextCallback(executionContext) 53 | 54 | self._client.on('Runtime.executionContextCreated', 55 | onExecutionContentCreated) 56 | try: 57 | # This might fail if the target is closed before we receive all 58 | # execution contexts. 59 | self._client.send('Runtime.enable', {}) 60 | except Exception as e: 61 | debugError(logger, e) 62 | 63 | def onConsoleAPICalled(event: Dict) -> None: 64 | args: List[JSHandle] = [] 65 | for arg in event.get('args', []): 66 | args.append(jsHandleFactory(arg)) 67 | consoleAPICalled(event['type'], args) 68 | 69 | self._client.on('Runtime.consoleAPICalled', onConsoleAPICalled) 70 | self._client.on( 71 | 'Runtime.exceptionThrown', 72 | lambda exception: exceptionThrown(exception['exceptionDetails']), 73 | ) 74 | 75 | def _executionContextCallback(self, value: ExecutionContext) -> None: 76 | self._executionContextPromise.set_result(value) 77 | 78 | @property 79 | def url(self) -> str: 80 | """Return URL.""" 81 | return self._url 82 | 83 | async def executionContext(self) -> ExecutionContext: 84 | """Return ExecutionContext.""" 85 | return await self._executionContextPromise 86 | 87 | async def evaluate(self, pageFunction: str, *args: Any) -> Any: 88 | """Evaluate ``pageFunction`` with ``args``. 89 | 90 | Shortcut for ``(await worker.executionContext).evaluate(pageFunction, *args)``. 91 | """ # noqa: E501 92 | return await (await self._executionContextPromise).evaluate( 93 | pageFunction, *args) 94 | 95 | async def evaluateHandle(self, pageFunction: str, *args: Any) -> JSHandle: 96 | """Evaluate ``pageFunction`` with ``args`` and return :class:`~pyppeteer.execution_context.JSHandle`. 97 | 98 | Shortcut for ``(await worker.executionContext).evaluateHandle(pageFunction, *args)``. 99 | """ # noqa: E501 100 | return await (await self._executionContextPromise).evaluateHandle( 101 | pageFunction, *args) 102 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | -r requirements-docs.txt 2 | -r requirements-test.txt 3 | 4 | livereload 5 | flake8 6 | mypy 7 | pydocstyle 8 | readme_renderer 9 | doit 10 | pytest 11 | pytest-xdist 12 | pylint 13 | git+https://github.com/miyakogi/pyenchant.git 14 | -------------------------------------------------------------------------------- /requirements-docs.txt: -------------------------------------------------------------------------------- 1 | sphinx>=1.6,<1.8 2 | sphinxcontrib-asyncio 3 | m2r 4 | -------------------------------------------------------------------------------- /requirements-test.txt: -------------------------------------------------------------------------------- 1 | syncer 2 | tornado>=5 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from os import path 5 | from setuptools import setup 6 | import sys 7 | 8 | basedir = path.dirname(path.abspath(__file__)) 9 | extra_args = {} 10 | 11 | if (3, 6) > sys.version_info >= (3, 5): 12 | in_dir = path.join(basedir, 'pyppeteer') 13 | out_dir = path.join(basedir, '.pyppeteer') 14 | packages = ['pyppeteer'] 15 | package_dir = {'pyppeteer': '.pyppeteer'} 16 | if not path.exists(out_dir): 17 | if path.exists(in_dir): 18 | try: 19 | from py_backwards.compiler import compile_files 20 | except ImportError: 21 | import subprocess 22 | subprocess.run( 23 | [sys.executable, '-m', 'pip', 'install', 'py-backwards'] 24 | ) 25 | from py_backwards.compiler import compile_files 26 | target = (sys.version_info[0], sys.version_info[1]) 27 | compile_files(in_dir, out_dir, target) 28 | else: 29 | raise Exception('Could not find package directory') 30 | else: 31 | packages = ['pyppeteer'] 32 | package_dir = {'pyppeteer': 'pyppeteer'} 33 | 34 | readme_file = path.join(basedir, 'README.md') 35 | with open(readme_file) as f: 36 | src = f.read() 37 | 38 | try: 39 | from m2r import M2R 40 | readme = M2R()(src) 41 | except ImportError: 42 | readme = src 43 | 44 | requirements = [ 45 | 'pyee<6', 46 | 'websockets', 47 | 'appdirs', 48 | 'urllib3<1.25', 49 | 'tqdm' 50 | ] 51 | 52 | test_requirements = [ 53 | 'syncer', 54 | 'tornado>=5', 55 | ] 56 | 57 | setup( 58 | name='pyppeteer', 59 | version='0.0.25', 60 | description=('Headless chrome/chromium automation library ' 61 | '(unofficial port of puppeteer)'), 62 | long_description=readme, 63 | 64 | author="Hiroyuki Takagi", 65 | author_email='miyako.dev@gmail.com', 66 | url='https://github.com/miyakogi/pyppeteer', 67 | 68 | packages=packages, 69 | package_dir=package_dir, 70 | include_package_data=True, 71 | install_requires=requirements, 72 | entry_points={ 73 | 'console_scripts': [ 74 | 'pyppeteer-install = pyppeteer.command:install', 75 | ], 76 | }, 77 | 78 | license="MIT license", 79 | zip_safe=False, 80 | keywords='pyppeteer', 81 | classifiers=[ 82 | 'Development Status :: 3 - Alpha', 83 | 'Intended Audience :: Developers', 84 | 'License :: OSI Approved :: MIT License', 85 | 'Natural Language :: English', 86 | 'Programming Language :: Python :: 3', 87 | 'Programming Language :: Python :: 3.5', 88 | 'Programming Language :: Python :: 3.6', 89 | 'Programming Language :: Python :: 3.7', 90 | ], 91 | python_requires='>=3.5', 92 | test_suite='tests', 93 | tests_require=test_requirements, 94 | **extra_args 95 | ) 96 | -------------------------------------------------------------------------------- /spell.txt: -------------------------------------------------------------------------------- 1 | abstracteventloop 2 | accessdenied 3 | ack 4 | addressunreachable 5 | api 6 | arg 7 | args 8 | arrowleft 9 | async 10 | asyncio 11 | auth 12 | awaitable 13 | beforeunload 14 | blockedbyclient 15 | blockedbyresponse 16 | bool 17 | connectionaborted 18 | connectionclosed 19 | connectionfailed 20 | connectionreset 21 | connectionrefused 22 | createincognitobrowsercontext 23 | csp 24 | css 25 | ctrl 26 | customargs 27 | defaultargs 28 | defaultdict 29 | devtool 30 | devtools 31 | dialog's 32 | dict 33 | doctype 34 | domcontentloaded 35 | dpr 36 | dumpio 37 | elementhandle 38 | emulatemedia 39 | endswith 40 | env 41 | eval 42 | evaluatehandle 43 | eventsource 44 | executioncontext 45 | google 46 | goto 47 | html 48 | http 49 | https 50 | iframe 51 | ignoredefaultargs 52 | innertext 53 | internetdisconnected 54 | ip 55 | isinstance 56 | jpeg 57 | jpg 58 | js 59 | json 60 | jsonized 61 | keya 62 | keydown 63 | keypress 64 | keyup 65 | killchrome 66 | kwargs 67 | len 68 | lifecycle 69 | mousedown 70 | mousemove 71 | mouseup 72 | msec 73 | multimap 74 | namenotresolved 75 | newpage 76 | noqa 77 | offline 78 | pagefunction 79 | params 80 | pdf 81 | png 82 | popup 83 | px 84 | pyee 85 | pyppeteer 86 | pyppeteer's 87 | queryselector 88 | queryselectorall 89 | queryselectoralleval 90 | queryselectoreval 91 | raf 92 | recalc 93 | req 94 | requestfailed 95 | requestfinished 96 | request's 97 | rst 98 | screenshot 99 | screenshots 100 | scrollable 101 | setbypasscsp 102 | setextrahttpheaders 103 | setrequestinterception 104 | ssl 105 | startcsscoverage 106 | startjscoverage 107 | stderr 108 | stdout 109 | stopcsscoverage 110 | stopjscoverage 111 | str 112 | stylesheet 113 | tcp 114 | texttrack 115 | timedout 116 | timeline 117 | timestamp 118 | touchend 119 | touchstart 120 | truthy 121 | url 122 | usedbytes 123 | username 124 | viewport 125 | waitfornavigation 126 | waitforrequest 127 | waitforresponse 128 | waitoptions 129 | webkit 130 | webpage 131 | websocket 132 | workercreated 133 | workerdestroyed 134 | www 135 | xhr 136 | xpath 137 | zipfile 138 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miyakogi/pyppeteer/f5313d0e7f973c57ed31fa443cea1834e223a96c/tests/__init__.py -------------------------------------------------------------------------------- /tests/base.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import unittest 5 | 6 | from syncer import sync 7 | 8 | from pyppeteer import launch 9 | from pyppeteer.util import get_free_port 10 | 11 | from .server import get_application 12 | 13 | DEFAULT_OPTIONS = {'args': ['--no-sandbox']} 14 | 15 | 16 | class BaseTestCase(unittest.TestCase): 17 | @classmethod 18 | def setUpClass(cls): 19 | cls.port = get_free_port() 20 | cls.app = get_application() 21 | cls.server = cls.app.listen(cls.port) 22 | cls.browser = sync(launch(DEFAULT_OPTIONS)) 23 | cls.url = 'http://localhost:{}/'.format(cls.port) 24 | 25 | @classmethod 26 | def tearDownClass(cls): 27 | sync(cls.browser.close()) 28 | cls.server.stop() 29 | 30 | def setUp(self): 31 | self.context = sync(self.browser.createIncognitoBrowserContext()) 32 | self.page = sync(self.context.newPage()) 33 | self.result = False 34 | 35 | def tearDown(self): 36 | sync(self.context.close()) 37 | self.context = None 38 | self.page = None 39 | 40 | def set_result(self, value): 41 | self.result = value 42 | -------------------------------------------------------------------------------- /tests/blank_800x600.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miyakogi/pyppeteer/f5313d0e7f973c57ed31fa443cea1834e223a96c/tests/blank_800x600.png -------------------------------------------------------------------------------- /tests/closeme.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import asyncio 5 | 6 | from pyppeteer import launch 7 | 8 | 9 | async def main() -> None: 10 | browser = await launch(args=['--no-sandbox']) 11 | print(browser.wsEndpoint, flush=True) 12 | 13 | 14 | asyncio.get_event_loop().run_until_complete(main()) 15 | -------------------------------------------------------------------------------- /tests/dumpio.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import asyncio 5 | import sys 6 | 7 | from pyppeteer import launch 8 | 9 | dumpio = '--dumpio' in sys.argv 10 | 11 | 12 | async def main(): 13 | browser = await launch(args=['--no-sandbox'], dumpio=dumpio) 14 | page = await browser.newPage() 15 | await page.evaluate('console.log("DUMPIO_TEST")') 16 | await page.close() 17 | await browser.close() 18 | 19 | 20 | asyncio.get_event_loop().run_until_complete(main()) 21 | -------------------------------------------------------------------------------- /tests/file-to-upload.txt: -------------------------------------------------------------------------------- 1 | contents of the file 2 | -------------------------------------------------------------------------------- /tests/frame_utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | from pyppeteer.frame_manager import Frame 5 | from pyppeteer.page import Page 6 | 7 | 8 | async def attachFrame(page: Page, frameId: str, url: str) -> None: 9 | func = ''' 10 | (frameId, url) => { 11 | const frame = document.createElement('iframe'); 12 | frame.src = url; 13 | frame.id = frameId; 14 | document.body.appendChild(frame); 15 | return new Promise(x => frame.onload = x); 16 | } 17 | ''' 18 | await page.evaluate(func, frameId, url) 19 | 20 | 21 | async def detachFrame(page: Page, frameId: str) -> None: 22 | func = ''' 23 | (frameId) => { 24 | const frame = document.getElementById(frameId); 25 | frame.remove(); 26 | } 27 | ''' 28 | await page.evaluate(func, frameId) 29 | 30 | 31 | async def navigateFrame(page: Page, frameId: str, url: str) -> None: 32 | func = ''' 33 | (frameId, url) => { 34 | const frame = document.getElementById(frameId); 35 | frame.src = url; 36 | return new Promise(x => frame.onload = x); 37 | } 38 | ''' 39 | await page.evaluate(func, frameId, url) 40 | 41 | 42 | def dumpFrames(frame: Frame, indentation: str = '') -> str: 43 | results = [] 44 | results.append(indentation + frame.url) 45 | for child in frame.childFrames: 46 | results.append(dumpFrames(child, ' ' + indentation)) 47 | return '\n'.join(results) 48 | -------------------------------------------------------------------------------- /tests/server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import asyncio 5 | import base64 6 | import functools 7 | import os 8 | from typing import Any, Callable 9 | 10 | from tornado import web 11 | from tornado.log import access_log 12 | 13 | 14 | BASE_HTML = ''' 15 | 16 | main 17 | 18 |

Hello

19 | link1 20 | link2 21 | 22 | 23 | ''' 24 | 25 | 26 | class BaseHandler(web.RequestHandler): 27 | def get(self) -> None: 28 | self.set_header( 29 | 'Cache-Control', 30 | 'no-store, no-cache, must-revalidate, max-age=0', 31 | ) 32 | 33 | 34 | class MainHandler(BaseHandler): 35 | def get(self) -> None: 36 | super().get() 37 | self.write(BASE_HTML) 38 | 39 | 40 | class EmptyHandler(BaseHandler): 41 | def get(self) -> None: 42 | super().get() 43 | self.write('') 44 | 45 | 46 | class LongHandler(BaseHandler): 47 | async def get(self) -> None: 48 | super().get() 49 | await asyncio.sleep(0.1) 50 | self.write('') 51 | 52 | 53 | class LinkHandler1(BaseHandler): 54 | def get(self) -> None: 55 | super().get() 56 | self.set_status(200) 57 | self.write(''' 58 | link1 59 |

Link1

60 | back1 61 | ''') 62 | 63 | 64 | class RedirectHandler1(BaseHandler): 65 | def get(self) -> None: 66 | super().get() 67 | self.redirect('/redirect2') 68 | 69 | 70 | class RedirectHandler2(BaseHandler): 71 | def get(self) -> None: 72 | super().get() 73 | self.write('

redirect2

') 74 | 75 | 76 | class RedirectHandler3(BaseHandler): 77 | def get(self) -> None: 78 | super().get() 79 | self.redirect('/static/one-frame.html') 80 | 81 | 82 | class ResourceRedirectHandler(BaseHandler): 83 | def get(self) -> None: 84 | super().get() 85 | self.write( 86 | '' 87 | '
hello, world!
' 88 | ) 89 | 90 | 91 | class CSSRedirectHandler1(BaseHandler): 92 | def get(self) -> None: 93 | super().get() 94 | self.redirect('/two-style.css') 95 | 96 | 97 | class CSSRedirectHandler2(BaseHandler): 98 | def get(self) -> None: 99 | super().get() 100 | self.redirect('/three-style.css') 101 | 102 | 103 | class CSSRedirectHandler3(BaseHandler): 104 | def get(self) -> None: 105 | super().get() 106 | self.redirect('/four-style.css') 107 | 108 | 109 | class CSSRedirectHandler4(BaseHandler): 110 | def get(self) -> None: 111 | super().get() 112 | self.write('body {box-sizing: border-box;}') 113 | 114 | 115 | class CSPHandler(BaseHandler): 116 | def get(self) -> None: 117 | super().get() 118 | self.set_header('Content-Security-Policy', 'script-src \'self\'') 119 | self.write('') 120 | 121 | 122 | def auth_api(username: str, password: str) -> bool: 123 | if username == 'user' and password == 'pass': 124 | return True 125 | else: 126 | return False 127 | 128 | 129 | def basic_auth(auth: Callable[[str, str], bool]) -> Callable: 130 | def wrapper(f: Callable) -> Callable: 131 | def _request_auth(handler: Any) -> None: 132 | handler.set_header('WWW-Authenticate', 'Basic realm=JSL') 133 | handler.set_status(401) 134 | handler.finish() 135 | 136 | @functools.wraps(f) 137 | def new_f(*args: Any) -> None: 138 | handler = args[0] 139 | 140 | auth_header = handler.request.headers.get('Authorization') 141 | if auth_header is None: 142 | return _request_auth(handler) 143 | if not auth_header.startswith('Basic '): 144 | return _request_auth(handler) 145 | 146 | auth_decoded = base64.b64decode(auth_header[6:]) 147 | username, password = auth_decoded.decode('utf-8').split(':', 2) 148 | 149 | if auth(username, password): 150 | f(*args) 151 | else: 152 | _request_auth(handler) 153 | 154 | return new_f 155 | return wrapper 156 | 157 | 158 | class AuthHandler(BaseHandler): 159 | @basic_auth(auth_api) 160 | def get(self) -> None: 161 | super().get() 162 | self.write('ok') 163 | 164 | 165 | def log_handler(handler: Any) -> None: 166 | """Override tornado's logging.""" 167 | # log only errors (status >= 500) 168 | if handler.get_status() >= 500: 169 | access_log.error( 170 | '{} {}'.format(handler.get_status(), handler._request_summary()) 171 | ) 172 | 173 | 174 | def get_application() -> web.Application: 175 | static_path = os.path.join(os.path.dirname(__file__), 'static') 176 | handlers = [ 177 | ('/', MainHandler), 178 | ('/1', LinkHandler1), 179 | ('/redirect1', RedirectHandler1), 180 | ('/redirect2', RedirectHandler2), 181 | ('/redirect3', RedirectHandler3), 182 | ('/one-style.html', ResourceRedirectHandler), 183 | ('/one-style.css', CSSRedirectHandler1), 184 | ('/two-style.css', CSSRedirectHandler2), 185 | ('/three-style.css', CSSRedirectHandler3), 186 | ('/four-style.css', CSSRedirectHandler4), 187 | ('/auth', AuthHandler), 188 | ('/empty', EmptyHandler), 189 | ('/long', LongHandler), 190 | ('/csp', CSPHandler), 191 | ('/static', web.StaticFileHandler, dict(path=static_path)), 192 | ] 193 | return web.Application( 194 | handlers, 195 | log_function=log_handler, 196 | static_path=static_path, 197 | ) 198 | 199 | 200 | if __name__ == '__main__': 201 | app = get_application() 202 | app.listen(9000) 203 | print('server running on http://localhost:9000') 204 | asyncio.get_event_loop().run_forever() 205 | -------------------------------------------------------------------------------- /tests/static/beforeunload.html: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /tests/static/button.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Button test 5 | 6 | 7 | 8 | 9 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /tests/static/cached/one-style.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: pink; 3 | } 4 | -------------------------------------------------------------------------------- /tests/static/cached/one-style.html: -------------------------------------------------------------------------------- 1 | 2 |
hello, world
3 | -------------------------------------------------------------------------------- /tests/static/checkbox.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Selection Test 5 | 6 | 7 | 8 | 9 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /tests/static/csp.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/static/csscoverage/involved.html: -------------------------------------------------------------------------------- 1 | 19 |
woof!
20 | fancy text 21 | -------------------------------------------------------------------------------- /tests/static/csscoverage/media.html: -------------------------------------------------------------------------------- 1 | 3 |
hello, world
4 | 5 | -------------------------------------------------------------------------------- /tests/static/csscoverage/multiple.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | -------------------------------------------------------------------------------- /tests/static/csscoverage/simple.html: -------------------------------------------------------------------------------- 1 | 5 |
hello, world
6 | -------------------------------------------------------------------------------- /tests/static/csscoverage/sourceurl.html: -------------------------------------------------------------------------------- 1 | 7 | 8 | -------------------------------------------------------------------------------- /tests/static/csscoverage/stylesheet1.css: -------------------------------------------------------------------------------- 1 | body { 2 | color: red; 3 | } 4 | -------------------------------------------------------------------------------- /tests/static/csscoverage/stylesheet2.css: -------------------------------------------------------------------------------- 1 | html { 2 | margin: 0; 3 | padding: 0; 4 | } 5 | -------------------------------------------------------------------------------- /tests/static/csscoverage/unused.html: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /tests/static/detect-touch.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Detect Touch Test 5 | 6 | 7 | 8 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /tests/static/digits/0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miyakogi/pyppeteer/f5313d0e7f973c57ed31fa443cea1834e223a96c/tests/static/digits/0.png -------------------------------------------------------------------------------- /tests/static/digits/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miyakogi/pyppeteer/f5313d0e7f973c57ed31fa443cea1834e223a96c/tests/static/digits/1.png -------------------------------------------------------------------------------- /tests/static/digits/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miyakogi/pyppeteer/f5313d0e7f973c57ed31fa443cea1834e223a96c/tests/static/digits/2.png -------------------------------------------------------------------------------- /tests/static/digits/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miyakogi/pyppeteer/f5313d0e7f973c57ed31fa443cea1834e223a96c/tests/static/digits/3.png -------------------------------------------------------------------------------- /tests/static/digits/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miyakogi/pyppeteer/f5313d0e7f973c57ed31fa443cea1834e223a96c/tests/static/digits/4.png -------------------------------------------------------------------------------- /tests/static/digits/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miyakogi/pyppeteer/f5313d0e7f973c57ed31fa443cea1834e223a96c/tests/static/digits/5.png -------------------------------------------------------------------------------- /tests/static/digits/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miyakogi/pyppeteer/f5313d0e7f973c57ed31fa443cea1834e223a96c/tests/static/digits/6.png -------------------------------------------------------------------------------- /tests/static/digits/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miyakogi/pyppeteer/f5313d0e7f973c57ed31fa443cea1834e223a96c/tests/static/digits/7.png -------------------------------------------------------------------------------- /tests/static/digits/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miyakogi/pyppeteer/f5313d0e7f973c57ed31fa443cea1834e223a96c/tests/static/digits/8.png -------------------------------------------------------------------------------- /tests/static/digits/9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miyakogi/pyppeteer/f5313d0e7f973c57ed31fa443cea1834e223a96c/tests/static/digits/9.png -------------------------------------------------------------------------------- /tests/static/error.html: -------------------------------------------------------------------------------- 1 | 16 | -------------------------------------------------------------------------------- /tests/static/es6/es6import.js: -------------------------------------------------------------------------------- 1 | import num from './es6module.js'; 2 | window.__es6injected = num; 3 | -------------------------------------------------------------------------------- /tests/static/es6/es6module.js: -------------------------------------------------------------------------------- 1 | export default 42; 2 | -------------------------------------------------------------------------------- /tests/static/es6/es6pathimport.js: -------------------------------------------------------------------------------- 1 | import num from '/static/es6/es6module.js'; 2 | window.__es6injected = num; 3 | -------------------------------------------------------------------------------- /tests/static/fileupload.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | File upload test 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /tests/static/frame-204.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/static/frame.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 |
Hi, I'm frame
9 | -------------------------------------------------------------------------------- /tests/static/grid.html: -------------------------------------------------------------------------------- 1 | 28 | 29 | 53 | -------------------------------------------------------------------------------- /tests/static/historyapi.html: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /tests/static/huge-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miyakogi/pyppeteer/f5313d0e7f973c57ed31fa443cea1834e223a96c/tests/static/huge-image.png -------------------------------------------------------------------------------- /tests/static/huge-page.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/static/injectedfile.js: -------------------------------------------------------------------------------- 1 | window.__injected = 42; 2 | window.__injectedError = new Error('hi'); 3 | -------------------------------------------------------------------------------- /tests/static/injectedstyle.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: red; 3 | } 4 | -------------------------------------------------------------------------------- /tests/static/jscoverage/eval.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/static/jscoverage/involved.html: -------------------------------------------------------------------------------- 1 | 16 | -------------------------------------------------------------------------------- /tests/static/jscoverage/multiple.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /tests/static/jscoverage/ranges.html: -------------------------------------------------------------------------------- 1 | 3 | -------------------------------------------------------------------------------- /tests/static/jscoverage/script1.js: -------------------------------------------------------------------------------- 1 | console.log(3); 2 | -------------------------------------------------------------------------------- /tests/static/jscoverage/script2.js: -------------------------------------------------------------------------------- 1 | console.log(3); 2 | -------------------------------------------------------------------------------- /tests/static/jscoverage/simple.html: -------------------------------------------------------------------------------- 1 | 3 | -------------------------------------------------------------------------------- /tests/static/jscoverage/sourceurl.html: -------------------------------------------------------------------------------- 1 | 5 | -------------------------------------------------------------------------------- /tests/static/jscoverage/unused.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/static/keyboard.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Keyboard test 5 | 6 | 7 | 8 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /tests/static/mobile.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/static/modernizr.js: -------------------------------------------------------------------------------- 1 | /*! modernizr 3.5.0 (Custom Build) | MIT * 2 | * https://modernizr.com/download/?-touchevents-setclasses !*/ 3 | !function(e,n,t){function o(e,n){return typeof e===n}function s(){var e,n,t,s,a,i,r;for(var l in c)if(c.hasOwnProperty(l)){if(e=[],n=c[l],n.name&&(e.push(n.name.toLowerCase()),n.options&&n.options.aliases&&n.options.aliases.length))for(t=0;t { 46 | box.style.left = event.pageX + 'px'; 47 | box.style.top = event.pageY + 'px'; 48 | updateButtons(event.buttons); 49 | }, true); 50 | document.addEventListener('mousedown', event => { 51 | updateButtons(event.buttons); 52 | box.classList.add('button-' + event.which); 53 | }, true); 54 | document.addEventListener('mouseup', event => { 55 | updateButtons(event.buttons); 56 | box.classList.remove('button-' + event.which); 57 | }, true); 58 | function updateButtons(buttons) { 59 | for (let i = 0; i < 5; i++) 60 | box.classList.toggle('button-' + i, buttons & (1 << i)); 61 | } 62 | })(); 63 | -------------------------------------------------------------------------------- /tests/static/nested-frames.html: -------------------------------------------------------------------------------- 1 | 15 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /tests/static/offscreenbuttons.html: -------------------------------------------------------------------------------- 1 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 36 | -------------------------------------------------------------------------------- /tests/static/one-frame.html: -------------------------------------------------------------------------------- 1 | > 2 | -------------------------------------------------------------------------------- /tests/static/one-style.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: pink; 3 | } 4 | -------------------------------------------------------------------------------- /tests/static/one-style.html: -------------------------------------------------------------------------------- 1 | 2 |
hello, world!
3 | -------------------------------------------------------------------------------- /tests/static/popup/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Popup 5 | 6 | 7 | I am a popup 8 | 9 | 10 | -------------------------------------------------------------------------------- /tests/static/popup/window-open.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Popup test 5 | 6 | 7 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /tests/static/resetcss.html: -------------------------------------------------------------------------------- 1 | 50 | -------------------------------------------------------------------------------- /tests/static/script.js: -------------------------------------------------------------------------------- 1 | console.log('Cheers!'); 2 | -------------------------------------------------------------------------------- /tests/static/scrollable.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Scrollable test 5 | 6 | 7 | 8 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /tests/static/select.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Selection Test 5 | 6 | 7 | 24 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /tests/static/self-request.html: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /tests/static/serviceworkers/empty/sw.html: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /tests/static/serviceworkers/empty/sw.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miyakogi/pyppeteer/f5313d0e7f973c57ed31fa443cea1834e223a96c/tests/static/serviceworkers/empty/sw.js -------------------------------------------------------------------------------- /tests/static/serviceworkers/fetch/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: pink; 3 | } 4 | -------------------------------------------------------------------------------- /tests/static/serviceworkers/fetch/sw.html: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | -------------------------------------------------------------------------------- /tests/static/serviceworkers/fetch/sw.js: -------------------------------------------------------------------------------- 1 | self.addEventListener('fetch', event => { 2 | event.respondWith(fetch(event.request)); 3 | }); 4 | 5 | self.addEventListener('activate', event => { 6 | event.waitUntil(clients.claim()); 7 | }); 8 | -------------------------------------------------------------------------------- /tests/static/shadow.html: -------------------------------------------------------------------------------- 1 | 18 | -------------------------------------------------------------------------------- /tests/static/simple-extension/index.js: -------------------------------------------------------------------------------- 1 | // Mock script for background extension 2 | -------------------------------------------------------------------------------- /tests/static/simple-extension/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Simple extension", 3 | "version": "0.1", 4 | "app": { 5 | "background": { 6 | "scripts": ["index.js"] 7 | } 8 | }, 9 | "permissions": ["background"], 10 | 11 | "manifest_version": 2 12 | } 13 | -------------------------------------------------------------------------------- /tests/static/simple.json: -------------------------------------------------------------------------------- 1 | {"foo": "bar"} 2 | -------------------------------------------------------------------------------- /tests/static/style.css: -------------------------------------------------------------------------------- 1 | div { 2 | color: blue; 3 | } 4 | -------------------------------------------------------------------------------- /tests/static/sw.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miyakogi/pyppeteer/f5313d0e7f973c57ed31fa443cea1834e223a96c/tests/static/sw.js -------------------------------------------------------------------------------- /tests/static/temperable.html: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /tests/static/textarea.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Textarea test 5 | 6 | 7 | 8 | 9 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /tests/static/touches.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Touch test 5 | 6 | 7 | 8 | 9 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /tests/static/two-frames.html: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /tests/static/worker/worker.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Worker test 5 | 6 | 7 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /tests/static/worker/worker.js: -------------------------------------------------------------------------------- 1 | console.log('hello from the worker'); 2 | function workerFunction() { 3 | return 'worker function result'; 4 | } 5 | self.addEventListener('message', event => { 6 | console.log('got this data: ' + event.data); 7 | }); 8 | (async function() { 9 | while (true) { 10 | self.postMessage(workerFunction.toString()); 11 | await new Promise(x => setTimeout(x, 100)); 12 | } 13 | })(); 14 | -------------------------------------------------------------------------------- /tests/static/wrappedlink.html: -------------------------------------------------------------------------------- 1 | 22 |
23 | 123321 24 |
25 | 27 | -------------------------------------------------------------------------------- /tests/test_abnormal_crash.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import asyncio 5 | import logging 6 | import unittest 7 | 8 | from syncer import sync 9 | 10 | from pyppeteer import launch 11 | from pyppeteer.chromium_downloader import current_platform 12 | from pyppeteer.errors import NetworkError 13 | 14 | 15 | class TestBrowserCrash(unittest.TestCase): 16 | @sync 17 | async def test_browser_crash_send(self): 18 | browser = await launch(args=['--no-sandbox']) 19 | page = await browser.newPage() 20 | await page.goto('about:blank') 21 | await page.querySelector("title") 22 | browser.process.terminate() 23 | browser.process.wait() 24 | 25 | if current_platform().startswith('win'): 26 | # wait for terminating browser process 27 | await asyncio.sleep(1) 28 | 29 | with self.assertRaises(NetworkError): 30 | await page.querySelector("title") 31 | with self.assertRaises(NetworkError): 32 | with self.assertLogs('pyppeteer', logging.ERROR): 33 | await page.querySelector("title") 34 | with self.assertRaises(ConnectionError): 35 | await browser.newPage() 36 | -------------------------------------------------------------------------------- /tests/test_browser.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import asyncio 5 | from copy import deepcopy 6 | import os 7 | from pathlib import Path 8 | import unittest 9 | 10 | from syncer import sync 11 | 12 | from pyppeteer import connect, launch 13 | 14 | from .base import BaseTestCase, DEFAULT_OPTIONS 15 | from .utils import waitEvent 16 | 17 | 18 | class TestBrowser(unittest.TestCase): 19 | extensionPath = Path(__file__).parent / 'static' / 'simple-extension' 20 | extensionOptions = { 21 | 'headless': False, 22 | 'args': [ 23 | '--no-sandbox', 24 | '--disable-extensions-except={}'.format(extensionPath), 25 | '--load-extensions={}'.format(extensionPath), 26 | ] 27 | } 28 | 29 | def waitForBackgroundPageTarget(self, browser): 30 | promise = asyncio.get_event_loop().create_future() 31 | for target in browser.targets(): 32 | if target.type == 'background_page': 33 | promise.set_result(target) 34 | return promise 35 | 36 | def _listener(target) -> None: 37 | if target.type != 'background_page': 38 | return 39 | browser.removeListener(_listener) 40 | promise.set_result(target) 41 | 42 | browser.on('targetcreated', _listener) 43 | return promise 44 | 45 | @sync 46 | async def test_browser_process(self): 47 | browser = await launch(DEFAULT_OPTIONS) 48 | process = browser.process 49 | self.assertGreater(process.pid, 0) 50 | wsEndpoint = browser.wsEndpoint 51 | browser2 = await connect({'browserWSEndpoint': wsEndpoint}) 52 | self.assertIsNone(browser2.process) 53 | await browser.close() 54 | 55 | @sync 56 | async def test_version(self): 57 | browser = await launch(DEFAULT_OPTIONS) 58 | version = await browser.version() 59 | self.assertTrue(len(version) > 0) 60 | self.assertTrue(version.startswith('Headless')) 61 | await browser.close() 62 | 63 | @sync 64 | async def test_user_agent(self): 65 | browser = await launch(DEFAULT_OPTIONS) 66 | userAgent = await browser.userAgent() 67 | self.assertGreater(len(userAgent), 0) 68 | self.assertIn('WebKit', userAgent) 69 | await browser.close() 70 | 71 | @sync 72 | async def test_disconnect(self): 73 | browser = await launch(DEFAULT_OPTIONS) 74 | endpoint = browser.wsEndpoint 75 | browser1 = await connect(browserWSEndpoint=endpoint) 76 | browser2 = await connect(browserWSEndpoint=endpoint) 77 | discon = [] 78 | discon1 = [] 79 | discon2 = [] 80 | browser.on('disconnected', lambda: discon.append(1)) 81 | browser1.on('disconnected', lambda: discon1.append(1)) 82 | browser2.on('disconnected', lambda: discon2.append(1)) 83 | 84 | await asyncio.wait([ 85 | browser2.disconnect(), 86 | waitEvent(browser2, 'disconnected'), 87 | ]) 88 | self.assertEqual(len(discon), 0) 89 | self.assertEqual(len(discon1), 0) 90 | self.assertEqual(len(discon2), 1) 91 | 92 | await asyncio.wait([ 93 | waitEvent(browser1, 'disconnected'), 94 | waitEvent(browser, 'disconnected'), 95 | browser.close(), 96 | ]) 97 | self.assertEqual(len(discon), 1) 98 | self.assertEqual(len(discon1), 1) 99 | self.assertEqual(len(discon2), 1) 100 | 101 | @sync 102 | async def test_crash(self): 103 | browser = await launch(DEFAULT_OPTIONS) 104 | page = await browser.newPage() 105 | errors = [] 106 | page.on('error', lambda e: errors.append(e)) 107 | asyncio.ensure_future(page.goto('chrome://crash')) 108 | for i in range(100): 109 | await asyncio.sleep(0.01) 110 | if errors: 111 | break 112 | await browser.close() 113 | self.assertTrue(errors) 114 | 115 | @unittest.skipIf('CI' in os.environ, 'skip in-browser test on CI server') 116 | @sync 117 | async def test_background_target_type(self): 118 | browser = await launch(self.extensionOptions) 119 | page = await browser.newPage() 120 | backgroundPageTarget = await self.waitForBackgroundPageTarget(browser) 121 | await page.close() 122 | await browser.close() 123 | self.assertTrue(backgroundPageTarget) 124 | 125 | @unittest.skipIf('CI' in os.environ, 'skip in-browser test on CI server') 126 | @sync 127 | async def test_OOPIF(self): 128 | options = deepcopy(DEFAULT_OPTIONS) 129 | options['headless'] = False 130 | browser = await launch(options) 131 | page = await browser.newPage() 132 | example_page = 'http://example.com/' 133 | await page.goto(example_page) 134 | await page.setRequestInterception(True) 135 | 136 | async def intercept(req): 137 | await req.respond({'body': 'YO, GOOGLE.COM'}) 138 | 139 | page.on('request', lambda req: asyncio.ensure_future(intercept(req))) 140 | await page.evaluate('''() => { 141 | const frame = document.createElement('iframe'); 142 | frame.setAttribute('src', 'https://google.com/'); 143 | document.body.appendChild(frame); 144 | return new Promise(x => frame.onload = x); 145 | }''') 146 | await page.waitForSelector('iframe[src="https://google.com/"]') 147 | urls = [] 148 | for frame in page.frames: 149 | urls.append(frame.url) 150 | urls.sort() 151 | self.assertEqual(urls, [example_page, 'https://google.com/']) 152 | await browser.close() 153 | 154 | @unittest.skipIf('CI' in os.environ, 'skip in-browser test on CI server') 155 | @sync 156 | async def test_background_page(self): 157 | browserWithExtension = await launch(self.extensionOptions) 158 | backgroundPageTarget = await self.waitForBackgroundPageTarget(browserWithExtension) # noqa: E501 159 | self.assertIsNotNone(backgroundPageTarget) 160 | page = await backgroundPageTarget.page() 161 | self.assertEqual(await page.evaluate('2 * 3'), 6) 162 | await browserWithExtension.close() 163 | 164 | 165 | class TestPageClose(BaseTestCase): 166 | @sync 167 | async def test_not_visible_in_browser_pages(self): 168 | newPage = await self.context.newPage() 169 | self.assertIn(newPage, await self.browser.pages()) 170 | await newPage.close() 171 | self.assertNotIn(newPage, await self.browser.pages()) 172 | 173 | @sync 174 | async def test_before_unload(self): 175 | newPage = await self.context.newPage() 176 | await newPage.goto(self.url + 'static/beforeunload.html') 177 | await newPage.click('body') 178 | asyncio.ensure_future(newPage.close(runBeforeUnload=True)) 179 | dialog = await waitEvent(newPage, 'dialog') 180 | self.assertEqual(dialog.type, 'beforeunload') 181 | self.assertEqual(dialog.defaultValue, '') 182 | self.assertEqual(dialog.message, '') 183 | asyncio.ensure_future(dialog.accept()) 184 | await waitEvent(newPage, 'close') 185 | 186 | @sync 187 | async def test_page_close_state(self): 188 | newPage = await self.context.newPage() 189 | self.assertFalse(newPage.isClosed()) 190 | await newPage.close() 191 | self.assertTrue(newPage.isClosed()) 192 | -------------------------------------------------------------------------------- /tests/test_browser_context.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import asyncio 5 | import unittest 6 | 7 | from pyppeteer import connect 8 | from pyppeteer.errors import BrowserError 9 | 10 | from syncer import sync 11 | 12 | from .base import BaseTestCase 13 | from .utils import waitEvent 14 | 15 | 16 | class BrowserBaseTestCase(BaseTestCase): 17 | def setUp(self): 18 | pass 19 | 20 | def tearDown(self): 21 | pass 22 | 23 | 24 | class TestBrowserContext(BrowserBaseTestCase): 25 | @sync 26 | async def test_default_context(self): 27 | self.assertEqual(len(self.browser.browserContexts), 1) 28 | defaultContext = self.browser.browserContexts[0] 29 | self.assertFalse(defaultContext.isIncognito()) 30 | with self.assertRaises(BrowserError) as cm: 31 | await defaultContext.close() 32 | self.assertIn('cannot be closed', cm.exception.args[0]) 33 | 34 | @unittest.skip('this test not pass in some environment') 35 | @sync 36 | async def test_incognito_context(self): 37 | self.assertEqual(len(self.browser.browserContexts), 1) 38 | context = await self.browser.createIncognitoBrowserContext() 39 | self.assertTrue(context.isIncognito()) 40 | self.assertEqual(len(self.browser.browserContexts), 2) 41 | self.assertIn(context, self.browser.browserContexts) 42 | await context.close() 43 | self.assertEqual(len(self.browser.browserContexts), 1) 44 | 45 | @sync 46 | async def test_close_all_targets_once(self): 47 | self.assertEqual(len(await self.browser.pages()), 1) 48 | context = await self.browser.createIncognitoBrowserContext() 49 | await context.newPage() 50 | self.assertEqual(len(await self.browser.pages()), 2) 51 | self.assertEqual(len(await context.pages()), 1) 52 | await context.close() 53 | self.assertEqual(len(await self.browser.pages()), 1) 54 | 55 | @sync 56 | async def test_window_open_use_parent_tab_context(self): 57 | context = await self.browser.createIncognitoBrowserContext() 58 | page = await context.newPage() 59 | await page.goto(self.url + 'empty') 60 | asyncio.ensure_future( 61 | page.evaluate('url => window.open(url)', self.url + 'empty')) 62 | popupTarget = await waitEvent(self.browser, 'targetcreated') 63 | self.assertEqual(popupTarget.browserContext, context) 64 | await context.close() 65 | 66 | @sync 67 | async def test_fire_target_event(self): 68 | context = await self.browser.createIncognitoBrowserContext() 69 | events = [] 70 | context.on('targetcreated', lambda t: events.append('CREATED: ' + t.url)) # noqa: E501 71 | context.on('targetchanged', lambda t: events.append('CHANGED: ' + t.url)) # noqa: E501 72 | context.on('targetdestroyed', lambda t: events.append('DESTROYED: ' + t.url)) # noqa: E501 73 | page = await context.newPage() 74 | await page.goto(self.url + 'empty') 75 | await page.close() 76 | self.assertEqual(events, [ 77 | 'CREATED: about:blank', 78 | 'CHANGED: ' + self.url + 'empty', 79 | 'DESTROYED: ' + self.url + 'empty', 80 | ]) 81 | 82 | @unittest.skip('this test not pass in some environment') 83 | @sync 84 | async def test_isolate_local_storage_and_cookie(self): 85 | context1 = await self.browser.createIncognitoBrowserContext() 86 | context2 = await self.browser.createIncognitoBrowserContext() 87 | self.assertEqual(len(context1.targets()), 0) 88 | self.assertEqual(len(context2.targets()), 0) 89 | 90 | # create a page in the first incognito context 91 | page1 = await context1.newPage() 92 | await page1.goto(self.url + 'empty') 93 | await page1.evaluate('''() => { 94 | localStorage.setItem('name', 'page1'); 95 | document.cookie = 'name=page1'; 96 | }''') 97 | 98 | self.assertEqual(len(context1.targets()), 1) 99 | self.assertEqual(len(context2.targets()), 0) 100 | 101 | # create a page in the second incognito context 102 | page2 = await context2.newPage() 103 | await page2.goto(self.url + 'empty') 104 | await page2.evaluate('''() => { 105 | localStorage.setItem('name', 'page2'); 106 | document.cookie = 'name=page2'; 107 | }''') 108 | 109 | self.assertEqual(len(context1.targets()), 1) 110 | self.assertEqual(context1.targets()[0], page1.target) 111 | self.assertEqual(len(context2.targets()), 1) 112 | self.assertEqual(context2.targets()[0], page2.target) 113 | 114 | # make sure pages don't share local storage and cookie 115 | self.assertEqual(await page1.evaluate('localStorage.getItem("name")'), 'page1') # noqa: E501 116 | self.assertEqual(await page1.evaluate('document.cookie'), 'name=page1') 117 | self.assertEqual(await page2.evaluate('localStorage.getItem("name")'), 'page2') # noqa: E501 118 | self.assertEqual(await page2.evaluate('document.cookie'), 'name=page2') 119 | 120 | await context1.close() 121 | await context2.close() 122 | self.assertEqual(len(self.browser.browserContexts), 1) 123 | 124 | @sync 125 | async def test_across_session(self): 126 | self.assertEqual(len(self.browser.browserContexts), 1) 127 | context = await self.browser.createIncognitoBrowserContext() 128 | self.assertEqual(len(self.browser.browserContexts), 2) 129 | remoteBrowser = await connect( 130 | browserWSEndpoint=self.browser.wsEndpoint) 131 | contexts = remoteBrowser.browserContexts 132 | self.assertEqual(len(contexts), 2) 133 | await remoteBrowser.disconnect() 134 | await context.close() 135 | -------------------------------------------------------------------------------- /tests/test_connection.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | from syncer import sync 5 | 6 | from pyppeteer.errors import NetworkError 7 | 8 | from .base import BaseTestCase 9 | 10 | 11 | class TestConnection(BaseTestCase): 12 | @sync 13 | async def test_error_msg(self): 14 | with self.assertRaises(NetworkError) as cm: 15 | await self.page._client.send('ThisCommand.DoesNotExists') 16 | self.assertIn('ThisCommand.DoesNotExists', cm.exception.args[0]) 17 | 18 | 19 | class TestCDPSession(BaseTestCase): 20 | @sync 21 | async def test_create_session(self): 22 | client = await self.page.target.createCDPSession() 23 | await client.send('Runtime.enable') 24 | await client.send('Runtime.evaluate', 25 | {'expression': 'window.foo = "bar"'}) 26 | foo = await self.page.evaluate('window.foo') 27 | self.assertEqual(foo, 'bar') 28 | 29 | @sync 30 | async def test_send_event(self): 31 | client = await self.page.target.createCDPSession() 32 | await client.send('Network.enable') 33 | events = [] 34 | client.on('Network.requestWillBeSent', lambda e: events.append(e)) 35 | await self.page.goto(self.url + 'empty') 36 | self.assertEqual(len(events), 1) 37 | 38 | @sync 39 | async def test_enable_disable_domain(self): 40 | client = await self.page.target.createCDPSession() 41 | await client.send('Runtime.enable') 42 | await client.send('Debugger.enable') 43 | await self.page.coverage.startJSCoverage() 44 | await self.page.coverage.stopJSCoverage() 45 | 46 | @sync 47 | async def test_detach(self): 48 | client = await self.page.target.createCDPSession() 49 | await client.send('Runtime.enable') 50 | evalResponse = await client.send( 51 | 'Runtime.evaluate', {'expression': '1 + 2', 'returnByValue': True}) 52 | self.assertEqual(evalResponse['result']['value'], 3) 53 | 54 | await client.detach() 55 | with self.assertRaises(NetworkError): 56 | await client.send( 57 | 'Runtime.evaluate', 58 | {'expression': '1 + 3', 'returnByValue': True} 59 | ) 60 | -------------------------------------------------------------------------------- /tests/test_coverage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | from syncer import sync 5 | 6 | from .base import BaseTestCase 7 | 8 | 9 | class TestJSCoverage(BaseTestCase): 10 | @sync 11 | async def test_js_coverage(self): 12 | await self.page.coverage.startJSCoverage() 13 | await self.page.goto( 14 | self.url + 'static/jscoverage/simple.html', 15 | waitUntil='networkidle0', 16 | ) 17 | coverage = await self.page.coverage.stopJSCoverage() 18 | self.assertEqual(len(coverage), 1) 19 | self.assertIn('/jscoverage/simple.html', coverage[0]['url']) 20 | self.assertEqual(coverage[0]['ranges'], [ 21 | {'start': 0, 'end': 17}, 22 | {'start': 35, 'end': 61}, 23 | ]) 24 | 25 | @sync 26 | async def test_js_coverage_source_url(self): 27 | await self.page.coverage.startJSCoverage() 28 | await self.page.goto(self.url + 'static/jscoverage/sourceurl.html') 29 | coverage = await self.page.coverage.stopJSCoverage() 30 | self.assertEqual(len(coverage), 1) 31 | self.assertEqual(coverage[0]['url'], 'nicename.js') 32 | 33 | @sync 34 | async def test_js_coverage_ignore_empty(self): 35 | await self.page.coverage.startJSCoverage() 36 | await self.page.goto(self.url + 'empty') 37 | coverage = await self.page.coverage.stopJSCoverage() 38 | self.assertEqual(coverage, []) 39 | 40 | @sync 41 | async def test_ignore_eval_script_by_default(self): 42 | await self.page.coverage.startJSCoverage() 43 | await self.page.goto(self.url + 'static/jscoverage/eval.html') 44 | coverage = await self.page.coverage.stopJSCoverage() 45 | self.assertEqual(len(coverage), 1) 46 | 47 | @sync 48 | async def test_not_ignore_eval_script_with_reportAnonymousScript(self): 49 | await self.page.coverage.startJSCoverage(reportAnonymousScript=True) 50 | await self.page.goto(self.url + 'static/jscoverage/eval.html') 51 | coverage = await self.page.coverage.stopJSCoverage() 52 | self.assertTrue(any(entry for entry in coverage 53 | if entry['url'].startswith('debugger://'))) 54 | self.assertEqual(len(coverage), 2) 55 | 56 | @sync 57 | async def test_ignore_injected_script(self): 58 | await self.page.coverage.startJSCoverage() 59 | await self.page.goto(self.url + 'empty') 60 | await self.page.evaluate('console.log("foo")') 61 | await self.page.evaluate('() => console.log("bar")') 62 | coverage = await self.page.coverage.stopJSCoverage() 63 | self.assertEqual(len(coverage), 0) 64 | 65 | @sync 66 | async def test_ignore_injected_script_with_reportAnonymousScript(self): 67 | await self.page.coverage.startJSCoverage(reportAnonymousScript=True) 68 | await self.page.goto(self.url + 'empty') 69 | await self.page.evaluate('console.log("foo")') 70 | await self.page.evaluate('() => console.log("bar")') 71 | coverage = await self.page.coverage.stopJSCoverage() 72 | self.assertEqual(len(coverage), 0) 73 | 74 | @sync 75 | async def test_js_coverage_multiple_script(self): 76 | await self.page.coverage.startJSCoverage() 77 | await self.page.goto(self.url + 'static/jscoverage/multiple.html') 78 | coverage = await self.page.coverage.stopJSCoverage() 79 | self.assertEqual(len(coverage), 2) 80 | coverage.sort(key=lambda cov: cov['url']) 81 | self.assertIn('/jscoverage/script1.js', coverage[0]['url']) 82 | self.assertIn('/jscoverage/script2.js', coverage[1]['url']) 83 | 84 | @sync 85 | async def test_js_coverage_ranges(self): 86 | await self.page.coverage.startJSCoverage() 87 | await self.page.goto(self.url + 'static/jscoverage/ranges.html') 88 | coverage = await self.page.coverage.stopJSCoverage() 89 | self.assertEqual(len(coverage), 1) 90 | entry = coverage[0] 91 | self.assertEqual(len(entry['ranges']), 1) 92 | range = entry['ranges'][0] 93 | self.assertEqual( 94 | entry['text'][range['start']:range['end']], 95 | 'console.log(\'used!\');', 96 | ) 97 | 98 | @sync 99 | async def test_no_coverage(self): 100 | await self.page.coverage.startJSCoverage() 101 | await self.page.goto(self.url + 'static/jscoverage/unused.html') 102 | coverage = await self.page.coverage.stopJSCoverage() 103 | self.assertEqual(len(coverage), 1) 104 | entry = coverage[0] 105 | self.assertIn('static/jscoverage/unused.html', entry['url']) 106 | self.assertEqual(len(entry['ranges']), 0) 107 | 108 | @sync 109 | async def test_js_coverage_condition(self): 110 | await self.page.coverage.startJSCoverage() 111 | await self.page.goto(self.url + 'static/jscoverage/involved.html') 112 | coverage = await self.page.coverage.stopJSCoverage() 113 | expected_range = [ 114 | {'start': 0, 'end': 35}, 115 | {'start': 50, 'end': 100}, 116 | {'start': 107, 'end': 141}, 117 | {'start': 148, 'end': 160}, 118 | {'start': 168, 'end': 207}, 119 | ] 120 | self.assertEqual(coverage[0]['ranges'], expected_range) 121 | 122 | @sync 123 | async def test_js_coverage_no_reset_navigation(self): 124 | await self.page.coverage.startJSCoverage(resetOnNavigation=False) 125 | await self.page.goto(self.url + 'static/jscoverage/multiple.html') 126 | await self.page.goto(self.url + 'empty') 127 | coverage = await self.page.coverage.stopJSCoverage() 128 | self.assertEqual(len(coverage), 2) 129 | 130 | @sync 131 | async def test_js_coverage_reset_navigation(self): 132 | await self.page.coverage.startJSCoverage() # enabled by default 133 | await self.page.goto(self.url + 'static/jscoverage/multiple.html') 134 | await self.page.goto(self.url + 'empty') 135 | coverage = await self.page.coverage.stopJSCoverage() 136 | self.assertEqual(len(coverage), 0) 137 | 138 | 139 | class TestCSSCoverage(BaseTestCase): 140 | @sync 141 | async def test_css_coverage(self): 142 | await self.page.coverage.startCSSCoverage() 143 | await self.page.goto(self.url + 'static/csscoverage/simple.html') 144 | coverage = await self.page.coverage.stopCSSCoverage() 145 | self.assertEqual(len(coverage), 1) 146 | self.assertIn('/csscoverage/simple.html', coverage[0]['url']) 147 | self.assertEqual(coverage[0]['ranges'], [{'start': 1, 'end': 22}]) 148 | range = coverage[0]['ranges'][0] 149 | self.assertEqual( 150 | coverage[0]['text'][range['start']:range['end']], 151 | 'div { color: green; }', 152 | ) 153 | 154 | @sync 155 | async def test_css_coverage_url(self): 156 | await self.page.coverage.startCSSCoverage() 157 | await self.page.goto(self.url + 'static/csscoverage/sourceurl.html') 158 | coverage = await self.page.coverage.stopCSSCoverage() 159 | self.assertEqual(len(coverage), 1) 160 | self.assertEqual(coverage[0]['url'], 'nicename.css') 161 | 162 | @sync 163 | async def test_css_coverage_multiple(self): 164 | await self.page.coverage.startCSSCoverage() 165 | await self.page.goto(self.url + 'static/csscoverage/multiple.html') 166 | coverage = await self.page.coverage.stopCSSCoverage() 167 | self.assertEqual(len(coverage), 2) 168 | coverage.sort(key=lambda cov: cov['url']) 169 | self.assertIn('/csscoverage/stylesheet1.css', coverage[0]['url']) 170 | self.assertIn('/csscoverage/stylesheet2.css', coverage[1]['url']) 171 | 172 | @sync 173 | async def test_css_coverage_no_coverage(self): 174 | await self.page.coverage.startCSSCoverage() 175 | await self.page.goto(self.url + 'static/csscoverage/unused.html') 176 | coverage = await self.page.coverage.stopCSSCoverage() 177 | self.assertEqual(len(coverage), 1) 178 | self.assertEqual(coverage[0]['url'], 'unused.css') 179 | self.assertEqual(coverage[0]['ranges'], []) 180 | 181 | @sync 182 | async def test_css_coverage_media(self): 183 | await self.page.coverage.startCSSCoverage() 184 | await self.page.goto(self.url + 'static/csscoverage/media.html') 185 | coverage = await self.page.coverage.stopCSSCoverage() 186 | self.assertEqual(len(coverage), 1) 187 | self.assertIn('/csscoverage/media.html', coverage[0]['url']) 188 | self.assertEqual(coverage[0]['ranges'], [{'start': 17, 'end': 38}]) 189 | 190 | @sync 191 | async def test_css_coverage_complicated(self): 192 | await self.page.coverage.startCSSCoverage() 193 | await self.page.goto(self.url + 'static/csscoverage/involved.html') 194 | coverage = await self.page.coverage.stopCSSCoverage() 195 | self.assertEqual(len(coverage), 1) 196 | range = coverage[0]['ranges'] 197 | self.assertEqual(range, [ 198 | {'start': 20, 'end': 168}, 199 | {'start': 198, 'end': 304}, 200 | ]) 201 | 202 | @sync 203 | async def test_css_ignore_injected_css(self): 204 | await self.page.goto(self.url + 'empty') 205 | await self.page.coverage.startCSSCoverage() 206 | await self.page.addStyleTag(content='body { margin: 10px; }') 207 | # trigger style recalc 208 | margin = await self.page.evaluate( 209 | '() => window.getComputedStyle(document.body).margin') 210 | self.assertEqual(margin, '10px') 211 | coverage = await self.page.coverage.stopCSSCoverage() 212 | self.assertEqual(coverage, []) 213 | 214 | @sync 215 | async def test_css_coverage_no_reset_navigation(self): 216 | await self.page.coverage.startCSSCoverage(resetOnNavigation=False) 217 | await self.page.goto(self.url + 'static/csscoverage/multiple.html') 218 | await self.page.goto(self.url + 'empty') 219 | coverage = await self.page.coverage.stopCSSCoverage() 220 | self.assertEqual(len(coverage), 2) 221 | 222 | @sync 223 | async def test_css_coverage_reset_navigation(self): 224 | await self.page.coverage.startCSSCoverage() # enabled by default 225 | await self.page.goto(self.url + 'static/csscoverage/multiple.html') 226 | await self.page.goto(self.url + 'empty') 227 | coverage = await self.page.coverage.stopCSSCoverage() 228 | self.assertEqual(len(coverage), 0) 229 | -------------------------------------------------------------------------------- /tests/test_dialog.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import asyncio 5 | 6 | from syncer import sync 7 | 8 | from .base import BaseTestCase 9 | 10 | 11 | class TestDialog(BaseTestCase): 12 | @sync 13 | async def test_alert(self): 14 | def dialog_test(dialog): 15 | self.assertEqual(dialog.type, 'alert') 16 | self.assertEqual(dialog.defaultValue, '') 17 | self.assertEqual(dialog.message, 'yo') 18 | asyncio.ensure_future(dialog.accept()) 19 | self.page.on('dialog', dialog_test) 20 | await self.page.evaluate('() => alert("yo")') 21 | 22 | @sync 23 | async def test_prompt(self): 24 | def dialog_test(dialog): 25 | self.assertEqual(dialog.type, 'prompt') 26 | self.assertEqual(dialog.defaultValue, 'yes.') 27 | self.assertEqual(dialog.message, 'question?') 28 | asyncio.ensure_future(dialog.accept('answer!')) 29 | self.page.on('dialog', dialog_test) 30 | answer = await self.page.evaluate('() => prompt("question?", "yes.")') 31 | self.assertEqual(answer, 'answer!') 32 | 33 | @sync 34 | async def test_prompt_dismiss(self): 35 | def dismiss_test(dialog, *args): 36 | asyncio.ensure_future(dialog.dismiss()) 37 | self.page.on('dialog', dismiss_test) 38 | result = await self.page.evaluate('() => prompt("question?", "yes.")') 39 | self.assertIsNone(result) 40 | -------------------------------------------------------------------------------- /tests/test_execution_context.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | from syncer import sync 5 | 6 | from pyppeteer.errors import ElementHandleError, NetworkError 7 | 8 | from .base import BaseTestCase 9 | 10 | 11 | class TestQueryObject(BaseTestCase): 12 | @sync 13 | async def test_query_objects(self): 14 | await self.page.goto(self.url + 'empty') 15 | await self.page.evaluate( 16 | '() => window.set = new Set(["hello", "world"])' 17 | ) 18 | prototypeHandle = await self.page.evaluateHandle('() => Set.prototype') 19 | objectsHandle = await self.page.queryObjects(prototypeHandle) 20 | count = await self.page.evaluate( 21 | 'objects => objects.length', 22 | objectsHandle, 23 | ) 24 | self.assertEqual(count, 1) 25 | values = await self.page.evaluate( 26 | 'objects => Array.from(objects[0].values())', 27 | objectsHandle, 28 | ) 29 | self.assertEqual(values, ['hello', 'world']) 30 | 31 | @sync 32 | async def test_query_objects_disposed(self): 33 | await self.page.goto(self.url + 'empty') 34 | prototypeHandle = await self.page.evaluateHandle( 35 | '() => HTMLBodyElement.prototype' 36 | ) 37 | await prototypeHandle.dispose() 38 | with self.assertRaises(ElementHandleError): 39 | await self.page.queryObjects(prototypeHandle) 40 | 41 | @sync 42 | async def test_query_objects_primitive_value_error(self): 43 | await self.page.goto(self.url + 'empty') 44 | prototypeHandle = await self.page.evaluateHandle('() => 42') 45 | with self.assertRaises(ElementHandleError): 46 | await self.page.queryObjects(prototypeHandle) 47 | 48 | 49 | class TestJSHandle(BaseTestCase): 50 | @sync 51 | async def test_get_property(self): 52 | handle1 = await self.page.evaluateHandle( 53 | '() => ({one: 1, two: 2, three: 3})' 54 | ) 55 | handle2 = await handle1.getProperty('two') 56 | self.assertEqual(await handle2.jsonValue(), 2) 57 | 58 | @sync 59 | async def test_json_value(self): 60 | handle1 = await self.page.evaluateHandle('() => ({foo: "bar"})') 61 | json = await handle1.jsonValue() 62 | self.assertEqual(json, {'foo': 'bar'}) 63 | 64 | @sync 65 | async def test_json_date_fail(self): 66 | handle = await self.page.evaluateHandle( 67 | '() => new Date("2017-09-26T00:00:00.000Z")' 68 | ) 69 | json = await handle.jsonValue() 70 | self.assertEqual(json, {}) 71 | 72 | @sync 73 | async def test_json_circular_object_error(self): 74 | windowHandle = await self.page.evaluateHandle('window') 75 | with self.assertRaises(NetworkError) as cm: 76 | await windowHandle.jsonValue() 77 | self.assertIn('Object reference chain is too long', 78 | cm.exception.args[0]) 79 | 80 | @sync 81 | async def test_get_properties(self): 82 | handle1 = await self.page.evaluateHandle('() => ({foo: "bar"})') 83 | properties = await handle1.getProperties() 84 | foo = properties.get('foo') 85 | self.assertTrue(foo) 86 | self.assertEqual(await foo.jsonValue(), 'bar') 87 | 88 | @sync 89 | async def test_return_non_own_properties(self): 90 | aHandle = await self.page.evaluateHandle('''() => { 91 | class A { 92 | constructor() { 93 | this.a = '1'; 94 | } 95 | } 96 | class B extends A { 97 | constructor() { 98 | super(); 99 | this.b = '2'; 100 | } 101 | } 102 | return new B(); 103 | }''') 104 | properties = await aHandle.getProperties() 105 | self.assertEqual(await properties.get('a').jsonValue(), '1') 106 | self.assertEqual(await properties.get('b').jsonValue(), '2') 107 | 108 | @sync 109 | async def test_as_element(self): 110 | aHandle = await self.page.evaluateHandle('() => document.body') 111 | element = aHandle.asElement() 112 | self.assertTrue(element) 113 | 114 | @sync 115 | async def test_as_element_non_element(self): 116 | aHandle = await self.page.evaluateHandle('() => 2') 117 | element = aHandle.asElement() 118 | self.assertIsNone(element) 119 | 120 | @sync 121 | async def test_as_element_text_node(self): 122 | await self.page.setContent('
ee!
') 123 | aHandle = await self.page.evaluateHandle( 124 | '() => document.querySelector("div").firstChild') 125 | element = aHandle.asElement() 126 | self.assertTrue(element) 127 | self.assertTrue(await self.page.evaluate( 128 | '(e) => e.nodeType === HTMLElement.TEXT_NODE', 129 | element, 130 | )) 131 | 132 | @sync 133 | async def test_to_string_number(self): 134 | handle = await self.page.evaluateHandle('() => 2') 135 | self.assertEqual(handle.toString(), 'JSHandle:2') 136 | 137 | @sync 138 | async def test_to_string_str(self): 139 | handle = await self.page.evaluateHandle('() => "a"') 140 | self.assertEqual(handle.toString(), 'JSHandle:a') 141 | 142 | @sync 143 | async def test_to_string_complicated_object(self): 144 | handle = await self.page.evaluateHandle('() => window') 145 | self.assertEqual(handle.toString(), 'JSHandle@object') 146 | -------------------------------------------------------------------------------- /tests/test_misc.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import logging 5 | import unittest 6 | 7 | import pyppeteer 8 | from pyppeteer.helper import debugError, get_positive_int 9 | from pyppeteer.page import convertPrintParameterToInches 10 | 11 | 12 | class TestVersion(unittest.TestCase): 13 | def test_version(self): 14 | version = pyppeteer.version 15 | self.assertTrue(isinstance(version, str)) 16 | self.assertEqual(version.count('.'), 2) 17 | 18 | def test_version_info(self): 19 | vinfo = pyppeteer.version_info 20 | self.assertEqual(len(vinfo), 3) 21 | for i in vinfo: 22 | self.assertTrue(isinstance(i, int)) 23 | 24 | 25 | class TestDefaultArgs(unittest.TestCase): 26 | def test_default_args(self): 27 | self.assertIn('--no-first-run', pyppeteer.defaultArgs()) 28 | self.assertIn('--headless', pyppeteer.defaultArgs()) 29 | self.assertNotIn('--headless', pyppeteer.defaultArgs({'headless': False})) # noqa: E501 30 | self.assertIn('--user-data-dir=foo', pyppeteer.defaultArgs(userDataDir='foo')) # noqa: E501 31 | 32 | 33 | class TestToInches(unittest.TestCase): 34 | def test_px(self): 35 | self.assertEqual( 36 | convertPrintParameterToInches('12px'), 37 | 12.0 / 96, 38 | ) 39 | 40 | def test_inch(self): 41 | self.assertAlmostEqual( 42 | convertPrintParameterToInches('12in'), 43 | 12.0, 44 | ) 45 | 46 | def test_cm(self): 47 | self.assertAlmostEqual( 48 | convertPrintParameterToInches('12cm'), 49 | 12.0 * 37.8 / 96, 50 | ) 51 | 52 | def test_mm(self): 53 | self.assertAlmostEqual( 54 | convertPrintParameterToInches('12mm'), 55 | 12.0 * 3.78 / 96, 56 | ) 57 | 58 | 59 | class TestPositiveInt(unittest.TestCase): 60 | def test_badtype(self): 61 | with self.assertRaises(TypeError): 62 | get_positive_int({'a': 'b'}, 'a') 63 | 64 | def test_negative_int(self): 65 | with self.assertRaises(ValueError): 66 | get_positive_int({'a': -1}, 'a') 67 | 68 | 69 | class TestDebugError(unittest.TestCase): 70 | def setUp(self): 71 | self._old_debug = pyppeteer.DEBUG 72 | self.logger = logging.getLogger('pyppeteer.test') 73 | 74 | def tearDown(self): 75 | pyppeteer.DEBUG = self._old_debug 76 | 77 | def test_debug_default(self): 78 | with self.assertLogs('pyppeteer.test', logging.DEBUG): 79 | debugError(self.logger, 'test') 80 | with self.assertRaises(AssertionError): 81 | with self.assertLogs('pyppeteer', logging.INFO): 82 | debugError(self.logger, 'test') 83 | 84 | def test_debug_enabled(self): 85 | pyppeteer.DEBUG = True 86 | with self.assertLogs('pyppeteer.test', logging.ERROR): 87 | debugError(self.logger, 'test') 88 | 89 | def test_debug_enable_disable(self): 90 | pyppeteer.DEBUG = True 91 | with self.assertLogs('pyppeteer.test', logging.ERROR): 92 | debugError(self.logger, 'test') 93 | pyppeteer.DEBUG = False 94 | with self.assertLogs('pyppeteer.test', logging.DEBUG): 95 | debugError(self.logger, 'test') 96 | with self.assertRaises(AssertionError): 97 | with self.assertLogs('pyppeteer.test', logging.INFO): 98 | debugError(self.logger, 'test') 99 | 100 | def test_debug_logger(self): 101 | with self.assertRaises(AssertionError): 102 | with self.assertLogs('pyppeteer', logging.DEBUG): 103 | debugError(logging.getLogger('test'), 'test message') 104 | -------------------------------------------------------------------------------- /tests/test_pyppeteer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | test_pyppeteer 6 | ---------------------------------- 7 | 8 | Tests for `pyppeteer` module. 9 | """ 10 | 11 | import asyncio 12 | import logging 13 | from pathlib import Path 14 | 15 | from syncer import sync 16 | 17 | from .base import BaseTestCase 18 | 19 | 20 | class TestPyppeteer(BaseTestCase): 21 | @sync 22 | async def test_get_https(self): 23 | await self.page.goto('https://example.com/') 24 | self.assertEqual(self.page.url, 'https://example.com/') 25 | 26 | @sync 27 | async def test_get_facebook(self): 28 | await self.page.goto('https://www.facebook.com/') 29 | self.assertEqual(self.page.url, 'https://www.facebook.com/') 30 | 31 | @sync 32 | async def test_plain_text_depr(self): 33 | await self.page.goto(self.url) 34 | with self.assertLogs('pyppeteer', logging.WARN) as log: 35 | text = await self.page.plainText() 36 | self.assertIn('deprecated', log.records[0].msg) 37 | self.assertEqual(text.split(), ['Hello', 'link1', 'link2']) 38 | 39 | @sync 40 | async def test_inject_file(self): # deprecated 41 | tmp_file = Path('tmp.js') 42 | with tmp_file.open('w') as f: 43 | f.write(''' 44 | () => document.body.appendChild(document.createElement("section")) 45 | '''.strip()) 46 | with self.assertLogs('pyppeteer', logging.WARN) as log: 47 | await self.page.injectFile(str(tmp_file)) 48 | self.assertIn('deprecated', log.records[0].msg) 49 | await self.page.waitForSelector('section') 50 | self.assertIsNotNone(await self.page.J('section')) 51 | tmp_file.unlink() 52 | 53 | 54 | class TestScreenshot(BaseTestCase): 55 | def setUp(self): 56 | super().setUp() 57 | self.target_path = Path(__file__).resolve().parent / 'test.png' 58 | if self.target_path.exists(): 59 | self.target_path.unlink() 60 | 61 | def tearDown(self): 62 | if self.target_path.exists(): 63 | self.target_path.unlink() 64 | super().tearDown() 65 | 66 | @sync 67 | async def test_screenshot_large(self): 68 | page = await self.context.newPage() 69 | await page.setViewport({ 70 | 'width': 2000, 71 | 'height': 2000, 72 | }) 73 | await page.goto(self.url + 'static/huge-page.html') 74 | options = {'path': str(self.target_path)} 75 | self.assertFalse(self.target_path.exists()) 76 | await asyncio.wait_for(page.screenshot(options), 30) 77 | self.assertTrue(self.target_path.exists()) 78 | with self.target_path.open('rb') as fh: 79 | bytes = fh.read() 80 | self.assertGreater(len(bytes), 2**20) 81 | -------------------------------------------------------------------------------- /tests/test_screenshot.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import base64 5 | from pathlib import Path 6 | from unittest import TestCase 7 | 8 | from syncer import sync 9 | 10 | from pyppeteer import launch 11 | 12 | root_path = Path(__file__).resolve().parent 13 | blank_png_path = root_path / 'blank_800x600.png' 14 | blank_pdf_path = root_path / 'blank.pdf' 15 | 16 | 17 | class TestScreenShot(TestCase): 18 | def setUp(self): 19 | self.browser = sync(launch(args=['--no-sandbox'])) 20 | self.target_path = Path(__file__).resolve().parent / 'test.png' 21 | if self.target_path.exists(): 22 | self.target_path.unlink() 23 | 24 | def tearDown(self): 25 | if self.target_path.exists(): 26 | self.target_path.unlink() 27 | sync(self.browser.close()) 28 | 29 | @sync 30 | async def test_screenshot(self): 31 | page = await self.browser.newPage() 32 | await page.goto('about:blank') 33 | options = {'path': str(self.target_path)} 34 | self.assertFalse(self.target_path.exists()) 35 | await page.screenshot(options) 36 | self.assertTrue(self.target_path.exists()) 37 | 38 | with self.target_path.open('rb') as f: 39 | result = f.read() 40 | with blank_png_path.open('rb') as f: 41 | sample = f.read() 42 | self.assertEqual(result, sample) 43 | 44 | @sync 45 | async def test_screenshot_binary(self): 46 | page = await self.browser.newPage() 47 | await page.goto('about:blank') 48 | result = await page.screenshot() 49 | with blank_png_path.open('rb') as f: 50 | sample = f.read() 51 | self.assertEqual(result, sample) 52 | 53 | @sync 54 | async def test_screenshot_base64(self): 55 | page = await self.browser.newPage() 56 | await page.goto('about:blank') 57 | options = {'encoding': 'base64'} 58 | result = await page.screenshot(options) 59 | with blank_png_path.open('rb') as f: 60 | sample = f.read() 61 | self.assertEqual(base64.b64decode(result), sample) 62 | 63 | @sync 64 | async def test_screenshot_element(self): 65 | page = await self.browser.newPage() 66 | await page.goto('http://example.com') 67 | element = await page.J('h1') 68 | options = {'path': str(self.target_path)} 69 | self.assertFalse(self.target_path.exists()) 70 | await element.screenshot(options) 71 | self.assertTrue(self.target_path.exists()) 72 | 73 | @sync 74 | async def test_unresolved_mimetype(self): 75 | page = await self.browser.newPage() 76 | await page.goto('about:blank') 77 | options = {'path': 'example.unsupported'} 78 | with self.assertRaises(ValueError, msg='mime type: unsupported'): 79 | await page.screenshot(options) 80 | 81 | 82 | class TestPDF(TestCase): 83 | def setUp(self): 84 | self.browser = sync(launch(args=['--no-sandbox'])) 85 | self.target_path = Path(__file__).resolve().parent / 'test.pdf' 86 | if self.target_path.exists(): 87 | self.target_path.unlink() 88 | 89 | @sync 90 | async def test_pdf(self): 91 | page = await self.browser.newPage() 92 | await page.goto('about:blank') 93 | self.assertFalse(self.target_path.exists()) 94 | await page.pdf(path=str(self.target_path)) 95 | self.assertTrue(self.target_path.exists()) 96 | self.assertTrue(self.target_path.stat().st_size >= 800) 97 | 98 | def tearDown(self): 99 | if self.target_path.exists: 100 | self.target_path.unlink() 101 | sync(self.browser.close()) 102 | -------------------------------------------------------------------------------- /tests/test_target.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import asyncio 5 | import unittest 6 | 7 | from syncer import sync 8 | 9 | from .base import BaseTestCase 10 | 11 | 12 | class TestTarget(BaseTestCase): 13 | @sync 14 | async def test_targets(self): 15 | targets = self.browser.targets() 16 | _list = [target for target in targets 17 | if target.type == 'page' and target.url == 'about:blank'] 18 | self.assertTrue(any(_list)) 19 | target_types = [t.type for t in targets] 20 | self.assertIn('browser', target_types) 21 | 22 | @sync 23 | async def test_return_all_pages(self): 24 | pages = await self.context.pages() 25 | self.assertEqual(len(pages), 1) 26 | self.assertIn(self.page, pages) 27 | 28 | @sync 29 | async def test_browser_target(self): 30 | targets = self.browser.targets() 31 | browserTarget = [t for t in targets if t.type == 'browser'] 32 | self.assertTrue(browserTarget) 33 | 34 | @sync 35 | async def test_default_page(self): 36 | pages = await self.browser.pages() 37 | page = [page for page in pages if page != self.page][0] 38 | self.assertEqual(await page.evaluate('["Hello", "world"].join(" ")'), 39 | 'Hello world') 40 | self.assertTrue(await page.J('body')) 41 | 42 | @sync 43 | async def test_report_new_page(self): 44 | otherPagePromise = asyncio.get_event_loop().create_future() 45 | self.context.once('targetcreated', 46 | lambda target: otherPagePromise.set_result(target)) 47 | await self.page.evaluate( 48 | 'url => window.open(url)', 49 | 'http://127.0.0.1:{}'.format(self.port)) 50 | otherPage = await (await otherPagePromise).page() 51 | 52 | self.assertIn('127.0.0.1', otherPage.url) 53 | self.assertEqual( 54 | await otherPage.evaluate('["Hello", "world"].join(" ")'), 55 | 'Hello world') 56 | self.assertTrue(await otherPage.J('body')) 57 | 58 | pages = await self.context.pages() 59 | self.assertIn(self.page, pages) 60 | self.assertIn(otherPage, pages) 61 | 62 | closePagePromise = asyncio.get_event_loop().create_future() 63 | 64 | async def get_close_page(target): 65 | page = await target.page() 66 | closePagePromise.set_result(page) 67 | 68 | self.context.once('targetdestroyed', 69 | lambda t: asyncio.ensure_future(get_close_page(t))) 70 | await otherPage.close() 71 | self.assertEqual(await closePagePromise, otherPage) 72 | 73 | pages = await self.context.pages() 74 | self.assertIn(self.page, pages) 75 | self.assertNotIn(otherPage, pages) 76 | 77 | @sync 78 | async def test_report_service_worker(self): 79 | await self.page.goto(self.url + 'empty') 80 | createdTargetPromise = asyncio.get_event_loop().create_future() 81 | self.context.once('targetcreated', 82 | lambda t: createdTargetPromise.set_result(t)) 83 | 84 | await self.page.goto(self.url + 'static/serviceworkers/empty/sw.html') 85 | createdTarget = await createdTargetPromise 86 | self.assertEqual(createdTarget.type, 'service_worker') 87 | self.assertEqual( 88 | createdTarget.url, self.url + 'static/serviceworkers/empty/sw.js') 89 | 90 | destroyedTargetPromise = asyncio.get_event_loop().create_future() 91 | self.context.once('targetdestroyed', 92 | lambda t: destroyedTargetPromise.set_result(t)) 93 | await self.page.evaluate( 94 | '() => window.registrationPromise.then(reg => reg.unregister())') 95 | destroyedTarget = await destroyedTargetPromise 96 | self.assertEqual(destroyedTarget, createdTarget) 97 | 98 | @sync 99 | async def test_url_change(self): 100 | await self.page.goto(self.url + 'empty') 101 | 102 | changedTargetPromise = asyncio.get_event_loop().create_future() 103 | self.context.once('targetchanged', 104 | lambda t: changedTargetPromise.set_result(t)) 105 | await self.page.goto('http://127.0.0.1:{}/'.format(self.port)) 106 | changedTarget = await changedTargetPromise 107 | self.assertEqual(changedTarget.url, 108 | 'http://127.0.0.1:{}/'.format(self.port)) 109 | 110 | changedTargetPromise = asyncio.get_event_loop().create_future() 111 | self.context.once('targetchanged', 112 | lambda t: changedTargetPromise.set_result(t)) 113 | await self.page.goto(self.url + 'empty') 114 | changedTarget = await changedTargetPromise 115 | self.assertEqual(changedTarget.url, self.url + 'empty') 116 | 117 | @sync 118 | async def test_not_report_uninitialized_page(self): 119 | changedTargets = [] 120 | 121 | def listener(target): 122 | changedTargets.append(target) 123 | 124 | self.context.on('targetchanged', listener) 125 | 126 | targetPromise = asyncio.get_event_loop().create_future() 127 | self.context.once('targetcreated', 128 | lambda t: targetPromise.set_result(t)) 129 | newPagePromise = asyncio.ensure_future(self.context.newPage()) 130 | target = await targetPromise 131 | self.assertEqual(target.url, 'about:blank') 132 | 133 | newPage = await newPagePromise 134 | targetPromise2 = asyncio.get_event_loop().create_future() 135 | self.context.once('targetcreated', 136 | lambda t: targetPromise2.set_result(t)) 137 | evaluatePromise = asyncio.ensure_future( 138 | newPage.evaluate('window.open("about:blank")')) 139 | target2 = await targetPromise2 140 | self.assertEqual(target2.url, 'about:blank') 141 | await evaluatePromise 142 | await newPage.close() 143 | 144 | self.assertFalse(changedTargets) 145 | self.context.remove_listener('targetchanged', listener) 146 | 147 | # cleanup 148 | await (await target2.page()).close() 149 | 150 | @unittest.skip('Need server-side implementation') 151 | @sync 152 | async def test_crash_while_redirect(self): 153 | pass 154 | 155 | @sync 156 | async def test_opener(self): 157 | await self.page.goto(self.url + 'empty') 158 | targetPromise = asyncio.get_event_loop().create_future() 159 | self.context.once('targetcreated', 160 | lambda target: targetPromise.set_result(target)) 161 | await self.page.goto(self.url + 'static/popup/window-open.html') 162 | createdTarget = await targetPromise 163 | self.assertEqual( 164 | (await createdTarget.page()).url, 165 | self.url + 'static/popup/popup.html', 166 | ) 167 | self.assertEqual(createdTarget.opener, self.page.target) 168 | self.assertIsNone(self.page.target.opener) 169 | await (await createdTarget.page()).close() 170 | -------------------------------------------------------------------------------- /tests/test_tracing.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import json 5 | from pathlib import Path 6 | import unittest 7 | 8 | from syncer import sync 9 | 10 | from pyppeteer.errors import NetworkError 11 | 12 | from .base import BaseTestCase 13 | 14 | 15 | class TestTracing(BaseTestCase): 16 | def setUp(self): 17 | self.outfile = Path(__file__).parent / 'trace.json' 18 | if self.outfile.is_file(): 19 | self.outfile.unlink() 20 | super().setUp() 21 | 22 | def tearDown(self): 23 | if self.outfile.is_file(): 24 | self.outfile.unlink() 25 | super().tearDown() 26 | 27 | @sync 28 | async def test_tracing(self): 29 | await self.page.tracing.start({ 30 | 'path': str(self.outfile) 31 | }) 32 | await self.page.goto(self.url) 33 | await self.page.tracing.stop() 34 | self.assertTrue(self.outfile.is_file()) 35 | 36 | @sync 37 | async def test_custom_categories(self): 38 | await self.page.tracing.start({ 39 | 'path': str(self.outfile), 40 | 'categories': ['disabled-by-default-v8.cpu_profiler.hires'], 41 | }) 42 | await self.page.tracing.stop() 43 | self.assertTrue(self.outfile.is_file()) 44 | with self.outfile.open() as f: 45 | trace_json = json.load(f) 46 | self.assertIn( 47 | 'disabled-by-default-v8.cpu_profiler.hires', 48 | trace_json['metadata']['trace-config'], 49 | ) 50 | 51 | @sync 52 | async def test_tracing_two_page_error(self): 53 | await self.page.tracing.start({'path': str(self.outfile)}) 54 | new_page = await self.browser.newPage() 55 | with self.assertRaises(NetworkError): 56 | await new_page.tracing.start({'path': str(self.outfile)}) 57 | await new_page.close() 58 | await self.page.tracing.stop() 59 | 60 | @sync 61 | async def test_return_buffer(self): 62 | await self.page.tracing.start(screenshots=True, path=str(self.outfile)) 63 | await self.page.goto(self.url + 'static/grid.html') 64 | trace = await self.page.tracing.stop() 65 | with self.outfile.open('r') as f: 66 | buf = f.read() 67 | self.assertEqual(trace, buf) 68 | 69 | @unittest.skip('Not implemented') 70 | @sync 71 | async def test_return_null_on_error(self): 72 | await self.page.tracing.start(screenshots=True) 73 | await self.page.goto(self.url + 'static/grid.html') 74 | 75 | @sync 76 | async def test_without_path(self): 77 | await self.page.tracing.start(screenshots=True) 78 | await self.page.goto(self.url + 'static/grid.html') 79 | trace = await self.page.tracing.stop() 80 | self.assertIn('screenshot', trace) 81 | -------------------------------------------------------------------------------- /tests/test_worker.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import asyncio 5 | 6 | from syncer import sync 7 | 8 | from .base import BaseTestCase 9 | 10 | 11 | class TestWorker(BaseTestCase): 12 | @sync 13 | async def test_worker(self): 14 | await self.page.goto(self.url + 'static/worker/worker.html') 15 | await self.page.waitForFunction('() => !!worker') 16 | worker = self.page.workers[0] 17 | self.assertIn('worker.js', worker.url) 18 | executionContext = await worker.executionContext() 19 | self.assertEqual( 20 | await executionContext.evaluate('self.workerFunction()'), 21 | 'worker function result', 22 | ) 23 | 24 | @sync 25 | async def test_create_destroy_events(self): 26 | workerCreatedPromise = asyncio.get_event_loop().create_future() 27 | self.page.once('workercreated', 28 | lambda w: workerCreatedPromise.set_result(w)) 29 | workerObj = await self.page.evaluateHandle( 30 | '() => new Worker("data:text/javascript,1")') 31 | worker = await workerCreatedPromise 32 | workerDestroyedPromise = asyncio.get_event_loop().create_future() 33 | self.page.once('workerdestroyed', 34 | lambda w: workerDestroyedPromise.set_result(w)) 35 | await self.page.evaluate( 36 | 'workerObj => workerObj.terminate()', workerObj) 37 | self.assertEqual(await workerDestroyedPromise, worker) 38 | 39 | @sync 40 | async def test_report_console_logs(self): 41 | logPromise = asyncio.get_event_loop().create_future() 42 | self.page.once('console', lambda m: logPromise.set_result(m)) 43 | await self.page.evaluate( 44 | '() => new Worker("data:text/javascript,console.log(1)")' 45 | ) 46 | log = await logPromise 47 | self.assertEqual(log.text, '1') 48 | 49 | @sync 50 | async def test_jshandle_for_console_log(self): 51 | logPromise = asyncio.get_event_loop().create_future() 52 | self.page.on('console', lambda m: logPromise.set_result(m)) 53 | await self.page.evaluate( 54 | '() => new Worker("data:text/javascript,console.log(1,2,3,this)")') 55 | log = await logPromise 56 | self.assertEqual(log.text, '1 2 3 JSHandle@object') 57 | self.assertEqual(len(log.args), 4) 58 | self.assertEqual( 59 | await (await log.args[3].getProperty('origin')).jsonValue(), 60 | 'null', 61 | ) 62 | 63 | @sync 64 | async def test_execution_context(self): 65 | workerCreatedPromise = asyncio.get_event_loop().create_future() 66 | self.page.once('workercreated', 67 | lambda w: workerCreatedPromise.set_result(w)) 68 | await self.page.evaluate( 69 | '() => new Worker("data:text/javascript,console.log(1)")') 70 | worker = await workerCreatedPromise 71 | self.assertEqual( 72 | await (await worker.executionContext()).evaluate('1+1'), 2) 73 | self.assertEqual(await worker.evaluate('1+2'), 3) 74 | 75 | @sync 76 | async def test_report_error(self): 77 | errorPromise = asyncio.get_event_loop().create_future() 78 | self.page.on('pageerror', lambda x: errorPromise.set_result(x)) 79 | await self.page.evaluate('() => new Worker(`data:text/javascript, throw new Error("this is my error");`)') # noqa: E501 80 | errorLog = await errorPromise 81 | self.assertIn('this is my error', errorLog.args[0]) 82 | -------------------------------------------------------------------------------- /tests/utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import asyncio 5 | 6 | 7 | def waitEvent(emitter, event_name): 8 | fut = asyncio.get_event_loop().create_future() 9 | 10 | def set_done(arg=None): 11 | fut.set_result(arg) 12 | 13 | emitter.once(event_name, set_done) 14 | return fut 15 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py{35,36,37},doit 3 | 4 | [testenv] 5 | passenv = DISPLAY CI HOME 6 | changedir = 7 | py35: {toxworkdir} 8 | recreate = 9 | py35: true 10 | whitelist_externals = 11 | py35: cp 12 | deps = 13 | py35,py36,py37: -rrequirements-test.txt 14 | py37: coverage 15 | commands = 16 | pyppeteer-install 17 | py35: cp -r {toxinidir}/tests {toxworkdir} 18 | py35: python -m unittest discover {toxworkdir} 19 | py36: python -m unittest discover 20 | py37: coverage run -m unittest discover 21 | py37: coverage report 22 | 23 | [testenv:doit] 24 | deps = 25 | doit 26 | flake8 27 | mypy 28 | pydocstyle 29 | readme_renderer 30 | -rrequirements-docs.txt 31 | commands = 32 | doit 33 | 34 | [testenv:codecov] 35 | passenv = CI TRAVIS TRAVIS_* 36 | deps = codecov 37 | skip_install = true 38 | commands = 39 | codecov 40 | 41 | 42 | [flake8] 43 | exclude = docs,.svn,CVS,.bzr,.hg,.git,__pycache__,.tox,.eggs,*.egg,out 44 | max-complexity = 7 45 | 46 | [pydocstyle] 47 | ignore = D105,D107,D203,D213,D402,D404 48 | match_dir = (?!(tmp|docs|ja_docs|tests|\.)).* 49 | --------------------------------------------------------------------------------