diff --git a/.bandit.yml b/.bandit.yml index 41f1bb597..2aae8a0aa 100644 --- a/.bandit.yml +++ b/.bandit.yml @@ -1,5 +1,6 @@ skips: - B101 +- B113 # https://github.com/PyCQA/bandit/issues/1010 - B105 - B301 - B303 @@ -17,3 +18,4 @@ skips: - B503 - B603 - B605 +exclude_dirs: ['tests'] diff --git a/.bumpversion.cfg b/.bumpversion.cfg index b949d81c4..f76bf783d 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 2.7.1 +current_version = 2.11.0 commit = True tag = True tag_name = {new_version} diff --git a/.flake8 b/.flake8 index 0c64d009e..544d72956 100644 --- a/.flake8 +++ b/.flake8 @@ -1,7 +1,7 @@ [flake8] max-line-length = 119 -ignore = W503 +ignore = W503, E203 exclude = docs/conf.py diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 000000000..dbcebfa0a --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,7 @@ +# .git-blame-ignore-revs +# adding black formatter to all the code +e211ec0aa26ecae0da8ae55d064ea60e1efe4d0d +# re applying black to the code with default line length +303f0a70fcf8067adf0a909c2096a5009162383a +# reaplying black again and removing line length on pre-commit black config +c5cdd0d30ceb68ccba04af0e71d1b8e6678e2962 \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md new file mode 100644 index 000000000..63cae77e7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/question.md @@ -0,0 +1,13 @@ +--- +name: Question / Help +about: Ask a question about Scrapy or ask for help with your Scrapy code. +--- + +Thanks for taking an interest in Scrapy! + +The Scrapy GitHub issue tracker is not meant for questions or help. Please ask +for help in the [Scrapy community resources](https://scrapy.org/community/) +instead. + +The GitHub issue tracker's purpose is to deal with bug reports and feature +requests for the project itself. diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index e9f9a6aea..d6fc0f6c5 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -1,6 +1,10 @@ name: Checks on: [push, pull_request] +concurrency: + group: ${{github.workflow}}-${{ github.ref }} + cancel-in-progress: true + jobs: checks: runs-on: ubuntu-latest @@ -8,27 +12,21 @@ jobs: fail-fast: false matrix: include: - - python-version: "3.11" - env: - TOXENV: security - - python-version: "3.11" - env: - TOXENV: flake8 - - python-version: "3.11" + - python-version: "3.12" env: TOXENV: pylint - - python-version: 3.7 + - python-version: 3.8 env: TOXENV: typing - python-version: "3.11" # Keep in sync with .readthedocs.yml env: TOXENV: docs - - python-version: "3.11" + - python-version: "3.12" env: TOXENV: twinecheck steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 @@ -40,3 +38,9 @@ jobs: run: | pip install -U tox tox + + pre-commit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: pre-commit/action@v3.0.0 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index eee9a4f02..affaa32a5 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,31 +1,25 @@ name: Publish -on: [push] +on: + push: + tags: + - '[0-9]+.[0-9]+.[0-9]+' + +concurrency: + group: ${{github.workflow}}-${{ github.ref }} + cancel-in-progress: true jobs: publish: runs-on: ubuntu-latest - if: startsWith(github.event.ref, 'refs/tags/') - steps: - - uses: actions/checkout@v3 - - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: "3.11" - - - name: Check Tag - id: check-release-tag - run: | - if [[ ${{ github.event.ref }} =~ ^refs/tags/[0-9]+[.][0-9]+[.][0-9]+(rc[0-9]+|[.]dev[0-9]+)?$ ]]; then - echo ::set-output name=release_tag::true - fi - - - name: Publish to PyPI - if: steps.check-release-tag.outputs.release_tag == 'true' - run: | - pip install --upgrade build twine - python -m build - export TWINE_USERNAME=__token__ - export TWINE_PASSWORD=${{ secrets.PYPI_TOKEN }} - twine upload dist/* + - uses: actions/checkout@v4 + - uses: actions/setup-python@v4 + with: + python-version: 3.12 + - run: | + pip install --upgrade build twine + python -m build + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@v1.6.4 + with: + password: ${{ secrets.PYPI_TOKEN }} diff --git a/.github/workflows/tests-macos.yml b/.github/workflows/tests-macos.yml index 174d245ca..252176464 100644 --- a/.github/workflows/tests-macos.yml +++ b/.github/workflows/tests-macos.yml @@ -1,16 +1,20 @@ name: macOS on: [push, pull_request] +concurrency: + group: ${{github.workflow}}-${{ github.ref }} + cancel-in-progress: true + jobs: tests: runs-on: macos-11 strategy: fail-fast: false matrix: - python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 diff --git a/.github/workflows/tests-ubuntu.yml b/.github/workflows/tests-ubuntu.yml index 8fcf90a18..f50a4d104 100644 --- a/.github/workflows/tests-ubuntu.yml +++ b/.github/workflows/tests-ubuntu.yml @@ -1,6 +1,10 @@ name: Ubuntu on: [push, pull_request] +concurrency: + group: ${{github.workflow}}-${{ github.ref }} + cancel-in-progress: true + jobs: tests: runs-on: ubuntu-latest @@ -8,9 +12,6 @@ jobs: fail-fast: false matrix: include: - - python-version: 3.8 - env: - TOXENV: py - python-version: 3.9 env: TOXENV: py @@ -20,30 +21,45 @@ jobs: - python-version: "3.11" env: TOXENV: py - - python-version: "3.11" + - python-version: "3.12" + env: + TOXENV: py + - python-version: "3.12" env: TOXENV: asyncio - python-version: pypy3.9 env: TOXENV: pypy3 + - python-version: pypy3.10 + env: + TOXENV: pypy3 # pinned deps - - python-version: 3.7.13 + - python-version: 3.8.17 env: TOXENV: pinned - - python-version: 3.7.13 + - python-version: 3.8.17 env: TOXENV: asyncio-pinned - - python-version: pypy3.7 + - python-version: pypy3.8 env: TOXENV: pypy3-pinned + - python-version: 3.8.17 + env: + TOXENV: extra-deps-pinned + - python-version: 3.8.17 + env: + TOXENV: botocore-pinned - - python-version: "3.11" + - python-version: "3.12" env: TOXENV: extra-deps + - python-version: "3.12" + env: + TOXENV: botocore steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 @@ -51,7 +67,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Install system libraries - if: matrix.python-version == 'pypy3.9' || contains(matrix.env.TOXENV, 'pinned') + if: contains(matrix.python-version, 'pypy') || contains(matrix.env.TOXENV, 'pinned') run: | sudo apt-get update sudo apt-get install libxml2-dev libxslt-dev diff --git a/.github/workflows/tests-windows.yml b/.github/workflows/tests-windows.yml index f60c48841..757d62285 100644 --- a/.github/workflows/tests-windows.yml +++ b/.github/workflows/tests-windows.yml @@ -1,6 +1,10 @@ name: Windows on: [push, pull_request] +concurrency: + group: ${{github.workflow}}-${{ github.ref }} + cancel-in-progress: true + jobs: tests: runs-on: windows-latest @@ -8,31 +12,27 @@ jobs: fail-fast: false matrix: include: - - python-version: 3.7 - env: - TOXENV: windows-pinned - python-version: 3.8 env: - TOXENV: py + TOXENV: windows-pinned - python-version: 3.9 env: TOXENV: py - python-version: "3.10" env: TOXENV: py - - python-version: "3.10" + - python-version: "3.11" + env: + TOXENV: py + - python-version: "3.12" + env: + TOXENV: py + - python-version: "3.12" env: TOXENV: asyncio -# no binary package for lxml for 3.11 yet -# - python-version: "3.11" -# env: -# TOXENV: py -# - python-version: "3.11" -# env: -# TOXENV: asyncio steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 diff --git a/.isort.cfg b/.isort.cfg new file mode 100644 index 000000000..f238bf7ea --- /dev/null +++ b/.isort.cfg @@ -0,0 +1,2 @@ +[settings] +profile = black diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 000000000..0cff5cc73 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,24 @@ +repos: +- repo: https://github.com/PyCQA/bandit + rev: 1.7.5 + hooks: + - id: bandit + args: [-r, -c, .bandit.yml] +- repo: https://github.com/PyCQA/flake8 + rev: 6.1.0 + hooks: + - id: flake8 +- repo: https://github.com/psf/black.git + rev: 23.9.1 + hooks: + - id: black +- repo: https://github.com/pycqa/isort + rev: 5.12.0 + hooks: + - id: isort +- repo: https://github.com/adamchainz/blacken-docs + rev: 1.16.0 + hooks: + - id: blacken-docs + additional_dependencies: + - black==23.9.1 diff --git a/MANIFEST.in b/MANIFEST.in index ae7db51fa..4920dc0c3 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -7,6 +7,7 @@ include NEWS include scrapy/VERSION include scrapy/mime.types +include scrapy/py.typed include codecov.yml include conftest.py diff --git a/README.rst b/README.rst index 970bf2c35..14adff648 100644 --- a/README.rst +++ b/README.rst @@ -17,9 +17,10 @@ Scrapy :target: https://github.com/scrapy/scrapy/actions?query=workflow%3AUbuntu :alt: Ubuntu -.. image:: https://github.com/scrapy/scrapy/workflows/macOS/badge.svg - :target: https://github.com/scrapy/scrapy/actions?query=workflow%3AmacOS - :alt: macOS +.. .. image:: https://github.com/scrapy/scrapy/workflows/macOS/badge.svg + .. :target: https://github.com/scrapy/scrapy/actions?query=workflow%3AmacOS + .. :alt: macOS + .. image:: https://github.com/scrapy/scrapy/workflows/Windows/badge.svg :target: https://github.com/scrapy/scrapy/actions?query=workflow%3AWindows @@ -41,7 +42,7 @@ Scrapy Overview ======== -Scrapy is a fast high-level web crawling and web scraping framework, used to +Scrapy is a BSD-licensed fast high-level web crawling and web scraping framework, used to crawl websites and extract structured data from their pages. It can be used for a wide range of purposes, from data mining to monitoring and automated testing. @@ -58,7 +59,7 @@ including a list of features. Requirements ============ -* Python 3.7+ +* Python 3.8+ * Works on Linux, Windows, macOS, BSD Install @@ -110,4 +111,4 @@ See https://scrapy.org/companies/ for a list. Commercial Support ================== -See https://scrapy.org/support/ for details. +See https://scrapy.org/support/ for details. \ No newline at end of file diff --git a/artwork/README.rst b/artwork/README.rst index 8a1028cde..c1880ef6c 100644 --- a/artwork/README.rst +++ b/artwork/README.rst @@ -2,19 +2,19 @@ Scrapy artwork ============== -This folder contains Scrapy artwork resources such as logos and fonts. +This folder contains the Scrapy artwork resources such as logos and fonts. scrapy-logo.jpg --------------- -Main Scrapy logo, in JPEG format. +The main Scrapy logo, in JPEG format. qlassik.zip ----------- -Font used for Scrapy logo. Homepage: https://www.dafont.com/qlassik.font +The font used for the Scrapy logo. Homepage: https://www.dafont.com/qlassik.font scrapy-blog.logo.xcf -------------------- -The logo used in Scrapy blog, in Gimp format. +The logo used in the Scrapy blog, in Gimp format. diff --git a/conftest.py b/conftest.py index 2a5d55083..2bfa46f5a 100644 --- a/conftest.py +++ b/conftest.py @@ -1,36 +1,43 @@ +import platform +import sys from pathlib import Path import pytest +from twisted import version as twisted_version +from twisted.python.versions import Version from twisted.web.http import H2_ENABLED from scrapy.utils.reactor import install_reactor - from tests.keys import generate_keys def _py_files(folder): - return (str(p) for p in Path(folder).rglob('*.py')) + return (str(p) for p in Path(folder).rglob("*.py")) collect_ignore = [ # not a test, but looks like a test "scrapy/utils/testsite.py", + "tests/ftpserver.py", + "tests/mockserver.py", + "tests/pipelines.py", + "tests/spiders.py", # contains scripts to be run by tests/test_crawler.py::CrawlerProcessSubprocess *_py_files("tests/CrawlerProcess"), # contains scripts to be run by tests/test_crawler.py::CrawlerRunnerSubprocess *_py_files("tests/CrawlerRunner"), ] -with Path('tests/ignores.txt').open(encoding="utf-8") as reader: +with Path("tests/ignores.txt").open(encoding="utf-8") as reader: for line in reader: file_path = line.strip() - if file_path and file_path[0] != '#': + if file_path and file_path[0] != "#": collect_ignore.append(file_path) if not H2_ENABLED: collect_ignore.extend( ( - 'scrapy/core/downloader/handlers/http2.py', + "scrapy/core/downloader/handlers/http2.py", *_py_files("scrapy/core/http2"), ) ) @@ -50,7 +57,7 @@ def pytest_addoption(parser): ) -@pytest.fixture(scope='class') +@pytest.fixture(scope="class") def reactor_pytest(request): if not request.cls: # doctests @@ -61,14 +68,31 @@ def reactor_pytest(request): @pytest.fixture(autouse=True) def only_asyncio(request, reactor_pytest): - if request.node.get_closest_marker('only_asyncio') and reactor_pytest != 'asyncio': - pytest.skip('This test is only run with --reactor=asyncio') + if request.node.get_closest_marker("only_asyncio") and reactor_pytest != "asyncio": + pytest.skip("This test is only run with --reactor=asyncio") @pytest.fixture(autouse=True) def only_not_asyncio(request, reactor_pytest): - if request.node.get_closest_marker('only_not_asyncio') and reactor_pytest == 'asyncio': - pytest.skip('This test is only run without --reactor=asyncio') + if ( + request.node.get_closest_marker("only_not_asyncio") + and reactor_pytest == "asyncio" + ): + pytest.skip("This test is only run without --reactor=asyncio") + + +@pytest.fixture(autouse=True) +def requires_uvloop(request): + if not request.node.get_closest_marker("requires_uvloop"): + return + if sys.implementation.name == "pypy": + pytest.skip("uvloop does not support pypy properly") + if platform.system() == "Windows": + pytest.skip("uvloop does not support Windows") + if twisted_version == Version("twisted", 21, 2, 0): + pytest.skip("https://twistedmatrix.com/trac/ticket/10106") + if sys.version_info >= (3, 12): + pytest.skip("uvloop doesn't support Python 3.12 yet") def pytest_configure(config): diff --git a/docs/Makefile b/docs/Makefile index 596cb6cef..48401bac8 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -87,7 +87,7 @@ coverage: build htmlview: html $(PYTHON) -c "import webbrowser; from pathlib import Path; \ - webbrowser.open('file://' + Path('build/html/index.html').resolve())" + webbrowser.open(Path('build/html/index.html').resolve().as_uri())" clean: -rm -rf build/* diff --git a/docs/_ext/scrapydocs.py b/docs/_ext/scrapydocs.py index 337604cf1..c23a89089 100644 --- a/docs/_ext/scrapydocs.py +++ b/docs/_ext/scrapydocs.py @@ -1,7 +1,8 @@ from operator import itemgetter -from docutils.parsers.rst.roles import set_classes + from docutils import nodes from docutils.parsers.rst import Directive +from docutils.parsers.rst.roles import set_classes from sphinx.util.nodes import make_refnode @@ -11,15 +12,15 @@ class settingslist_node(nodes.General, nodes.Element): class SettingsListDirective(Directive): def run(self): - return [settingslist_node('')] + return [settingslist_node("")] def is_setting_index(node): - if node.tagname == 'index' and node['entries']: + if node.tagname == "index" and node["entries"]: # index entries for setting directives look like: # [('pair', 'SETTING_NAME; setting', 'std:setting-SETTING_NAME', '')] - entry_type, info, refid = node['entries'][0][:3] - return entry_type == 'pair' and info.endswith('; setting') + entry_type, info, refid = node["entries"][0][:3] + return entry_type == "pair" and info.endswith("; setting") return False @@ -30,14 +31,14 @@ def get_setting_target(node): def get_setting_name_and_refid(node): """Extract setting name from directive index node""" - entry_type, info, refid = node['entries'][0][:3] - return info.replace('; setting', ''), refid + entry_type, info, refid = node["entries"][0][:3] + return info.replace("; setting", ""), refid def collect_scrapy_settings_refs(app, doctree): env = app.builder.env - if not hasattr(env, 'scrapy_all_settings'): + if not hasattr(env, "scrapy_all_settings"): env.scrapy_all_settings = [] for node in doctree.traverse(is_setting_index): @@ -46,18 +47,23 @@ def collect_scrapy_settings_refs(app, doctree): setting_name, refid = get_setting_name_and_refid(node) - env.scrapy_all_settings.append({ - 'docname': env.docname, - 'setting_name': setting_name, - 'refid': refid, - }) + env.scrapy_all_settings.append( + { + "docname": env.docname, + "setting_name": setting_name, + "refid": refid, + } + ) def make_setting_element(setting_data, app, fromdocname): - refnode = make_refnode(app.builder, fromdocname, - todocname=setting_data['docname'], - targetid=setting_data['refid'], - child=nodes.Text(setting_data['setting_name'])) + refnode = make_refnode( + app.builder, + fromdocname, + todocname=setting_data["docname"], + targetid=setting_data["refid"], + child=nodes.Text(setting_data["setting_name"]), + ) p = nodes.paragraph() p += refnode @@ -71,10 +77,13 @@ def replace_settingslist_nodes(app, doctree, fromdocname): for node in doctree.traverse(settingslist_node): settings_list = nodes.bullet_list() - settings_list.extend([make_setting_element(d, app, fromdocname) - for d in sorted(env.scrapy_all_settings, - key=itemgetter('setting_name')) - if fromdocname != d['docname']]) + settings_list.extend( + [ + make_setting_element(d, app, fromdocname) + for d in sorted(env.scrapy_all_settings, key=itemgetter("setting_name")) + if fromdocname != d["docname"] + ] + ) node.replace_self(settings_list) @@ -99,41 +108,41 @@ def setup(app): rolename="reqmeta", indextemplate="pair: %s; reqmeta", ) - app.add_role('source', source_role) - app.add_role('commit', commit_role) - app.add_role('issue', issue_role) - app.add_role('rev', rev_role) + app.add_role("source", source_role) + app.add_role("commit", commit_role) + app.add_role("issue", issue_role) + app.add_role("rev", rev_role) app.add_node(settingslist_node) - app.add_directive('settingslist', SettingsListDirective) + app.add_directive("settingslist", SettingsListDirective) - app.connect('doctree-read', collect_scrapy_settings_refs) - app.connect('doctree-resolved', replace_settingslist_nodes) + app.connect("doctree-read", collect_scrapy_settings_refs) + app.connect("doctree-resolved", replace_settingslist_nodes) def source_role(name, rawtext, text, lineno, inliner, options={}, content=[]): - ref = 'https://github.com/scrapy/scrapy/blob/master/' + text + ref = "https://github.com/scrapy/scrapy/blob/master/" + text set_classes(options) node = nodes.reference(rawtext, text, refuri=ref, **options) return [node], [] def issue_role(name, rawtext, text, lineno, inliner, options={}, content=[]): - ref = 'https://github.com/scrapy/scrapy/issues/' + text + ref = "https://github.com/scrapy/scrapy/issues/" + text set_classes(options) - node = nodes.reference(rawtext, 'issue ' + text, refuri=ref, **options) + node = nodes.reference(rawtext, "issue " + text, refuri=ref, **options) return [node], [] def commit_role(name, rawtext, text, lineno, inliner, options={}, content=[]): - ref = 'https://github.com/scrapy/scrapy/commit/' + text + ref = "https://github.com/scrapy/scrapy/commit/" + text set_classes(options) - node = nodes.reference(rawtext, 'commit ' + text, refuri=ref, **options) + node = nodes.reference(rawtext, "commit " + text, refuri=ref, **options) return [node], [] def rev_role(name, rawtext, text, lineno, inliner, options={}, content=[]): - ref = 'http://hg.scrapy.org/scrapy/changeset/' + text + ref = "http://hg.scrapy.org/scrapy/changeset/" + text set_classes(options) - node = nodes.reference(rawtext, 'r' + text, refuri=ref, **options) + node = nodes.reference(rawtext, "r" + text, refuri=ref, **options) return [node], [] diff --git a/docs/_templates/layout.html b/docs/_templates/layout.html deleted file mode 100644 index 18a5231ee..000000000 --- a/docs/_templates/layout.html +++ /dev/null @@ -1,11 +0,0 @@ -{% extends "!layout.html" %} - -{% block footer %} -{{ super() }} - -{% endblock %} diff --git a/docs/_tests/quotes.html b/docs/_tests/quotes.html index 71aff8847..f4002ecd1 100644 --- a/docs/_tests/quotes.html +++ b/docs/_tests/quotes.html @@ -273,7 +273,7 @@ Quotes by: GoodReads.com
- Made with ❤ by Scrapinghub + Made with ❤ by Zyte
diff --git a/docs/_tests/quotes1.html b/docs/_tests/quotes1.html index 71aff8847..f4002ecd1 100644 --- a/docs/_tests/quotes1.html +++ b/docs/_tests/quotes1.html @@ -273,7 +273,7 @@ Quotes by: GoodReads.com- Made with ❤ by Scrapinghub + Made with ❤ by Zyte
diff --git a/docs/conf.py b/docs/conf.py index d2a77003e..9ca0f817a 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -25,30 +25,30 @@ sys.path.insert(0, str(Path(__file__).parent.parent)) # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = [ - 'hoverxref.extension', - 'notfound.extension', - 'scrapydocs', - 'sphinx.ext.autodoc', - 'sphinx.ext.coverage', - 'sphinx.ext.intersphinx', - 'sphinx.ext.viewcode', + "hoverxref.extension", + "notfound.extension", + "scrapydocs", + "sphinx.ext.autodoc", + "sphinx.ext.coverage", + "sphinx.ext.intersphinx", + "sphinx.ext.viewcode", ] # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The suffix of source filenames. -source_suffix = '.rst' +source_suffix = ".rst" # The encoding of source files. -#source_encoding = 'utf-8' +# source_encoding = 'utf-8' # The master toctree document. -master_doc = 'index' +master_doc = "index" # General information about the project. -project = 'Scrapy' -copyright = f'2008–{datetime.now().year}, Scrapy developers' +project = "Scrapy" +copyright = f"2008–{datetime.now().year}, Scrapy developers" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -57,50 +57,51 @@ copyright = f'2008–{datetime.now().year}, Scrapy developers' # The short X.Y version. try: import scrapy - version = '.'.join(map(str, scrapy.version_info[:2])) + + version = ".".join(map(str, scrapy.version_info[:2])) release = scrapy.__version__ except ImportError: - version = '' - release = '' + version = "" + release = "" # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. -language = 'en' +language = "en" # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: -#today = '' +# today = '' # Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' +# today_fmt = '%B %d, %Y' # List of documents that shouldn't be included in the build. -#unused_docs = [] +# unused_docs = [] -exclude_patterns = ['build'] +exclude_patterns = ["build"] # List of directories, relative to source directory, that shouldn't be searched # for source files. -exclude_trees = ['.build'] +exclude_trees = [".build"] # The reST default role (used for this markup: `text`) to use for all documents. -#default_role = None +# default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True +# add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). -#add_module_names = True +# add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. -#show_authors = False +# show_authors = False # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" # List of Sphinx warnings that will not be raised -suppress_warnings = ['epub.unknown_project_files'] +suppress_warnings = ["epub.unknown_project_files"] # Options for HTML output @@ -108,17 +109,18 @@ suppress_warnings = ['epub.unknown_project_files'] # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'sphinx_rtd_theme' +html_theme = "sphinx_rtd_theme" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. -#html_theme_options = {} +# html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. # Add path to the RTD explicitly to robustify builds (otherwise might # fail in a clean Debian build env) import sphinx_rtd_theme + html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] # The style sheet to use for HTML and HTML Help pages. A file of that name @@ -128,44 +130,44 @@ html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] # The name for this set of Sphinx documents. If None, it defaults to # "`` elements inside ``
`` elements from the document, not only those inside ``
from the whole document -... print(p.get()) +.. code-block:: pycon + + >>> for p in divs.xpath("//p"): # this is wrong - gets all
from the whole document + ... print(p.get()) + ... This is the proper way to do it (note the dot prefixing the ``.//p`` XPath): ->>> for p in divs.xpath('.//p'): # extracts all
inside -... print(p.get()) +.. code-block:: pycon + + >>> for p in divs.xpath(".//p"): # extracts all
inside + ... print(p.get()) + ... Another common case would be to extract all direct ``
`` children: ->>> for p in divs.xpath('p'): -... print(p.get()) +.. code-block:: pycon + + >>> for p in divs.xpath("p"): + ... print(p.get()) + ... For more details about relative XPaths see the `Location Paths`_ section in the XPath specification. @@ -522,10 +609,14 @@ class name that shares the string ``someclass``. As it turns out, Scrapy selectors allow you to chain selectors, so most of the time you can just select by class using CSS and then switch to XPath when needed: ->>> from scrapy import Selector ->>> sel = Selector(text='
') ->>> sel.css('.shout').xpath('./time/@datetime').getall() -['2014-07-23 19:00'] +.. code-block:: pycon + + >>> from scrapy import Selector + >>> sel = Selector( + ... text='' + ... ) + >>> sel.css(".shout").xpath("./time/@datetime").getall() + ['2014-07-23 19:00'] This is cleaner than using the verbose XPath trick shown above. Just remember to use the ``.`` in the XPath expressions that will follow. @@ -539,39 +630,51 @@ Beware of the difference between //node[1] and (//node)[1] Example: ->>> from scrapy import Selector ->>> sel = Selector(text=""" -....:Second
- -Fourth
- -.. highlight:: python + >>> from scrapy.http import HtmlResponse + >>> response = HtmlResponse( + ... url="http://example.com", + ... body=""" + ... + ... + ... + ...Second
+ ... + ...Fourth
+ ... + ... + ... """, + ... encoding="utf-8", + ... ) You can use it like this: ->>> response.xpath('//p[has-class("foo")]') -[`` tags and print their class attribute:: +3. Iterate over all ``
`` tags and print their class attribute:
+
+
+ .. code-block:: python
for node in sel.xpath("//p"):
- print(node.attrib['class'])
+ print(node.attrib["class"])
.. _selector-examples-xml:
@@ -1007,20 +1157,30 @@ a :class:`~scrapy.http.HtmlResponse` object like this::
Selector examples on XML response
---------------------------------
+.. skip: start
+
Here are some examples to illustrate concepts for :class:`Selector` objects
-instantiated with an :class:`~scrapy.http.XmlResponse` object::
+instantiated with an :class:`~scrapy.http.XmlResponse` object:
+
+.. code-block:: python
sel = Selector(xml_response)
1. Select all ``
You are being redirected
' - resp = Response(req.url, status=302, body=body, headers={ - 'Content-Length': str(len(body)), - 'Content-Type': 'text/html', - 'Content-Encoding': 'gzip', - 'Location': 'http://example.com/login', - }) + req = Request("http://example.com") + body = b"You are being redirected
" + resp = Response( + req.url, + status=302, + body=body, + headers={ + "Content-Length": str(len(body)), + "Content-Type": "text/html", + "Content-Encoding": "gzip", + "Location": "http://example.com/login", + }, + ) ret = self._download(request=req, response=resp) - self.assertTrue(isinstance(ret, Request), - f"Not redirected: {ret!r}") - self.assertEqual(to_bytes(ret.url), resp.headers['Location'], - "Not redirected to location header") + self.assertTrue(isinstance(ret, Request), f"Not redirected: {ret!r}") + self.assertEqual( + to_bytes(ret.url), + resp.headers["Location"], + "Not redirected to location header", + ) def test_200_and_invalid_gzipped_body_must_fail(self): - req = Request('http://example.com') - body = b'You are being redirected
' - resp = Response(req.url, status=200, body=body, headers={ - 'Content-Length': str(len(body)), - 'Content-Type': 'text/html', - 'Content-Encoding': 'gzip', - 'Location': 'http://example.com/login', - }) - self.assertRaises(IOError, self._download, request=req, response=resp) + req = Request("http://example.com") + body = b"You are being redirected
" + resp = Response( + req.url, + status=200, + body=body, + headers={ + "Content-Length": str(len(body)), + "Content-Type": "text/html", + "Content-Encoding": "gzip", + "Location": "http://example.com/login", + }, + ) + self.assertRaises(OSError, self._download, request=req, response=resp) class ResponseFromProcessRequestTest(ManagerTestCase): """Tests middleware returning a response from process_request.""" def test_download_func_not_called(self): - resp = Response('http://example.com/index.html') + resp = Response("http://example.com/index.html") class ResponseMiddleware: def process_request(self, request, spider): @@ -112,7 +123,7 @@ class ResponseFromProcessRequestTest(ManagerTestCase): self.mwman._add_middleware(ResponseMiddleware()) - req = Request('http://example.com/index.html') + req = Request("http://example.com/index.html") download_func = mock.MagicMock() dfd = self.mwman.download(download_func, req, self.spider) results = [] @@ -127,7 +138,7 @@ class ProcessRequestInvalidOutput(ManagerTestCase): """Invalid return value for process_request method should raise an exception""" def test_invalid_process_request(self): - req = Request('http://example.com/index.html') + req = Request("http://example.com/index.html") class InvalidProcessRequestMiddleware: def process_request(self, request, spider): @@ -146,7 +157,7 @@ class ProcessResponseInvalidOutput(ManagerTestCase): """Invalid return value for process_response method should raise an exception""" def test_invalid_process_response(self): - req = Request('http://example.com/index.html') + req = Request("http://example.com/index.html") class InvalidProcessResponseMiddleware: def process_response(self, request, response, spider): @@ -165,7 +176,7 @@ class ProcessExceptionInvalidOutput(ManagerTestCase): """Invalid return value for process_exception method should raise an exception""" def test_invalid_process_exception(self): - req = Request('http://example.com/index.html') + req = Request("http://example.com/index.html") class InvalidProcessExceptionMiddleware: def process_request(self, request, spider): @@ -187,7 +198,7 @@ class MiddlewareUsingDeferreds(ManagerTestCase): """Middlewares using Deferreds should work""" def test_deferred(self): - resp = Response('http://example.com/index.html') + resp = Response("http://example.com/index.html") class DeferredMiddleware: def cb(self, result): @@ -200,7 +211,7 @@ class MiddlewareUsingDeferreds(ManagerTestCase): return d self.mwman._add_middleware(DeferredMiddleware()) - req = Request('http://example.com/index.html') + req = Request("http://example.com/index.html") download_func = mock.MagicMock() dfd = self.mwman.download(download_func, req, self.spider) results = [] @@ -211,12 +222,12 @@ class MiddlewareUsingDeferreds(ManagerTestCase): self.assertFalse(download_func.called) -@mark.usefixtures('reactor_pytest') +@mark.usefixtures("reactor_pytest") class MiddlewareUsingCoro(ManagerTestCase): """Middlewares using asyncio coroutines should work""" def test_asyncdef(self): - resp = Response('http://example.com/index.html') + resp = Response("http://example.com/index.html") class CoroMiddleware: async def process_request(self, request, spider): @@ -224,7 +235,7 @@ class MiddlewareUsingCoro(ManagerTestCase): return resp self.mwman._add_middleware(CoroMiddleware()) - req = Request('http://example.com/index.html') + req = Request("http://example.com/index.html") download_func = mock.MagicMock() dfd = self.mwman.download(download_func, req, self.spider) results = [] @@ -236,7 +247,7 @@ class MiddlewareUsingCoro(ManagerTestCase): @mark.only_asyncio() def test_asyncdef_asyncio(self): - resp = Response('http://example.com/index.html') + resp = Response("http://example.com/index.html") class CoroMiddleware: async def process_request(self, request, spider): @@ -245,7 +256,7 @@ class MiddlewareUsingCoro(ManagerTestCase): return result self.mwman._add_middleware(CoroMiddleware()) - req = Request('http://example.com/index.html') + req = Request("http://example.com/index.html") download_func = mock.MagicMock() dfd = self.mwman.download(download_func, req, self.spider) results = [] diff --git a/tests/test_downloadermiddleware_ajaxcrawlable.py b/tests/test_downloadermiddleware_ajaxcrawlable.py index 5a56c9db2..043dc0a12 100644 --- a/tests/test_downloadermiddleware_ajaxcrawlable.py +++ b/tests/test_downloadermiddleware_ajaxcrawlable.py @@ -1,18 +1,17 @@ import unittest from scrapy.downloadermiddlewares.ajaxcrawl import AjaxCrawlMiddleware +from scrapy.http import HtmlResponse, Request, Response from scrapy.spiders import Spider -from scrapy.http import Request, HtmlResponse, Response from scrapy.utils.test import get_crawler - -__doctests__ = ['scrapy.downloadermiddlewares.ajaxcrawl'] +__doctests__ = ["scrapy.downloadermiddlewares.ajaxcrawl"] class AjaxCrawlMiddlewareTest(unittest.TestCase): def setUp(self): - crawler = get_crawler(Spider, {'AJAXCRAWL_ENABLED': True}) - self.spider = crawler._create_spider('foo') + crawler = get_crawler(Spider, {"AJAXCRAWL_ENABLED": True}) + self.spider = crawler._create_spider("foo") self.mw = AjaxCrawlMiddleware.from_crawler(crawler) def _ajaxcrawlable_body(self): @@ -24,37 +23,41 @@ class AjaxCrawlMiddlewareTest(unittest.TestCase): return req, resp def test_non_get(self): - req, resp = self._req_resp('http://example.com/', {'method': 'HEAD'}) + req, resp = self._req_resp("http://example.com/", {"method": "HEAD"}) resp2 = self.mw.process_response(req, resp, self.spider) self.assertEqual(resp, resp2) def test_binary_response(self): - req = Request('http://example.com/') - resp = Response('http://example.com/', body=b'foobar\x00\x01\x02', request=req) + req = Request("http://example.com/") + resp = Response("http://example.com/", body=b"foobar\x00\x01\x02", request=req) resp2 = self.mw.process_response(req, resp, self.spider) self.assertIs(resp, resp2) def test_ajaxcrawl(self): req, resp = self._req_resp( - 'http://example.com/', - {'meta': {'foo': 'bar'}}, - {'body': self._ajaxcrawlable_body()} + "http://example.com/", + {"meta": {"foo": "bar"}}, + {"body": self._ajaxcrawlable_body()}, ) req2 = self.mw.process_response(req, resp, self.spider) - self.assertEqual(req2.url, 'http://example.com/?_escaped_fragment_=') - self.assertEqual(req2.meta['foo'], 'bar') + self.assertEqual(req2.url, "http://example.com/?_escaped_fragment_=") + self.assertEqual(req2.meta["foo"], "bar") def test_ajaxcrawl_loop(self): - req, resp = self._req_resp('http://example.com/', {}, {'body': self._ajaxcrawlable_body()}) + req, resp = self._req_resp( + "http://example.com/", {}, {"body": self._ajaxcrawlable_body()} + ) req2 = self.mw.process_response(req, resp, self.spider) resp2 = HtmlResponse(req2.url, body=resp.body, request=req2) resp3 = self.mw.process_response(req2, resp2, self.spider) assert isinstance(resp3, HtmlResponse), (resp3.__class__, resp3) - self.assertEqual(resp3.request.url, 'http://example.com/?_escaped_fragment_=') + self.assertEqual(resp3.request.url, "http://example.com/?_escaped_fragment_=") assert resp3 is resp2 def test_noncrawlable_body(self): - req, resp = self._req_resp('http://example.com/', {}, {'body': b''}) + req, resp = self._req_resp( + "http://example.com/", {}, {"body": b""} + ) resp2 = self.mw.process_response(req, resp, self.spider) self.assertIs(resp, resp2) diff --git a/tests/test_downloadermiddleware_cookies.py b/tests/test_downloadermiddleware_cookies.py index dd835b9c9..4a81a638e 100644 --- a/tests/test_downloadermiddleware_cookies.py +++ b/tests/test_downloadermiddleware_cookies.py @@ -1,14 +1,14 @@ import logging from unittest import TestCase -from testfixtures import LogCapture import pytest +from testfixtures import LogCapture from scrapy.downloadermiddlewares.cookies import CookiesMiddleware from scrapy.downloadermiddlewares.defaultheaders import DefaultHeadersMiddleware from scrapy.downloadermiddlewares.redirect import RedirectMiddleware from scrapy.exceptions import NotConfigured -from scrapy.http import Response, Request +from scrapy.http import Request, Response from scrapy.settings import Settings from scrapy.spiders import Spider from scrapy.utils.python import to_bytes @@ -48,24 +48,18 @@ def _cookies_to_set_cookie_list(cookies): return [] if isinstance(cookies, dict): cookies = ({"name": k, "value": v} for k, v in cookies.items()) - return filter( - None, - ( - _cookie_to_set_cookie_value(cookie) - for cookie in cookies - ) - ) + return filter(None, (_cookie_to_set_cookie_value(cookie) for cookie in cookies)) class CookiesMiddlewareTest(TestCase): - def assertCookieValEqual(self, first, second, msg=None): def split_cookies(cookies): return sorted([s.strip() for s in to_bytes(cookies).split(b";")]) + return self.assertEqual(split_cookies(first), split_cookies(second), msg=msg) def setUp(self): - self.spider = Spider('foo') + self.spider = Spider("foo") self.mw = CookiesMiddleware() self.redirect_middleware = RedirectMiddleware(settings=Settings()) @@ -74,346 +68,391 @@ class CookiesMiddlewareTest(TestCase): del self.redirect_middleware def test_basic(self): - req = Request('http://scrapytest.org/') + req = Request("http://scrapytest.org/") assert self.mw.process_request(req, self.spider) is None - assert 'Cookie' not in req.headers + assert "Cookie" not in req.headers - headers = {'Set-Cookie': 'C1=value1; path=/'} - res = Response('http://scrapytest.org/', headers=headers) + headers = {"Set-Cookie": "C1=value1; path=/"} + res = Response("http://scrapytest.org/", headers=headers) assert self.mw.process_response(req, res, self.spider) is res - req2 = Request('http://scrapytest.org/sub1/') + req2 = Request("http://scrapytest.org/sub1/") assert self.mw.process_request(req2, self.spider) is None - self.assertEqual(req2.headers.get('Cookie'), b"C1=value1") + self.assertEqual(req2.headers.get("Cookie"), b"C1=value1") def test_setting_false_cookies_enabled(self): self.assertRaises( NotConfigured, CookiesMiddleware.from_crawler, - get_crawler(settings_dict={'COOKIES_ENABLED': False}) + get_crawler(settings_dict={"COOKIES_ENABLED": False}), ) def test_setting_default_cookies_enabled(self): self.assertIsInstance( - CookiesMiddleware.from_crawler(get_crawler()), - CookiesMiddleware + CookiesMiddleware.from_crawler(get_crawler()), CookiesMiddleware ) def test_setting_true_cookies_enabled(self): self.assertIsInstance( CookiesMiddleware.from_crawler( - get_crawler(settings_dict={'COOKIES_ENABLED': True}) + get_crawler(settings_dict={"COOKIES_ENABLED": True}) ), - CookiesMiddleware + CookiesMiddleware, ) def test_setting_enabled_cookies_debug(self): - crawler = get_crawler(settings_dict={'COOKIES_DEBUG': True}) + crawler = get_crawler(settings_dict={"COOKIES_DEBUG": True}) mw = CookiesMiddleware.from_crawler(crawler) with LogCapture( - 'scrapy.downloadermiddlewares.cookies', + "scrapy.downloadermiddlewares.cookies", propagate=False, level=logging.DEBUG, ) as log: - req = Request('http://scrapytest.org/') - res = Response('http://scrapytest.org/', headers={'Set-Cookie': 'C1=value1; path=/'}) + req = Request("http://scrapytest.org/") + res = Response( + "http://scrapytest.org/", headers={"Set-Cookie": "C1=value1; path=/"} + ) mw.process_response(req, res, crawler.spider) - req2 = Request('http://scrapytest.org/sub1/') + req2 = Request("http://scrapytest.org/sub1/") mw.process_request(req2, crawler.spider) log.check( - ('scrapy.downloadermiddlewares.cookies', - 'DEBUG', - 'Received cookies from: <200 http://scrapytest.org/>\n' - 'Set-Cookie: C1=value1; path=/\n'), - ('scrapy.downloadermiddlewares.cookies', - 'DEBUG', - 'Sending cookies to:File not found.
\n" b" \n" - b"\n" + b"\n", ) elif run.getpath(request.url) == "/numbers": # signal was fired multiple times @@ -389,12 +407,16 @@ class EngineTest(unittest.TestCase): assert signals.spider_closed in run.signals_caught assert signals.headers_received in run.signals_caught - self.assertEqual({'spider': run.spider}, - run.signals_caught[signals.spider_opened]) - self.assertEqual({'spider': run.spider}, - run.signals_caught[signals.spider_idle]) - self.assertEqual({'spider': run.spider, 'reason': 'finished'}, - run.signals_caught[signals.spider_closed]) + self.assertEqual( + {"spider": run.spider}, run.signals_caught[signals.spider_opened] + ) + self.assertEqual( + {"spider": run.spider}, run.signals_caught[signals.spider_idle] + ) + self.assertEqual( + {"spider": run.spider, "reason": "finished"}, + run.signals_caught[signals.spider_closed], + ) @defer.inlineCallbacks def test_close_downloader(self): @@ -413,90 +435,17 @@ class EngineTest(unittest.TestCase): finally: yield e.stop() - @defer.inlineCallbacks - def test_close_spiders_downloader(self): - with pytest.warns(ScrapyDeprecationWarning, - match="ExecutionEngine.open_spiders is deprecated, " - "please use ExecutionEngine.spider instead"): - e = ExecutionEngine(get_crawler(TestSpider), lambda _: None) - yield e.open_spider(TestSpider(), []) - self.assertEqual(len(e.open_spiders), 1) - yield e.close() - self.assertEqual(len(e.open_spiders), 0) - - @defer.inlineCallbacks - def test_close_engine_spiders_downloader(self): - with pytest.warns(ScrapyDeprecationWarning, - match="ExecutionEngine.open_spiders is deprecated, " - "please use ExecutionEngine.spider instead"): - e = ExecutionEngine(get_crawler(TestSpider), lambda _: None) - yield e.open_spider(TestSpider(), []) - e.start() - self.assertTrue(e.running) - yield e.close() - self.assertFalse(e.running) - self.assertEqual(len(e.open_spiders), 0) - - @defer.inlineCallbacks - def test_crawl_deprecated_spider_arg(self): - with pytest.warns(ScrapyDeprecationWarning, - match="Passing a 'spider' argument to " - "ExecutionEngine.crawl is deprecated"): - e = ExecutionEngine(get_crawler(TestSpider), lambda _: None) - spider = TestSpider() - yield e.open_spider(spider, []) - e.start() - e.crawl(Request("data:,"), spider) - yield e.close() - - @defer.inlineCallbacks - def test_download_deprecated_spider_arg(self): - with pytest.warns(ScrapyDeprecationWarning, - match="Passing a 'spider' argument to " - "ExecutionEngine.download is deprecated"): - e = ExecutionEngine(get_crawler(TestSpider), lambda _: None) - spider = TestSpider() - yield e.open_spider(spider, []) - e.start() - e.download(Request("data:,"), spider) - yield e.close() - - @defer.inlineCallbacks - def test_deprecated_schedule(self): - with pytest.warns(ScrapyDeprecationWarning, - match="ExecutionEngine.schedule is deprecated, please use " - "ExecutionEngine.crawl or ExecutionEngine.download instead"): - e = ExecutionEngine(get_crawler(TestSpider), lambda _: None) - spider = TestSpider() - yield e.open_spider(spider, []) - e.start() - e.schedule(Request("data:,"), spider) - yield e.close() - - @defer.inlineCallbacks - def test_deprecated_has_capacity(self): - with pytest.warns(ScrapyDeprecationWarning, - match="ExecutionEngine.has_capacity is deprecated"): - e = ExecutionEngine(get_crawler(TestSpider), lambda _: None) - self.assertTrue(e.has_capacity()) - spider = TestSpider() - yield e.open_spider(spider, []) - self.assertFalse(e.has_capacity()) - e.start() - yield e.close() - self.assertTrue(e.has_capacity()) - def test_short_timeout(self): args = ( sys.executable, - '-m', - 'scrapy.cmdline', - 'fetch', - '-s', - 'CLOSESPIDER_TIMEOUT=0.001', - '-s', - 'LOG_LEVEL=DEBUG', - 'http://toscrape.com', + "-m", + "scrapy.cmdline", + "fetch", + "-s", + "CLOSESPIDER_TIMEOUT=0.001", + "-s", + "LOG_LEVEL=DEBUG", + "http://toscrape.com", ) p = subprocess.Popen( args, @@ -506,7 +455,7 @@ class EngineTest(unittest.TestCase): def kill_proc(): p.kill() p.communicate() - assert False, 'Command took too much time to complete' + assert False, "Command took too much time to complete" timer = Timer(15, kill_proc) try: @@ -515,10 +464,10 @@ class EngineTest(unittest.TestCase): finally: timer.cancel() - self.assertNotIn(b'Traceback', stderr) + self.assertNotIn(b"Traceback", stderr) if __name__ == "__main__": - if len(sys.argv) > 1 and sys.argv[1] == 'runserver': + if len(sys.argv) > 1 and sys.argv[1] == "runserver": start_test_site(debug=True) reactor.run() diff --git a/tests/test_engine_stop_download_bytes.py b/tests/test_engine_stop_download_bytes.py index 933e4067d..8dbb5b7ea 100644 --- a/tests/test_engine_stop_download_bytes.py +++ b/tests/test_engine_stop_download_bytes.py @@ -2,14 +2,13 @@ from testfixtures import LogCapture from twisted.internet import defer from scrapy.exceptions import StopDownload - from tests.test_engine import ( AttrsItemsSpider, + CrawlerRun, DataClassItemsSpider, DictItemsSpider, - TestSpider, - CrawlerRun, EngineTest, + TestSpider, ) @@ -22,22 +21,39 @@ class BytesReceivedCrawlerRun(CrawlerRun): class BytesReceivedEngineTest(EngineTest): @defer.inlineCallbacks def test_crawler(self): - for spider in (TestSpider, DictItemsSpider, AttrsItemsSpider, DataClassItemsSpider): + for spider in ( + TestSpider, + DictItemsSpider, + AttrsItemsSpider, + DataClassItemsSpider, + ): run = BytesReceivedCrawlerRun(spider) with LogCapture() as log: yield run.run() - log.check_present(("scrapy.core.downloader.handlers.http11", - "DEBUG", - f"Download stopped for{val}
-''' - return bytes(response, 'utf-8') +""" + return bytes(response, "utf-8") class DummySpider(Spider): - name = 'dummy' + name = "dummy" start_urls: list = [] def parse(self, response): @@ -51,50 +55,51 @@ class DummySpider(Spider): class Data: SMALL_SIZE = 1024 # 1 KB - LARGE_SIZE = 1024 ** 2 # 1 MB + LARGE_SIZE = 1024**2 # 1 MB STR_SMALL = generate_random_string(SMALL_SIZE) STR_LARGE = generate_random_string(LARGE_SIZE) EXTRA_SMALL = generate_random_string(1024 * 15) - EXTRA_LARGE = generate_random_string((1024 ** 2) * 15) + EXTRA_LARGE = generate_random_string((1024**2) * 15) HTML_SMALL = make_html_body(STR_SMALL) HTML_LARGE = make_html_body(STR_LARGE) - JSON_SMALL = {'data': STR_SMALL} - JSON_LARGE = {'data': STR_LARGE} + JSON_SMALL = {"data": STR_SMALL} + JSON_LARGE = {"data": STR_LARGE} - DATALOSS = b'Dataloss Content' - NO_CONTENT_LENGTH = b'This response do not have any content-length header' + DATALOSS = b"Dataloss Content" + NO_CONTENT_LENGTH = b"This response do not have any content-length header" class GetDataHtmlSmall(LeafResource): def render_GET(self, request: TxRequest): - request.setHeader('Content-Type', 'text/html; charset=UTF-8') + request.setHeader("Content-Type", "text/html; charset=UTF-8") return Data.HTML_SMALL class GetDataHtmlLarge(LeafResource): def render_GET(self, request: TxRequest): - request.setHeader('Content-Type', 'text/html; charset=UTF-8') + request.setHeader("Content-Type", "text/html; charset=UTF-8") return Data.HTML_LARGE class PostDataJsonMixin: @staticmethod def make_response(request: TxRequest, extra_data: str): + assert request.content is not None response = { - 'request-headers': {}, - 'request-body': json.loads(request.content.read()), - 'extra-data': extra_data + "request-headers": {}, + "request-body": json.loads(request.content.read()), + "extra-data": extra_data, } for k, v in request.requestHeaders.getAllRawHeaders(): - response['request-headers'][str(k, 'utf-8')] = str(v[0], 'utf-8') + response["request-headers"][str(k, "utf-8")] = str(v[0], "utf-8") - response_bytes = bytes(json.dumps(response), 'utf-8') - request.setHeader('Content-Type', 'application/json; charset=UTF-8') - request.setHeader('Content-Encoding', 'UTF-8') + response_bytes = bytes(json.dumps(response), "utf-8") + request.setHeader("Content-Type", "application/json; charset=UTF-8") + request.setHeader("Content-Encoding", "UTF-8") return response_bytes @@ -109,7 +114,6 @@ class PostDataJsonLarge(LeafResource, PostDataJsonMixin): class Dataloss(LeafResource): - def render_GET(self, request: TxRequest): request.setHeader(b"Content-Length", b"1024") self.deferRequest(request, 0, self._delayed_render, request) @@ -123,7 +127,7 @@ class Dataloss(LeafResource): class NoContentLengthHeader(LeafResource): def render_GET(self, request: TxRequest): - request.requestHeaders.removeHeader('Content-Length') + request.requestHeaders.removeHeader("Content-Length") self.deferRequest(request, 0, self._delayed_render, request) return NOT_DONE_YET @@ -140,57 +144,62 @@ class TimeoutResponse(LeafResource): class QueryParams(LeafResource): def render_GET(self, request: TxRequest): - request.setHeader('Content-Type', 'application/json; charset=UTF-8') - request.setHeader('Content-Encoding', 'UTF-8') + request.setHeader("Content-Type", "application/json; charset=UTF-8") + request.setHeader("Content-Encoding", "UTF-8") - query_params = {} + query_params: Dict[str, str] = {} + assert request.args is not None for k, v in request.args.items(): - query_params[str(k, 'utf-8')] = str(v[0], 'utf-8') + query_params[str(k, "utf-8")] = str(v[0], "utf-8") - return bytes(json.dumps(query_params), 'utf-8') + return bytes(json.dumps(query_params), "utf-8") class RequestHeaders(LeafResource): """Sends all the headers received as a response""" def render_GET(self, request: TxRequest): - request.setHeader('Content-Type', 'application/json; charset=UTF-8') - request.setHeader('Content-Encoding', 'UTF-8') + request.setHeader("Content-Type", "application/json; charset=UTF-8") + request.setHeader("Content-Encoding", "UTF-8") headers = {} for k, v in request.requestHeaders.getAllRawHeaders(): - headers[str(k, 'utf-8')] = str(v[0], 'utf-8') + headers[str(k, "utf-8")] = str(v[0], "utf-8") - return bytes(json.dumps(headers), 'utf-8') + return bytes(json.dumps(headers), "utf-8") -def get_client_certificate(key_file: Path, certificate_file: Path) -> PrivateCertificate: - pem = key_file.read_text(encoding="utf-8") + certificate_file.read_text(encoding="utf-8") +def get_client_certificate( + key_file: Path, certificate_file: Path +) -> PrivateCertificate: + pem = key_file.read_text(encoding="utf-8") + certificate_file.read_text( + encoding="utf-8" + ) return PrivateCertificate.loadPEM(pem) @skipIf(not H2_ENABLED, "HTTP/2 support in Twisted is not enabled") class Https2ClientProtocolTestCase(TestCase): - scheme = 'https' - key_file = Path(__file__).parent / 'keys' / 'localhost.key' - certificate_file = Path(__file__).parent / 'keys' / 'localhost.crt' + scheme = "https" + key_file = Path(__file__).parent / "keys" / "localhost.key" + certificate_file = Path(__file__).parent / "keys" / "localhost.crt" def _init_resource(self): self.temp_directory = self.mktemp() Path(self.temp_directory).mkdir() r = File(self.temp_directory) - r.putChild(b'get-data-html-small', GetDataHtmlSmall()) - r.putChild(b'get-data-html-large', GetDataHtmlLarge()) + r.putChild(b"get-data-html-small", GetDataHtmlSmall()) + r.putChild(b"get-data-html-large", GetDataHtmlLarge()) - r.putChild(b'post-data-json-small', PostDataJsonSmall()) - r.putChild(b'post-data-json-large', PostDataJsonLarge()) + r.putChild(b"post-data-json-small", PostDataJsonSmall()) + r.putChild(b"post-data-json-large", PostDataJsonLarge()) - r.putChild(b'dataloss', Dataloss()) - r.putChild(b'no-content-length-header', NoContentLengthHeader()) - r.putChild(b'status', Status()) - r.putChild(b'query-params', QueryParams()) - r.putChild(b'timeout', TimeoutResponse()) - r.putChild(b'request-headers', RequestHeaders()) + r.putChild(b"dataloss", Dataloss()) + r.putChild(b"no-content-length-header", NoContentLengthHeader()) + r.putChild(b"status", Status()) + r.putChild(b"query-params", QueryParams()) + r.putChild(b"timeout", TimeoutResponse()) + r.putChild(b"request-headers", RequestHeaders()) return r @inlineCallbacks @@ -200,26 +209,35 @@ class Https2ClientProtocolTestCase(TestCase): self.site = Site(root, timeout=None) # Start server for testing - self.hostname = 'localhost' - context_factory = ssl_context_factory(str(self.key_file), str(self.certificate_file)) + self.hostname = "localhost" + context_factory = ssl_context_factory( + str(self.key_file), str(self.certificate_file) + ) - server_endpoint = SSL4ServerEndpoint(reactor, 0, context_factory, interface=self.hostname) + server_endpoint = SSL4ServerEndpoint( + reactor, 0, context_factory, interface=self.hostname + ) self.server = yield server_endpoint.listen(self.site) self.port_number = self.server.getHost().port # Connect H2 client with server - self.client_certificate = get_client_certificate(self.key_file, self.certificate_file) + self.client_certificate = get_client_certificate( + self.key_file, self.certificate_file + ) client_options = optionsForClientTLS( hostname=self.hostname, trustRoot=self.client_certificate, - acceptableProtocols=[b'h2'] + acceptableProtocols=[b"h2"], ) - uri = URI.fromBytes(bytes(self.get_url('/'), 'utf-8')) + uri = URI.fromBytes(bytes(self.get_url("/"), "utf-8")) self.conn_closed_deferred = Deferred() from scrapy.core.http2.protocol import H2ClientFactory + h2_client_factory = H2ClientFactory(uri, Settings(), self.conn_closed_deferred) - client_endpoint = SSL4ClientEndpoint(reactor, self.hostname, self.port_number, client_options) + client_endpoint = SSL4ClientEndpoint( + reactor, self.hostname, self.port_number, client_options + ) self.client = yield client_endpoint.connect(h2_client_factory) @inlineCallbacks @@ -236,8 +254,8 @@ class Https2ClientProtocolTestCase(TestCase): :param path: Should have / at the starting compulsorily if not empty :return: Complete url """ - assert len(path) > 0 and (path[0] == '/' or path[0] == '&') - return f'{self.scheme}://{self.hostname}:{self.port_number}{path}' + assert len(path) > 0 and (path[0] == "/" or path[0] == "&") + return f"{self.scheme}://{self.hostname}:{self.port_number}{path}" def make_request(self, request: Request) -> Deferred: return self.client.request(request, DummySpider()) @@ -251,18 +269,15 @@ class Https2ClientProtocolTestCase(TestCase): return DeferredList(d_list, fireOnOneErrback=True) - def _check_GET( - self, - request: Request, - expected_body, - expected_status - ): + def _check_GET(self, request: Request, expected_body, expected_status): def check_response(response: Response): self.assertEqual(response.status, expected_status) self.assertEqual(response.body, expected_body) self.assertEqual(response.request, request) - content_length = int(response.headers.get('Content-Length')) + content_length_header = response.headers.get("Content-Length") + assert content_length_header is not None + content_length = int(content_length_header) self.assertEqual(len(response.body), content_length) d = self.make_request(request) @@ -271,11 +286,11 @@ class Https2ClientProtocolTestCase(TestCase): return d def test_GET_small_body(self): - request = Request(self.get_url('/get-data-html-small')) + request = Request(self.get_url("/get-data-html-small")) return self._check_GET(request, Data.HTML_SMALL, 200) def test_GET_large_body(self): - request = Request(self.get_url('/get-data-html-large')) + request = Request(self.get_url("/get-data-html-large")) return self._check_GET(request, Data.HTML_LARGE, 200) def _check_GET_x10(self, *args, **kwargs): @@ -286,16 +301,12 @@ class Https2ClientProtocolTestCase(TestCase): def test_GET_small_body_x10(self): return self._check_GET_x10( - Request(self.get_url('/get-data-html-small')), - Data.HTML_SMALL, - 200 + Request(self.get_url("/get-data-html-small")), Data.HTML_SMALL, 200 ) def test_GET_large_body_x10(self): return self._check_GET_x10( - Request(self.get_url('/get-data-html-large')), - Data.HTML_LARGE, - 200 + Request(self.get_url("/get-data-html-large")), Data.HTML_LARGE, 200 ) def _check_POST_json( @@ -303,7 +314,7 @@ class Https2ClientProtocolTestCase(TestCase): request: Request, expected_request_body, expected_extra_data, - expected_status: int + expected_status: int, ): d = self.make_request(request) @@ -311,50 +322,52 @@ class Https2ClientProtocolTestCase(TestCase): self.assertEqual(response.status, expected_status) self.assertEqual(response.request, request) - content_length = int(response.headers.get('Content-Length')) + content_length_header = response.headers.get("Content-Length") + assert content_length_header is not None + content_length = int(content_length_header) self.assertEqual(len(response.body), content_length) # Parse the body - content_encoding = str(response.headers[b'Content-Encoding'], 'utf-8') + content_encoding_header = response.headers[b"Content-Encoding"] + assert content_encoding_header is not None + content_encoding = str(content_encoding_header, "utf-8") body = json.loads(str(response.body, content_encoding)) - self.assertIn('request-body', body) - self.assertIn('extra-data', body) - self.assertIn('request-headers', body) + self.assertIn("request-body", body) + self.assertIn("extra-data", body) + self.assertIn("request-headers", body) - request_body = body['request-body'] + request_body = body["request-body"] self.assertEqual(request_body, expected_request_body) - extra_data = body['extra-data'] + extra_data = body["extra-data"] self.assertEqual(extra_data, expected_extra_data) # Check if headers were sent successfully - request_headers = body['request-headers'] + request_headers = body["request-headers"] for k, v in request.headers.items(): - k_str = str(k, 'utf-8') + k_str = str(k, "utf-8") self.assertIn(k_str, request_headers) - self.assertEqual(request_headers[k_str], str(v[0], 'utf-8')) + self.assertEqual(request_headers[k_str], str(v[0], "utf-8")) d.addCallback(assert_response) d.addErrback(self.fail) return d def test_POST_small_json(self): - request = JsonRequest(url=self.get_url('/post-data-json-small'), method='POST', data=Data.JSON_SMALL) - return self._check_POST_json( - request, - Data.JSON_SMALL, - Data.EXTRA_SMALL, - 200 + request = JsonRequest( + url=self.get_url("/post-data-json-small"), + method="POST", + data=Data.JSON_SMALL, ) + return self._check_POST_json(request, Data.JSON_SMALL, Data.EXTRA_SMALL, 200) def test_POST_large_json(self): - request = JsonRequest(url=self.get_url('/post-data-json-large'), method='POST', data=Data.JSON_LARGE) - return self._check_POST_json( - request, - Data.JSON_LARGE, - Data.EXTRA_LARGE, - 200 + request = JsonRequest( + url=self.get_url("/post-data-json-large"), + method="POST", + data=Data.JSON_LARGE, ) + return self._check_POST_json(request, Data.JSON_LARGE, Data.EXTRA_LARGE, 200) def _check_POST_json_x10(self, *args, **kwargs): def get_deferred(): @@ -363,32 +376,36 @@ class Https2ClientProtocolTestCase(TestCase): return self._check_repeat(get_deferred, 10) def test_POST_small_json_x10(self): - request = JsonRequest(url=self.get_url('/post-data-json-small'), method='POST', data=Data.JSON_SMALL) + request = JsonRequest( + url=self.get_url("/post-data-json-small"), + method="POST", + data=Data.JSON_SMALL, + ) return self._check_POST_json_x10( - request, - Data.JSON_SMALL, - Data.EXTRA_SMALL, - 200 + request, Data.JSON_SMALL, Data.EXTRA_SMALL, 200 ) def test_POST_large_json_x10(self): - request = JsonRequest(url=self.get_url('/post-data-json-large'), method='POST', data=Data.JSON_LARGE) + request = JsonRequest( + url=self.get_url("/post-data-json-large"), + method="POST", + data=Data.JSON_LARGE, + ) return self._check_POST_json_x10( - request, - Data.JSON_LARGE, - Data.EXTRA_LARGE, - 200 + request, Data.JSON_LARGE, Data.EXTRA_LARGE, 200 ) @inlineCallbacks def test_invalid_negotiated_protocol(self): - with mock.patch("scrapy.core.http2.protocol.PROTOCOL_NAME", return_value=b"not-h2"): - request = Request(url=self.get_url('/status?n=200')) + with mock.patch( + "scrapy.core.http2.protocol.PROTOCOL_NAME", return_value=b"not-h2" + ): + request = Request(url=self.get_url("/status?n=200")) with self.assertRaises(ResponseFailed): yield self.make_request(request) def test_cancel_request(self): - request = Request(url=self.get_url('/get-data-html-large')) + request = Request(url=self.get_url("/get-data-html-large")) def assert_response(response: Response): self.assertEqual(response.status, 499) @@ -402,13 +419,15 @@ class Https2ClientProtocolTestCase(TestCase): return d def test_download_maxsize_exceeded(self): - request = Request(url=self.get_url('/get-data-html-large'), meta={'download_maxsize': 1000}) + request = Request( + url=self.get_url("/get-data-html-large"), meta={"download_maxsize": 1000} + ) def assert_cancelled_error(failure): self.assertIsInstance(failure.value, CancelledError) error_pattern = re.compile( - rf'Cancelling download of {request.url}: received response ' - rf'size \(\d*\) larger than download max size \(1000\)' + rf"Cancelling download of {request.url}: received response " + rf"size \(\d*\) larger than download max size \(1000\)" ) self.assertEqual(len(re.findall(error_pattern, str(failure.value))), 1) @@ -420,15 +439,18 @@ class Https2ClientProtocolTestCase(TestCase): def test_received_dataloss_response(self): """In case when value of Header Content-Length != len(Received Data) ProtocolError is raised""" - request = Request(url=self.get_url('/dataloss')) + request = Request(url=self.get_url("/dataloss")) def assert_failure(failure: Failure): self.assertTrue(len(failure.value.reasons) > 0) from h2.exceptions import InvalidBodyLengthError - self.assertTrue(any( - isinstance(error, InvalidBodyLengthError) - for error in failure.value.reasons - )) + + self.assertTrue( + any( + isinstance(error, InvalidBodyLengthError) + for error in failure.value.reasons + ) + ) d = self.make_request(request) d.addCallback(self.fail) @@ -436,13 +458,13 @@ class Https2ClientProtocolTestCase(TestCase): return d def test_missing_content_length_header(self): - request = Request(url=self.get_url('/no-content-length-header')) + request = Request(url=self.get_url("/no-content-length-header")) def assert_content_length(response: Response): self.assertEqual(response.status, 200) self.assertEqual(response.body, Data.NO_CONTENT_LENGTH) self.assertEqual(response.request, request) - self.assertNotIn('Content-Length', response.headers) + self.assertNotIn("Content-Length", response.headers) d = self.make_request(request) d.addCallback(assert_content_length) @@ -450,40 +472,39 @@ class Https2ClientProtocolTestCase(TestCase): return d @inlineCallbacks - def _check_log_warnsize( - self, - request, - warn_pattern, - expected_body - ): - with self.assertLogs('scrapy.core.http2.stream', level='WARNING') as cm: + def _check_log_warnsize(self, request, warn_pattern, expected_body): + with self.assertLogs("scrapy.core.http2.stream", level="WARNING") as cm: response = yield self.make_request(request) self.assertEqual(response.status, 200) self.assertEqual(response.request, request) self.assertEqual(response.body, expected_body) # Check the warning is raised only once for this request - self.assertEqual(sum( - len(re.findall(warn_pattern, log)) - for log in cm.output - ), 1) + self.assertEqual( + sum(len(re.findall(warn_pattern, log)) for log in cm.output), 1 + ) @inlineCallbacks def test_log_expected_warnsize(self): - request = Request(url=self.get_url('/get-data-html-large'), meta={'download_warnsize': 1000}) + request = Request( + url=self.get_url("/get-data-html-large"), meta={"download_warnsize": 1000} + ) warn_pattern = re.compile( - rf'Expected response size \(\d*\) larger than ' - rf'download warn size \(1000\) in request {request}' + rf"Expected response size \(\d*\) larger than " + rf"download warn size \(1000\) in request {request}" ) yield self._check_log_warnsize(request, warn_pattern, Data.HTML_LARGE) @inlineCallbacks def test_log_received_warnsize(self): - request = Request(url=self.get_url('/no-content-length-header'), meta={'download_warnsize': 10}) + request = Request( + url=self.get_url("/no-content-length-header"), + meta={"download_warnsize": 10}, + ) warn_pattern = re.compile( - rf'Received more \(\d*\) bytes than download ' - rf'warn size \(10\) in request {request}' + rf"Received more \(\d*\) bytes than download " + rf"warn size \(10\) in request {request}" ) yield self._check_log_warnsize(request, warn_pattern, Data.NO_CONTENT_LENGTH) @@ -495,9 +516,7 @@ class Https2ClientProtocolTestCase(TestCase): def get_deferred(): return self._check_GET( - Request(self.get_url('/get-data-html-small')), - Data.HTML_SMALL, - 200 + Request(self.get_url("/get-data-html-small")), Data.HTML_SMALL, 200 ) return self._check_repeat(get_deferred, 500) @@ -511,20 +530,20 @@ class Https2ClientProtocolTestCase(TestCase): def assert_inactive_stream(failure): self.assertIsNotNone(failure.check(ResponseFailed)) from scrapy.core.http2.stream import InactiveStreamClosed - self.assertTrue(any( - isinstance(e, InactiveStreamClosed) - for e in failure.value.reasons - )) + + self.assertTrue( + any(isinstance(e, InactiveStreamClosed) for e in failure.value.reasons) + ) # Send 100 request (we do not check the result) for _ in range(100): - d = self.make_request(Request(self.get_url('/get-data-html-small'))) + d = self.make_request(Request(self.get_url("/get-data-html-small"))) d.addBoth(lambda _: None) d_list.append(d) # Now send 10 extra request and save the response deferred in a list for _ in range(10): - d = self.make_request(Request(self.get_url('/get-data-html-small'))) + d = self.make_request(Request(self.get_url("/get-data-html-small"))) d.addCallback(self.fail) d.addErrback(assert_inactive_stream) d_list.append(d) @@ -537,19 +556,21 @@ class Https2ClientProtocolTestCase(TestCase): def test_invalid_request_type(self): with self.assertRaises(TypeError): - self.make_request('https://InvalidDataTypePassed.com') + self.make_request("https://InvalidDataTypePassed.com") def test_query_parameters(self): params = { - 'a': generate_random_string(20), - 'b': generate_random_string(20), - 'c': generate_random_string(20), - 'd': generate_random_string(20) + "a": generate_random_string(20), + "b": generate_random_string(20), + "c": generate_random_string(20), + "d": generate_random_string(20), } - request = Request(self.get_url(f'/query-params?{urlencode(params)}')) + request = Request(self.get_url(f"/query-params?{urlencode(params)}")) def assert_query_params(response: Response): - content_encoding = str(response.headers[b'Content-Encoding'], 'utf-8') + content_encoding_header = response.headers[b"Content-Encoding"] + assert content_encoding_header is not None + content_encoding = str(content_encoding_header, "utf-8") data = json.loads(str(response.body, content_encoding)) self.assertEqual(data, params) @@ -565,7 +586,7 @@ class Https2ClientProtocolTestCase(TestCase): d_list = [] for status in [200, 404]: - request = Request(self.get_url(f'/status?n={status}')) + request = Request(self.get_url(f"/status?n={status}")) d = self.make_request(request) d.addCallback(assert_response_status, status) d.addErrback(self.fail) @@ -574,17 +595,24 @@ class Https2ClientProtocolTestCase(TestCase): return DeferredList(d_list, fireOnOneErrback=True) def test_response_has_correct_certificate_ip_address(self): - request = Request(self.get_url('/status?n=200')) + request = Request(self.get_url("/status?n=200")) def assert_metadata(response: Response): self.assertEqual(response.request, request) self.assertIsInstance(response.certificate, Certificate) + assert response.certificate # typing self.assertIsNotNone(response.certificate.original) - self.assertEqual(response.certificate.getIssuer(), self.client_certificate.getIssuer()) - self.assertTrue(response.certificate.getPublicKey().matches(self.client_certificate.getPublicKey())) + self.assertEqual( + response.certificate.getIssuer(), self.client_certificate.getIssuer() + ) + self.assertTrue( + response.certificate.getPublicKey().matches( + self.client_certificate.getPublicKey() + ) + ) self.assertIsInstance(response.ip_address, IPv4Address) - self.assertEqual(str(response.ip_address), '127.0.0.1') + self.assertEqual(str(response.ip_address), "127.0.0.1") d = self.make_request(request) d.addCallback(assert_metadata) @@ -597,10 +625,11 @@ class Https2ClientProtocolTestCase(TestCase): def assert_invalid_hostname(failure: Failure): from scrapy.core.http2.stream import InvalidHostname + self.assertIsNotNone(failure.check(InvalidHostname)) error_msg = str(failure.value) - self.assertIn('localhost', error_msg) - self.assertIn('127.0.0.1', error_msg) + self.assertIn("localhost", error_msg) + self.assertIn("127.0.0.1", error_msg) self.assertIn(str(request), error_msg) d = self.make_request(request) @@ -609,24 +638,24 @@ class Https2ClientProtocolTestCase(TestCase): return d def test_invalid_hostname(self): - return self._check_invalid_netloc('https://notlocalhost.notlocalhostdomain') + return self._check_invalid_netloc("https://notlocalhost.notlocalhostdomain") def test_invalid_host_port(self): port = self.port_number + 1 - return self._check_invalid_netloc(f'https://127.0.0.1:{port}') + return self._check_invalid_netloc(f"https://127.0.0.1:{port}") def test_connection_stays_with_invalid_requests(self): d_list = [ self.test_invalid_hostname(), self.test_invalid_host_port(), self.test_GET_small_body(), - self.test_POST_small_json() + self.test_POST_small_json(), ] return DeferredList(d_list, fireOnOneErrback=True) def test_connection_timeout(self): - request = Request(self.get_url('/timeout')) + request = Request(self.get_url("/timeout")) d = self.make_request(request) # Update the timer to 1s to test connection timeout @@ -635,8 +664,12 @@ class Https2ClientProtocolTestCase(TestCase): def assert_timeout_error(failure: Failure): for err in failure.value.reasons: from scrapy.core.http2.protocol import H2ClientProtocol + if isinstance(err, TimeoutError): - self.assertIn(f"Connection was IDLE for more than {H2ClientProtocol.IDLE_TIMEOUT}s", str(err)) + self.assertIn( + f"Connection was IDLE for more than {H2ClientProtocol.IDLE_TIMEOUT}s", + str(err), + ) break else: self.fail() @@ -646,20 +679,20 @@ class Https2ClientProtocolTestCase(TestCase): return d def test_request_headers_received(self): - request = Request(self.get_url('/request-headers'), headers={ - 'header-1': 'header value 1', - 'header-2': 'header value 2' - }) + request = Request( + self.get_url("/request-headers"), + headers={"header-1": "header value 1", "header-2": "header value 2"}, + ) d = self.make_request(request) def assert_request_headers(response: Response): self.assertEqual(response.status, 200) self.assertEqual(response.request, request) - response_headers = json.loads(str(response.body, 'utf-8')) + response_headers = json.loads(str(response.body, "utf-8")) self.assertIsInstance(response_headers, dict) for k, v in request.headers.items(): - k, v = str(k, 'utf-8'), str(v[0], 'utf-8') + k, v = str(k, "utf-8"), str(v[0], "utf-8") self.assertIn(k, response_headers) self.assertEqual(v, response_headers[k]) diff --git a/tests/test_http_cookies.py b/tests/test_http_cookies.py index 08420332c..9e43b72b0 100644 --- a/tests/test_http_cookies.py +++ b/tests/test_http_cookies.py @@ -1,15 +1,15 @@ -from urllib.parse import urlparse from unittest import TestCase +from urllib.parse import urlparse from scrapy.http import Request, Response from scrapy.http.cookies import WrappedRequest, WrappedResponse class WrappedRequestTest(TestCase): - def setUp(self): - self.request = Request("http://www.example.com/page.html", - headers={"Content-Type": "text/html"}) + self.request = Request( + "http://www.example.com/page.html", headers={"Content-Type": "text/html"} + ) self.wrapped = WrappedRequest(self.request) def test_get_full_url(self): @@ -29,35 +29,34 @@ class WrappedRequestTest(TestCase): self.assertFalse(self.wrapped.unverifiable) def test_is_unverifiable2(self): - self.request.meta['is_unverifiable'] = True + self.request.meta["is_unverifiable"] = True self.assertTrue(self.wrapped.is_unverifiable()) self.assertTrue(self.wrapped.unverifiable) def test_get_origin_req_host(self): - self.assertEqual(self.wrapped.origin_req_host, 'www.example.com') + self.assertEqual(self.wrapped.origin_req_host, "www.example.com") def test_has_header(self): - self.assertTrue(self.wrapped.has_header('content-type')) - self.assertFalse(self.wrapped.has_header('xxxxx')) + self.assertTrue(self.wrapped.has_header("content-type")) + self.assertFalse(self.wrapped.has_header("xxxxx")) def test_get_header(self): - self.assertEqual(self.wrapped.get_header('content-type'), 'text/html') - self.assertEqual(self.wrapped.get_header('xxxxx', 'def'), 'def') + self.assertEqual(self.wrapped.get_header("content-type"), "text/html") + self.assertEqual(self.wrapped.get_header("xxxxx", "def"), "def") def test_header_items(self): - self.assertEqual(self.wrapped.header_items(), - [('Content-Type', ['text/html'])]) + self.assertEqual(self.wrapped.header_items(), [("Content-Type", ["text/html"])]) def test_add_unredirected_header(self): - self.wrapped.add_unredirected_header('hello', 'world') - self.assertEqual(self.request.headers['hello'], b'world') + self.wrapped.add_unredirected_header("hello", "world") + self.assertEqual(self.request.headers["hello"], b"world") class WrappedResponseTest(TestCase): - def setUp(self): - self.response = Response("http://www.example.com/page.html", - headers={"Content-TYpe": "text/html"}) + self.response = Response( + "http://www.example.com/page.html", headers={"Content-TYpe": "text/html"} + ) self.wrapped = WrappedResponse(self.response) def test_info(self): @@ -65,4 +64,4 @@ class WrappedResponseTest(TestCase): def test_get_all(self): # get_all result must be native string - self.assertEqual(self.wrapped.get_all('content-type'), ['text/html']) + self.assertEqual(self.wrapped.get_all("content-type"), ["text/html"]) diff --git a/tests/test_http_headers.py b/tests/test_http_headers.py index 1ca936247..7db1eb8c5 100644 --- a/tests/test_http_headers.py +++ b/tests/test_http_headers.py @@ -1,42 +1,43 @@ -import unittest import copy +import unittest from scrapy.http import Headers class HeadersTest(unittest.TestCase): - def assertSortedEqual(self, first, second, msg=None): return self.assertEqual(sorted(first), sorted(second), msg) def test_basics(self): - h = Headers({'Content-Type': 'text/html', 'Content-Length': 1234}) - assert h['Content-Type'] - assert h['Content-Length'] + h = Headers({"Content-Type": "text/html", "Content-Length": 1234}) + assert h["Content-Type"] + assert h["Content-Length"] - self.assertRaises(KeyError, h.__getitem__, 'Accept') - self.assertEqual(h.get('Accept'), None) - self.assertEqual(h.getlist('Accept'), []) + self.assertRaises(KeyError, h.__getitem__, "Accept") + self.assertEqual(h.get("Accept"), None) + self.assertEqual(h.getlist("Accept"), []) - self.assertEqual(h.get('Accept', '*/*'), b'*/*') - self.assertEqual(h.getlist('Accept', '*/*'), [b'*/*']) - self.assertEqual(h.getlist('Accept', ['text/html', 'images/jpeg']), - [b'text/html', b'images/jpeg']) + self.assertEqual(h.get("Accept", "*/*"), b"*/*") + self.assertEqual(h.getlist("Accept", "*/*"), [b"*/*"]) + self.assertEqual( + h.getlist("Accept", ["text/html", "images/jpeg"]), + [b"text/html", b"images/jpeg"], + ) def test_single_value(self): h = Headers() - h['Content-Type'] = 'text/html' - self.assertEqual(h['Content-Type'], b'text/html') - self.assertEqual(h.get('Content-Type'), b'text/html') - self.assertEqual(h.getlist('Content-Type'), [b'text/html']) + h["Content-Type"] = "text/html" + self.assertEqual(h["Content-Type"], b"text/html") + self.assertEqual(h.get("Content-Type"), b"text/html") + self.assertEqual(h.getlist("Content-Type"), [b"text/html"]) def test_multivalue(self): h = Headers() - h['X-Forwarded-For'] = hlist = ['ip1', 'ip2'] - self.assertEqual(h['X-Forwarded-For'], b'ip2') - self.assertEqual(h.get('X-Forwarded-For'), b'ip2') - self.assertEqual(h.getlist('X-Forwarded-For'), [b'ip1', b'ip2']) - assert h.getlist('X-Forwarded-For') is not hlist + h["X-Forwarded-For"] = hlist = ["ip1", "ip2"] + self.assertEqual(h["X-Forwarded-For"], b"ip2") + self.assertEqual(h.get("X-Forwarded-For"), b"ip2") + self.assertEqual(h.getlist("X-Forwarded-For"), [b"ip1", b"ip2"]) + assert h.getlist("X-Forwarded-For") is not hlist def test_multivalue_for_one_header(self): h = Headers((("a", "b"), ("a", "c"))) @@ -45,117 +46,121 @@ class HeadersTest(unittest.TestCase): self.assertEqual(h.getlist("a"), [b"b", b"c"]) def test_encode_utf8(self): - h = Headers({'key': '\xa3'}, encoding='utf-8') + h = Headers({"key": "\xa3"}, encoding="utf-8") key, val = dict(h).popitem() assert isinstance(key, bytes), key assert isinstance(val[0], bytes), val[0] - self.assertEqual(val[0], b'\xc2\xa3') + self.assertEqual(val[0], b"\xc2\xa3") def test_encode_latin1(self): - h = Headers({'key': '\xa3'}, encoding='latin1') + h = Headers({"key": "\xa3"}, encoding="latin1") key, val = dict(h).popitem() - self.assertEqual(val[0], b'\xa3') + self.assertEqual(val[0], b"\xa3") def test_encode_multiple(self): - h = Headers({'key': ['\xa3']}, encoding='utf-8') + h = Headers({"key": ["\xa3"]}, encoding="utf-8") key, val = dict(h).popitem() - self.assertEqual(val[0], b'\xc2\xa3') + self.assertEqual(val[0], b"\xc2\xa3") def test_delete_and_contains(self): h = Headers() - h['Content-Type'] = 'text/html' - assert 'Content-Type' in h - del h['Content-Type'] - assert 'Content-Type' not in h + h["Content-Type"] = "text/html" + assert "Content-Type" in h + del h["Content-Type"] + assert "Content-Type" not in h def test_setdefault(self): h = Headers() - hlist = ['ip1', 'ip2'] - olist = h.setdefault('X-Forwarded-For', hlist) - assert h.getlist('X-Forwarded-For') is not hlist - assert h.getlist('X-Forwarded-For') is olist + hlist = ["ip1", "ip2"] + olist = h.setdefault("X-Forwarded-For", hlist) + assert h.getlist("X-Forwarded-For") is not hlist + assert h.getlist("X-Forwarded-For") is olist h = Headers() - olist = h.setdefault('X-Forwarded-For', 'ip1') - self.assertEqual(h.getlist('X-Forwarded-For'), [b'ip1']) - assert h.getlist('X-Forwarded-For') is olist + olist = h.setdefault("X-Forwarded-For", "ip1") + self.assertEqual(h.getlist("X-Forwarded-For"), [b"ip1"]) + assert h.getlist("X-Forwarded-For") is olist def test_iterables(self): - idict = {'Content-Type': 'text/html', 'X-Forwarded-For': ['ip1', 'ip2']} + idict = {"Content-Type": "text/html", "X-Forwarded-For": ["ip1", "ip2"]} h = Headers(idict) - self.assertDictEqual(dict(h), - {b'Content-Type': [b'text/html'], - b'X-Forwarded-For': [b'ip1', b'ip2']}) - self.assertSortedEqual(h.keys(), - [b'X-Forwarded-For', b'Content-Type']) - self.assertSortedEqual(h.items(), - [(b'X-Forwarded-For', [b'ip1', b'ip2']), - (b'Content-Type', [b'text/html'])]) - self.assertSortedEqual(h.values(), [b'ip2', b'text/html']) + self.assertDictEqual( + dict(h), + {b"Content-Type": [b"text/html"], b"X-Forwarded-For": [b"ip1", b"ip2"]}, + ) + self.assertSortedEqual(h.keys(), [b"X-Forwarded-For", b"Content-Type"]) + self.assertSortedEqual( + h.items(), + [(b"X-Forwarded-For", [b"ip1", b"ip2"]), (b"Content-Type", [b"text/html"])], + ) + self.assertSortedEqual(h.values(), [b"ip2", b"text/html"]) def test_update(self): h = Headers() - h.update({'Content-Type': 'text/html', - 'X-Forwarded-For': ['ip1', 'ip2']}) - self.assertEqual(h.getlist('Content-Type'), [b'text/html']) - self.assertEqual(h.getlist('X-Forwarded-For'), [b'ip1', b'ip2']) + h.update({"Content-Type": "text/html", "X-Forwarded-For": ["ip1", "ip2"]}) + self.assertEqual(h.getlist("Content-Type"), [b"text/html"]) + self.assertEqual(h.getlist("X-Forwarded-For"), [b"ip1", b"ip2"]) def test_copy(self): - h1 = Headers({'header1': ['value1', 'value2']}) + h1 = Headers({"header1": ["value1", "value2"]}) h2 = copy.copy(h1) self.assertEqual(h1, h2) - self.assertEqual(h1.getlist('header1'), h2.getlist('header1')) - assert h1.getlist('header1') is not h2.getlist('header1') + self.assertEqual(h1.getlist("header1"), h2.getlist("header1")) + assert h1.getlist("header1") is not h2.getlist("header1") assert isinstance(h2, Headers) def test_appendlist(self): - h1 = Headers({'header1': 'value1'}) - h1.appendlist('header1', 'value3') - self.assertEqual(h1.getlist('header1'), [b'value1', b'value3']) + h1 = Headers({"header1": "value1"}) + h1.appendlist("header1", "value3") + self.assertEqual(h1.getlist("header1"), [b"value1", b"value3"]) h1 = Headers() - h1.appendlist('header1', 'value1') - h1.appendlist('header1', 'value3') - self.assertEqual(h1.getlist('header1'), [b'value1', b'value3']) + h1.appendlist("header1", "value1") + h1.appendlist("header1", "value3") + self.assertEqual(h1.getlist("header1"), [b"value1", b"value3"]) def test_setlist(self): - h1 = Headers({'header1': 'value1'}) - self.assertEqual(h1.getlist('header1'), [b'value1']) - h1.setlist('header1', [b'value2', b'value3']) - self.assertEqual(h1.getlist('header1'), [b'value2', b'value3']) + h1 = Headers({"header1": "value1"}) + self.assertEqual(h1.getlist("header1"), [b"value1"]) + h1.setlist("header1", [b"value2", b"value3"]) + self.assertEqual(h1.getlist("header1"), [b"value2", b"value3"]) def test_setlistdefault(self): - h1 = Headers({'header1': 'value1'}) - h1.setlistdefault('header1', ['value2', 'value3']) - h1.setlistdefault('header2', ['value2', 'value3']) - self.assertEqual(h1.getlist('header1'), [b'value1']) - self.assertEqual(h1.getlist('header2'), [b'value2', b'value3']) + h1 = Headers({"header1": "value1"}) + h1.setlistdefault("header1", ["value2", "value3"]) + h1.setlistdefault("header2", ["value2", "value3"]) + self.assertEqual(h1.getlist("header1"), [b"value1"]) + self.assertEqual(h1.getlist("header2"), [b"value2", b"value3"]) def test_none_value(self): h1 = Headers() - h1['foo'] = 'bar' - h1['foo'] = None - h1.setdefault('foo', 'bar') - self.assertEqual(h1.get('foo'), None) - self.assertEqual(h1.getlist('foo'), []) + h1["foo"] = "bar" + h1["foo"] = None + h1.setdefault("foo", "bar") + self.assertEqual(h1.get("foo"), None) + self.assertEqual(h1.getlist("foo"), []) def test_int_value(self): - h1 = Headers({'hey': 5}) - h1['foo'] = 1 - h1.setdefault('bar', 2) - h1.setlist('buz', [1, 'dos', 3]) - self.assertEqual(h1.getlist('foo'), [b'1']) - self.assertEqual(h1.getlist('bar'), [b'2']) - self.assertEqual(h1.getlist('buz'), [b'1', b'dos', b'3']) - self.assertEqual(h1.getlist('hey'), [b'5']) + h1 = Headers({"hey": 5}) + h1["foo"] = 1 + h1.setdefault("bar", 2) + h1.setlist("buz", [1, "dos", 3]) + self.assertEqual(h1.getlist("foo"), [b"1"]) + self.assertEqual(h1.getlist("bar"), [b"2"]) + self.assertEqual(h1.getlist("buz"), [b"1", b"dos", b"3"]) + self.assertEqual(h1.getlist("hey"), [b"5"]) def test_invalid_value(self): - self.assertRaisesRegex(TypeError, 'Unsupported value type', - Headers, {'foo': object()}) - self.assertRaisesRegex(TypeError, 'Unsupported value type', - Headers().__setitem__, 'foo', object()) - self.assertRaisesRegex(TypeError, 'Unsupported value type', - Headers().setdefault, 'foo', object()) - self.assertRaisesRegex(TypeError, 'Unsupported value type', - Headers().setlist, 'foo', [object()]) + self.assertRaisesRegex( + TypeError, "Unsupported value type", Headers, {"foo": object()} + ) + self.assertRaisesRegex( + TypeError, "Unsupported value type", Headers().__setitem__, "foo", object() + ) + self.assertRaisesRegex( + TypeError, "Unsupported value type", Headers().setdefault, "foo", object() + ) + self.assertRaisesRegex( + TypeError, "Unsupported value type", Headers().setlist, "foo", [object()] + ) diff --git a/tests/test_http_request.py b/tests/test_http_request.py index 9f7f1854f..6dc9ec8b7 100644 --- a/tests/test_http_request.py +++ b/tests/test_http_request.py @@ -1,21 +1,29 @@ -import unittest -import re import json -import xmlrpc.client +import re +import unittest import warnings +import xmlrpc.client +from typing import Any, Dict, List from unittest import mock from urllib.parse import parse_qs, unquote_to_bytes, urlparse -from scrapy.http import Request, FormRequest, XmlRpcRequest, JsonRequest, Headers, HtmlResponse +from scrapy.http import ( + FormRequest, + Headers, + HtmlResponse, + JsonRequest, + Request, + XmlRpcRequest, +) +from scrapy.http.request import NO_CALLBACK from scrapy.utils.python import to_bytes, to_unicode class RequestTest(unittest.TestCase): - request_class = Request - default_method = 'GET' - default_headers = {} - default_meta = {} + default_method = "GET" + default_headers: Dict[bytes, List[bytes]] = {} + default_meta: Dict[str, Any] = {} def test_init(self): # Request requires url in the __init__ method @@ -23,7 +31,7 @@ class RequestTest(unittest.TestCase): # url argument must be basestring self.assertRaises(TypeError, self.request_class, 123) - r = self.request_class('http://www.example.com') + r = self.request_class("http://www.example.com") r = self.request_class("http://www.example.com") assert isinstance(r.url, str) @@ -36,7 +44,9 @@ class RequestTest(unittest.TestCase): meta = {"lala": "lolo"} headers = {b"caca": b"coco"} - r = self.request_class("http://www.example.com", meta=meta, headers=headers, body="a body") + r = self.request_class( + "http://www.example.com", meta=meta, headers=headers, body="a body" + ) assert r.meta is not meta self.assertEqual(r.meta, meta) @@ -45,22 +55,22 @@ class RequestTest(unittest.TestCase): def test_url_scheme(self): # This test passes by not raising any (ValueError) exception - self.request_class('http://example.org') - self.request_class('https://example.org') - self.request_class('s3://example.org') - self.request_class('ftp://example.org') - self.request_class('about:config') - self.request_class('data:,Hello%2C%20World!') + self.request_class("http://example.org") + self.request_class("https://example.org") + self.request_class("s3://example.org") + self.request_class("ftp://example.org") + self.request_class("about:config") + self.request_class("data:,Hello%2C%20World!") def test_url_no_scheme(self): - self.assertRaises(ValueError, self.request_class, 'foo') - self.assertRaises(ValueError, self.request_class, '/foo/') - self.assertRaises(ValueError, self.request_class, '/foo:bar') + self.assertRaises(ValueError, self.request_class, "foo") + self.assertRaises(ValueError, self.request_class, "/foo/") + self.assertRaises(ValueError, self.request_class, "/foo:bar") def test_headers(self): # Different ways of setting headers attribute - url = 'http://www.scrapy.org' - headers = {b'Accept': 'gzip', b'Custom-Header': 'nothing to tell you'} + url = "http://www.scrapy.org" + headers = {b"Accept": "gzip", b"Custom-Header": "nothing to tell you"} r = self.request_class(url=url, headers=headers) p = self.request_class(url=url, headers=r.headers) @@ -69,15 +79,15 @@ class RequestTest(unittest.TestCase): self.assertFalse(p.headers is r.headers) # headers must not be unicode - h = Headers({'key1': 'val1', 'key2': 'val2'}) - h['newkey'] = 'newval' + h = Headers({"key1": "val1", "key2": "val2"}) + h["newkey"] = "newval" for k, v in h.items(): self.assertIsInstance(k, bytes) for s in v: self.assertIsInstance(s, bytes) def test_eq(self): - url = 'http://www.scrapy.org' + url = "http://www.scrapy.org" r1 = self.request_class(url=url) r2 = self.request_class(url=url) self.assertNotEqual(r1, r2) @@ -115,13 +125,17 @@ class RequestTest(unittest.TestCase): self.assertEqual(r1.url, "http://www.scrapy.org/price/%C2%A3?unit=%C2%B5") # should be same as above - r2 = self.request_class(url="http://www.scrapy.org/price/£?unit=µ", encoding="utf-8") + r2 = self.request_class( + url="http://www.scrapy.org/price/£?unit=µ", encoding="utf-8" + ) self.assertEqual(r2.url, "http://www.scrapy.org/price/%C2%A3?unit=%C2%B5") def test_url_encoding_query_latin1(self): # encoding is used for encoding query-string before percent-escaping; # path is still UTF-8 encoded before percent-escaping - r3 = self.request_class(url="http://www.scrapy.org/price/µ?currency=£", encoding="latin1") + r3 = self.request_class( + url="http://www.scrapy.org/price/µ?currency=£", encoding="latin1" + ) self.assertEqual(r3.url, "http://www.scrapy.org/price/%C2%B5?currency=%A3") def test_url_encoding_nonutf8_untouched(self): @@ -154,27 +168,35 @@ class RequestTest(unittest.TestCase): def test_body(self): r1 = self.request_class(url="http://www.example.com/") - assert r1.body == b'' + assert r1.body == b"" r2 = self.request_class(url="http://www.example.com/", body=b"") assert isinstance(r2.body, bytes) - self.assertEqual(r2.encoding, 'utf-8') # default encoding + self.assertEqual(r2.encoding, "utf-8") # default encoding - r3 = self.request_class(url="http://www.example.com/", body="Price: \xa3100", encoding='utf-8') + r3 = self.request_class( + url="http://www.example.com/", body="Price: \xa3100", encoding="utf-8" + ) assert isinstance(r3.body, bytes) self.assertEqual(r3.body, b"Price: \xc2\xa3100") - r4 = self.request_class(url="http://www.example.com/", body="Price: \xa3100", encoding='latin1') + r4 = self.request_class( + url="http://www.example.com/", body="Price: \xa3100", encoding="latin1" + ) assert isinstance(r4.body, bytes) self.assertEqual(r4.body, b"Price: \xa3100") def test_ajax_url(self): # ascii url r = self.request_class(url="http://www.example.com/ajax.html#!key=value") - self.assertEqual(r.url, "http://www.example.com/ajax.html?_escaped_fragment_=key%3Dvalue") + self.assertEqual( + r.url, "http://www.example.com/ajax.html?_escaped_fragment_=key%3Dvalue" + ) # unicode url r = self.request_class(url="http://www.example.com/ajax.html#!key=value") - self.assertEqual(r.url, "http://www.example.com/ajax.html?_escaped_fragment_=key%3Dvalue") + self.assertEqual( + r.url, "http://www.example.com/ajax.html?_escaped_fragment_=key%3Dvalue" + ) def test_copy(self): """Test Request copy""" @@ -182,10 +204,14 @@ class RequestTest(unittest.TestCase): def somecallback(): pass - r1 = self.request_class("http://www.example.com", flags=['f1', 'f2'], - callback=somecallback, errback=somecallback) - r1.meta['foo'] = 'bar' - r1.cb_kwargs['key'] = 'value' + r1 = self.request_class( + "http://www.example.com", + flags=["f1", "f2"], + callback=somecallback, + errback=somecallback, + ) + r1.meta["foo"] = "bar" + r1.cb_kwargs["key"] = "value" r2 = r1.copy() # make sure copy does not propagate callbacks @@ -199,7 +225,9 @@ class RequestTest(unittest.TestCase): self.assertEqual(r1.flags, r2.flags) # make sure cb_kwargs dict is shallow copied - assert r1.cb_kwargs is not r2.cb_kwargs, "cb_kwargs must be a shallow copy, not identical" + assert ( + r1.cb_kwargs is not r2.cb_kwargs + ), "cb_kwargs must be a shallow copy, not identical" self.assertEqual(r1.cb_kwargs, r2.cb_kwargs) # make sure meta dict is shallow copied @@ -207,7 +235,9 @@ class RequestTest(unittest.TestCase): self.assertEqual(r1.meta, r2.meta) # make sure headers attribute is shallow copied - assert r1.headers is not r2.headers, "headers must be a shallow copy, not identical" + assert ( + r1.headers is not r2.headers + ), "headers must be a shallow copy, not identical" self.assertEqual(r1.headers, r2.headers) self.assertEqual(r1.encoding, r2.encoding) self.assertEqual(r1.dont_filter, r2.dont_filter) @@ -220,27 +250,31 @@ class RequestTest(unittest.TestCase): class CustomRequest(self.request_class): pass - r1 = CustomRequest('http://www.example.com') + r1 = CustomRequest("http://www.example.com") r2 = r1.copy() assert isinstance(r2, CustomRequest) def test_replace(self): """Test Request.replace() method""" - r1 = self.request_class("http://www.example.com", method='GET') + r1 = self.request_class("http://www.example.com", method="GET") hdrs = Headers(r1.headers) - hdrs[b'key'] = b'value' + hdrs[b"key"] = b"value" r2 = r1.replace(method="POST", body="New body", headers=hdrs) self.assertEqual(r1.url, r2.url) self.assertEqual((r1.method, r2.method), ("GET", "POST")) - self.assertEqual((r1.body, r2.body), (b'', b"New body")) + self.assertEqual((r1.body, r2.body), (b"", b"New body")) self.assertEqual((r1.headers, r2.headers), (self.default_headers, hdrs)) # Empty attributes (which may fail if not compared properly) - r3 = self.request_class("http://www.example.com", meta={'a': 1}, dont_filter=True) - r4 = r3.replace(url="http://www.example.com/2", body=b'', meta={}, dont_filter=False) + r3 = self.request_class( + "http://www.example.com", meta={"a": 1}, dont_filter=True + ) + r4 = r3.replace( + url="http://www.example.com/2", body=b"", meta={}, dont_filter=False + ) self.assertEqual(r4.url, "http://www.example.com/2") - self.assertEqual(r4.body, b'') + self.assertEqual(r4.body, b"") self.assertEqual(r4.meta, {}) assert r4.dont_filter is False @@ -250,45 +284,57 @@ class RequestTest(unittest.TestCase): def test_immutable_attributes(self): r = self.request_class("http://example.com") - self.assertRaises(AttributeError, setattr, r, 'url', 'http://example2.com') - self.assertRaises(AttributeError, setattr, r, 'body', 'xxx') + self.assertRaises(AttributeError, setattr, r, "url", "http://example2.com") + self.assertRaises(AttributeError, setattr, r, "body", "xxx") def test_callback_and_errback(self): def a_function(): pass - r1 = self.request_class('http://example.com') + r1 = self.request_class("http://example.com") self.assertIsNone(r1.callback) self.assertIsNone(r1.errback) - r2 = self.request_class('http://example.com', callback=a_function) + r2 = self.request_class("http://example.com", callback=a_function) self.assertIs(r2.callback, a_function) self.assertIsNone(r2.errback) - r3 = self.request_class('http://example.com', errback=a_function) + r3 = self.request_class("http://example.com", errback=a_function) self.assertIsNone(r3.callback) self.assertIs(r3.errback, a_function) r4 = self.request_class( - url='http://example.com', + url="http://example.com", callback=a_function, errback=a_function, ) self.assertIs(r4.callback, a_function) self.assertIs(r4.errback, a_function) + r5 = self.request_class( + url="http://example.com", + callback=NO_CALLBACK, + errback=NO_CALLBACK, + ) + self.assertIs(r5.callback, NO_CALLBACK) + self.assertIs(r5.errback, NO_CALLBACK) + def test_callback_and_errback_type(self): with self.assertRaises(TypeError): - self.request_class('http://example.com', callback='a_function') + self.request_class("http://example.com", callback="a_function") with self.assertRaises(TypeError): - self.request_class('http://example.com', errback='a_function') + self.request_class("http://example.com", errback="a_function") with self.assertRaises(TypeError): self.request_class( - url='http://example.com', - callback='a_function', - errback='a_function', + url="http://example.com", + callback="a_function", + errback="a_function", ) + def test_no_callback(self): + with self.assertRaises(RuntimeError): + NO_CALLBACK() + def test_from_curl(self): # Note: more curated tests regarding curl conversion are in # `test_utils_curl.py` @@ -311,37 +357,48 @@ class RequestTest(unittest.TestCase): r = self.request_class.from_curl(curl_command) self.assertEqual(r.method, "POST") self.assertEqual(r.url, "http://httpbin.org/post") - self.assertEqual(r.body, - b"custname=John+Smith&custtel=500&custemail=jsmith%40" - b"example.org&size=small&topping=cheese&topping=onion" - b"&delivery=12%3A15&comments=") - self.assertEqual(r.cookies, { - '_gauges_unique_year': '1', - '_gauges_unique': '1', - '_gauges_unique_month': '1', - '_gauges_unique_hour': '1', - '_gauges_unique_day': '1' - }) - self.assertEqual(r.headers, { - b'Origin': [b'http://httpbin.org'], - b'Accept-Encoding': [b'gzip, deflate'], - b'Accept-Language': [b'en-US,en;q=0.9,ru;q=0.8,es;q=0.7'], - b'Upgrade-Insecure-Requests': [b'1'], - b'User-Agent': [b'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.' - b'36 (KHTML, like Gecko) Ubuntu Chromium/62.0.3202' - b'.75 Chrome/62.0.3202.75 Safari/537.36'], - b'Content-Type': [b'application /x-www-form-urlencoded'], - b'Accept': [b'text/html,application/xhtml+xml,application/xml;q=0.' - b'9,image/webp,image/apng,*/*;q=0.8'], - b'Cache-Control': [b'max-age=0'], - b'Referer': [b'http://httpbin.org/forms/post'], - b'Connection': [b'keep-alive']}) + self.assertEqual( + r.body, + b"custname=John+Smith&custtel=500&custemail=jsmith%40" + b"example.org&size=small&topping=cheese&topping=onion" + b"&delivery=12%3A15&comments=", + ) + self.assertEqual( + r.cookies, + { + "_gauges_unique_year": "1", + "_gauges_unique": "1", + "_gauges_unique_month": "1", + "_gauges_unique_hour": "1", + "_gauges_unique_day": "1", + }, + ) + self.assertEqual( + r.headers, + { + b"Origin": [b"http://httpbin.org"], + b"Accept-Encoding": [b"gzip, deflate"], + b"Accept-Language": [b"en-US,en;q=0.9,ru;q=0.8,es;q=0.7"], + b"Upgrade-Insecure-Requests": [b"1"], + b"User-Agent": [ + b"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537." + b"36 (KHTML, like Gecko) Ubuntu Chromium/62.0.3202" + b".75 Chrome/62.0.3202.75 Safari/537.36" + ], + b"Content-Type": [b"application /x-www-form-urlencoded"], + b"Accept": [ + b"text/html,application/xhtml+xml,application/xml;q=0." + b"9,image/webp,image/apng,*/*;q=0.8" + ], + b"Cache-Control": [b"max-age=0"], + b"Referer": [b"http://httpbin.org/forms/post"], + b"Connection": [b"keep-alive"], + }, + ) def test_from_curl_with_kwargs(self): r = self.request_class.from_curl( - 'curl -X PATCH "http://example.org"', - method="POST", - meta={'key': 'value'} + 'curl -X PATCH "http://example.org"', method="POST", meta={"key": "value"} ) self.assertEqual(r.method, "POST") self.assertEqual(r.meta, {"key": "value"}) @@ -349,13 +406,13 @@ class RequestTest(unittest.TestCase): def test_from_curl_ignore_unknown_options(self): # By default: it works and ignores the unknown options: --foo and -z with warnings.catch_warnings(): # avoid warning when executing tests - warnings.simplefilter('ignore') + warnings.simplefilter("ignore") r = self.request_class.from_curl( 'curl -X DELETE "http://example.org" --foo -z', ) self.assertEqual(r.method, "DELETE") - # If `ignore_unknon_options` is set to `False` it raises an error with + # If `ignore_unknown_options` is set to `False` it raises an error with # the unknown options: --foo and -z self.assertRaises( ValueError, @@ -367,7 +424,6 @@ class RequestTest(unittest.TestCase): class FormRequestTest(RequestTest): - request_class = FormRequest def assertQueryEqual(self, first, second, msg=None): @@ -377,68 +433,85 @@ class FormRequestTest(RequestTest): def test_empty_formdata(self): r1 = self.request_class("http://www.example.com", formdata={}) - self.assertEqual(r1.body, b'') + self.assertEqual(r1.body, b"") def test_formdata_overrides_querystring(self): - data = (('a', 'one'), ('a', 'two'), ('b', '2')) - url = self.request_class('http://www.example.com/?a=0&b=1&c=3#fragment', - method='GET', formdata=data).url.split('#')[0] - fs = _qs(self.request_class(url, method='GET', formdata=data)) - self.assertEqual(set(fs[b'a']), {b'one', b'two'}) - self.assertEqual(fs[b'b'], [b'2']) - self.assertIsNone(fs.get(b'c')) + data = (("a", "one"), ("a", "two"), ("b", "2")) + url = self.request_class( + "http://www.example.com/?a=0&b=1&c=3#fragment", method="GET", formdata=data + ).url.split("#")[0] + fs = _qs(self.request_class(url, method="GET", formdata=data)) + self.assertEqual(set(fs[b"a"]), {b"one", b"two"}) + self.assertEqual(fs[b"b"], [b"2"]) + self.assertIsNone(fs.get(b"c")) - data = {'a': '1', 'b': '2'} - fs = _qs(self.request_class('http://www.example.com/', method='GET', formdata=data)) - self.assertEqual(fs[b'a'], [b'1']) - self.assertEqual(fs[b'b'], [b'2']) + data = {"a": "1", "b": "2"} + fs = _qs( + self.request_class("http://www.example.com/", method="GET", formdata=data) + ) + self.assertEqual(fs[b"a"], [b"1"]) + self.assertEqual(fs[b"b"], [b"2"]) def test_default_encoding_bytes(self): # using default encoding (utf-8) - data = {b'one': b'two', b'price': b'\xc2\xa3 100'} + data = {b"one": b"two", b"price": b"\xc2\xa3 100"} r2 = self.request_class("http://www.example.com", formdata=data) - self.assertEqual(r2.method, 'POST') - self.assertEqual(r2.encoding, 'utf-8') - self.assertQueryEqual(r2.body, b'price=%C2%A3+100&one=two') - self.assertEqual(r2.headers[b'Content-Type'], b'application/x-www-form-urlencoded') + self.assertEqual(r2.method, "POST") + self.assertEqual(r2.encoding, "utf-8") + self.assertQueryEqual(r2.body, b"price=%C2%A3+100&one=two") + self.assertEqual( + r2.headers[b"Content-Type"], b"application/x-www-form-urlencoded" + ) def test_default_encoding_textual_data(self): # using default encoding (utf-8) - data = {'µ one': 'two', 'price': '£ 100'} + data = {"µ one": "two", "price": "£ 100"} r2 = self.request_class("http://www.example.com", formdata=data) - self.assertEqual(r2.method, 'POST') - self.assertEqual(r2.encoding, 'utf-8') - self.assertQueryEqual(r2.body, b'price=%C2%A3+100&%C2%B5+one=two') - self.assertEqual(r2.headers[b'Content-Type'], b'application/x-www-form-urlencoded') + self.assertEqual(r2.method, "POST") + self.assertEqual(r2.encoding, "utf-8") + self.assertQueryEqual(r2.body, b"price=%C2%A3+100&%C2%B5+one=two") + self.assertEqual( + r2.headers[b"Content-Type"], b"application/x-www-form-urlencoded" + ) def test_default_encoding_mixed_data(self): # using default encoding (utf-8) - data = {'\u00b5one': b'two', b'price\xc2\xa3': '\u00a3 100'} + data = {"\u00b5one": b"two", b"price\xc2\xa3": "\u00a3 100"} r2 = self.request_class("http://www.example.com", formdata=data) - self.assertEqual(r2.method, 'POST') - self.assertEqual(r2.encoding, 'utf-8') - self.assertQueryEqual(r2.body, b'%C2%B5one=two&price%C2%A3=%C2%A3+100') - self.assertEqual(r2.headers[b'Content-Type'], b'application/x-www-form-urlencoded') + self.assertEqual(r2.method, "POST") + self.assertEqual(r2.encoding, "utf-8") + self.assertQueryEqual(r2.body, b"%C2%B5one=two&price%C2%A3=%C2%A3+100") + self.assertEqual( + r2.headers[b"Content-Type"], b"application/x-www-form-urlencoded" + ) def test_custom_encoding_bytes(self): - data = {b'\xb5 one': b'two', b'price': b'\xa3 100'} - r2 = self.request_class("http://www.example.com", formdata=data, encoding='latin1') - self.assertEqual(r2.method, 'POST') - self.assertEqual(r2.encoding, 'latin1') - self.assertQueryEqual(r2.body, b'price=%A3+100&%B5+one=two') - self.assertEqual(r2.headers[b'Content-Type'], b'application/x-www-form-urlencoded') + data = {b"\xb5 one": b"two", b"price": b"\xa3 100"} + r2 = self.request_class( + "http://www.example.com", formdata=data, encoding="latin1" + ) + self.assertEqual(r2.method, "POST") + self.assertEqual(r2.encoding, "latin1") + self.assertQueryEqual(r2.body, b"price=%A3+100&%B5+one=two") + self.assertEqual( + r2.headers[b"Content-Type"], b"application/x-www-form-urlencoded" + ) def test_custom_encoding_textual_data(self): - data = {'price': '£ 100'} - r3 = self.request_class("http://www.example.com", formdata=data, encoding='latin1') - self.assertEqual(r3.encoding, 'latin1') - self.assertEqual(r3.body, b'price=%A3+100') + data = {"price": "£ 100"} + r3 = self.request_class( + "http://www.example.com", formdata=data, encoding="latin1" + ) + self.assertEqual(r3.encoding, "latin1") + self.assertEqual(r3.body, b"price=%A3+100") def test_multi_key_values(self): # using multiples values for a single key - data = {'price': '\xa3 100', 'colours': ['red', 'blue', 'green']} + data = {"price": "\xa3 100", "colours": ["red", "blue", "green"]} r3 = self.request_class("http://www.example.com", formdata=data) - self.assertQueryEqual(r3.body, b'colours=red&colours=blue&colours=green&price=%C2%A3+100') + self.assertQueryEqual( + r3.body, b"colours=red&colours=blue&colours=green&price=%C2%A3+100" + ) def test_from_response_post(self): response = _buildresponse( @@ -447,17 +520,22 @@ class FormRequestTest(RequestTest): """, - url="http://www.example.com/this/list.html") - req = self.request_class.from_response(response, formdata={'one': ['two', 'three'], 'six': 'seven'}) + url="http://www.example.com/this/list.html", + ) + req = self.request_class.from_response( + response, formdata={"one": ["two", "three"], "six": "seven"} + ) - self.assertEqual(req.method, 'POST') - self.assertEqual(req.headers[b'Content-type'], b'application/x-www-form-urlencoded') + self.assertEqual(req.method, "POST") + self.assertEqual( + req.headers[b"Content-type"], b"application/x-www-form-urlencoded" + ) self.assertEqual(req.url, "http://www.example.com/this/post.php") fs = _qs(req) - self.assertEqual(set(fs[b'test']), {b'val1', b'val2'}) - self.assertEqual(set(fs[b'one']), {b'two', b'three'}) - self.assertEqual(fs[b'test2'], [b'xxx']) - self.assertEqual(fs[b'six'], [b'seven']) + self.assertEqual(set(fs[b"test"]), {b"val1", b"val2"}) + self.assertEqual(set(fs[b"one"]), {b"two", b"three"}) + self.assertEqual(fs[b"test2"], [b"xxx"]) + self.assertEqual(fs[b"six"], [b"seven"]) def test_from_response_post_nonascii_bytes_utf8(self): response = _buildresponse( @@ -466,17 +544,22 @@ class FormRequestTest(RequestTest): """, - url="http://www.example.com/this/list.html") - req = self.request_class.from_response(response, formdata={'one': ['two', 'three'], 'six': 'seven'}) + url="http://www.example.com/this/list.html", + ) + req = self.request_class.from_response( + response, formdata={"one": ["two", "three"], "six": "seven"} + ) - self.assertEqual(req.method, 'POST') - self.assertEqual(req.headers[b'Content-type'], b'application/x-www-form-urlencoded') + self.assertEqual(req.method, "POST") + self.assertEqual( + req.headers[b"Content-type"], b"application/x-www-form-urlencoded" + ) self.assertEqual(req.url, "http://www.example.com/this/post.php") fs = _qs(req, to_unicode=True) - self.assertEqual(set(fs['test £']), {'val1', 'val2'}) - self.assertEqual(set(fs['one']), {'two', 'three'}) - self.assertEqual(fs['test2'], ['xxx µ']) - self.assertEqual(fs['six'], ['seven']) + self.assertEqual(set(fs["test £"]), {"val1", "val2"}) + self.assertEqual(set(fs["one"]), {"two", "three"}) + self.assertEqual(fs["test2"], ["xxx µ"]) + self.assertEqual(fs["six"], ["seven"]) def test_from_response_post_nonascii_bytes_latin1(self): response = _buildresponse( @@ -486,18 +569,22 @@ class FormRequestTest(RequestTest): """, url="http://www.example.com/this/list.html", - encoding='latin1', + encoding="latin1", + ) + req = self.request_class.from_response( + response, formdata={"one": ["two", "three"], "six": "seven"} ) - req = self.request_class.from_response(response, formdata={'one': ['two', 'three'], 'six': 'seven'}) - self.assertEqual(req.method, 'POST') - self.assertEqual(req.headers[b'Content-type'], b'application/x-www-form-urlencoded') + self.assertEqual(req.method, "POST") + self.assertEqual( + req.headers[b"Content-type"], b"application/x-www-form-urlencoded" + ) self.assertEqual(req.url, "http://www.example.com/this/post.php") - fs = _qs(req, to_unicode=True, encoding='latin1') - self.assertEqual(set(fs['test £']), {'val1', 'val2'}) - self.assertEqual(set(fs['one']), {'two', 'three'}) - self.assertEqual(fs['test2'], ['xxx µ']) - self.assertEqual(fs['six'], ['seven']) + fs = _qs(req, to_unicode=True, encoding="latin1") + self.assertEqual(set(fs["test £"]), {"val1", "val2"}) + self.assertEqual(set(fs["one"]), {"two", "three"}) + self.assertEqual(fs["test2"], ["xxx µ"]) + self.assertEqual(fs["six"], ["seven"]) def test_from_response_post_nonascii_unicode(self): response = _buildresponse( @@ -506,42 +593,46 @@ class FormRequestTest(RequestTest): """, - url="http://www.example.com/this/list.html") - req = self.request_class.from_response(response, formdata={'one': ['two', 'three'], 'six': 'seven'}) + url="http://www.example.com/this/list.html", + ) + req = self.request_class.from_response( + response, formdata={"one": ["two", "three"], "six": "seven"} + ) - self.assertEqual(req.method, 'POST') - self.assertEqual(req.headers[b'Content-type'], b'application/x-www-form-urlencoded') + self.assertEqual(req.method, "POST") + self.assertEqual( + req.headers[b"Content-type"], b"application/x-www-form-urlencoded" + ) self.assertEqual(req.url, "http://www.example.com/this/post.php") fs = _qs(req, to_unicode=True) - self.assertEqual(set(fs['test £']), {'val1', 'val2'}) - self.assertEqual(set(fs['one']), {'two', 'three'}) - self.assertEqual(fs['test2'], ['xxx µ']) - self.assertEqual(fs['six'], ['seven']) + self.assertEqual(set(fs["test £"]), {"val1", "val2"}) + self.assertEqual(set(fs["one"]), {"two", "three"}) + self.assertEqual(fs["test2"], ["xxx µ"]) + self.assertEqual(fs["six"], ["seven"]) def test_from_response_duplicate_form_key(self): - response = _buildresponse( - '', - url='http://www.example.com') + response = _buildresponse("", url="http://www.example.com") req = self.request_class.from_response( response=response, - method='GET', - formdata=(('foo', 'bar'), ('foo', 'baz')), + method="GET", + formdata=(("foo", "bar"), ("foo", "baz")), ) - self.assertEqual(urlparse(req.url).hostname, 'www.example.com') - self.assertEqual(urlparse(req.url).query, 'foo=bar&foo=baz') + self.assertEqual(urlparse(req.url).hostname, "www.example.com") + self.assertEqual(urlparse(req.url).query, "foo=bar&foo=baz") def test_from_response_override_duplicate_form_key(self): response = _buildresponse( """""") + """ + ) req = self.request_class.from_response( - response, - formdata=(('two', '2'), ('two', '4'))) + response, formdata=(("two", "2"), ("two", "4")) + ) fs = _qs(req) - self.assertEqual(fs[b'one'], [b'1']) - self.assertEqual(fs[b'two'], [b'2', b'4']) + self.assertEqual(fs[b"one"], [b"1"]) + self.assertEqual(fs[b"two"], [b"2", b"4"]) def test_from_response_extra_headers(self): response = _buildresponse( @@ -549,15 +640,18 @@ class FormRequestTest(RequestTest): - """) + """ + ) req = self.request_class.from_response( response=response, - formdata={'one': ['two', 'three'], 'six': 'seven'}, + formdata={"one": ["two", "three"], "six": "seven"}, headers={"Accept-Encoding": "gzip,deflate"}, ) - self.assertEqual(req.method, 'POST') - self.assertEqual(req.headers['Content-type'], b'application/x-www-form-urlencoded') - self.assertEqual(req.headers['Accept-Encoding'], b'gzip,deflate') + self.assertEqual(req.method, "POST") + self.assertEqual( + req.headers["Content-type"], b"application/x-www-form-urlencoded" + ) + self.assertEqual(req.headers["Accept-Encoding"], b"gzip,deflate") def test_from_response_get(self): response = _buildresponse( @@ -566,60 +660,67 @@ class FormRequestTest(RequestTest): """, - url="http://www.example.com/this/list.html") - r1 = self.request_class.from_response(response, formdata={'one': ['two', 'three'], 'six': 'seven'}) - self.assertEqual(r1.method, 'GET') + url="http://www.example.com/this/list.html", + ) + r1 = self.request_class.from_response( + response, formdata={"one": ["two", "three"], "six": "seven"} + ) + self.assertEqual(r1.method, "GET") self.assertEqual(urlparse(r1.url).hostname, "www.example.com") self.assertEqual(urlparse(r1.url).path, "/this/get.php") fs = _qs(r1) - self.assertEqual(set(fs[b'test']), {b'val1', b'val2'}) - self.assertEqual(set(fs[b'one']), {b'two', b'three'}) - self.assertEqual(fs[b'test2'], [b'xxx']) - self.assertEqual(fs[b'six'], [b'seven']) + self.assertEqual(set(fs[b"test"]), {b"val1", b"val2"}) + self.assertEqual(set(fs[b"one"]), {b"two", b"three"}) + self.assertEqual(fs[b"test2"], [b"xxx"]) + self.assertEqual(fs[b"six"], [b"seven"]) def test_from_response_override_params(self): response = _buildresponse( """""") - req = self.request_class.from_response(response, formdata={'two': '2'}) + """ + ) + req = self.request_class.from_response(response, formdata={"two": "2"}) fs = _qs(req) - self.assertEqual(fs[b'one'], [b'1']) - self.assertEqual(fs[b'two'], [b'2']) + self.assertEqual(fs[b"one"], [b"1"]) + self.assertEqual(fs[b"two"], [b"2"]) def test_from_response_drop_params(self): response = _buildresponse( """""") - req = self.request_class.from_response(response, formdata={'two': None}) + """ + ) + req = self.request_class.from_response(response, formdata={"two": None}) fs = _qs(req) - self.assertEqual(fs[b'one'], [b'1']) - self.assertNotIn(b'two', fs) + self.assertEqual(fs[b"one"], [b"1"]) + self.assertNotIn(b"two", fs) def test_from_response_override_method(self): response = _buildresponse( - ''' + """ - ''') + """ + ) request = FormRequest.from_response(response) - self.assertEqual(request.method, 'GET') - request = FormRequest.from_response(response, method='POST') - self.assertEqual(request.method, 'POST') + self.assertEqual(request.method, "GET") + request = FormRequest.from_response(response, method="POST") + self.assertEqual(request.method, "POST") def test_from_response_override_url(self): response = _buildresponse( - ''' + """ - ''') + """ + ) request = FormRequest.from_response(response) - self.assertEqual(request.url, 'http://example.com/app') - request = FormRequest.from_response(response, url='http://foo.bar/absolute') - self.assertEqual(request.url, 'http://foo.bar/absolute') - request = FormRequest.from_response(response, url='/relative') - self.assertEqual(request.url, 'http://example.com/relative') + self.assertEqual(request.url, "http://example.com/app") + request = FormRequest.from_response(response, url="http://foo.bar/absolute") + self.assertEqual(request.url, "http://foo.bar/absolute") + request = FormRequest.from_response(response, url="/relative") + self.assertEqual(request.url, "http://example.com/relative") def test_from_response_case_insensitive(self): response = _buildresponse( @@ -627,12 +728,13 @@ class FormRequestTest(RequestTest): - """) + """ + ) req = self.request_class.from_response(response) fs = _qs(req) - self.assertEqual(fs[b'clickable1'], [b'clicked1']) - self.assertFalse(b'i1' in fs, fs) # xpath in _get_inputs() - self.assertFalse(b'clickable2' in fs, fs) # xpath in _get_clickable() + self.assertEqual(fs[b"clickable1"], [b"clicked1"]) + self.assertFalse(b"i1" in fs, fs) # xpath in _get_inputs() + self.assertFalse(b"clickable2" in fs, fs) # xpath in _get_clickable() def test_from_response_submit_first_clickable(self): response = _buildresponse( @@ -641,13 +743,14 @@ class FormRequestTest(RequestTest): - """) - req = self.request_class.from_response(response, formdata={'two': '2'}) + """ + ) + req = self.request_class.from_response(response, formdata={"two": "2"}) fs = _qs(req) - self.assertEqual(fs[b'clickable1'], [b'clicked1']) - self.assertFalse(b'clickable2' in fs, fs) - self.assertEqual(fs[b'one'], [b'1']) - self.assertEqual(fs[b'two'], [b'2']) + self.assertEqual(fs[b"clickable1"], [b"clicked1"]) + self.assertFalse(b"clickable2" in fs, fs) + self.assertEqual(fs[b"one"], [b"1"]) + self.assertEqual(fs[b"two"], [b"2"]) def test_from_response_submit_not_first_clickable(self): response = _buildresponse( @@ -656,15 +759,16 @@ class FormRequestTest(RequestTest): - """) + """ + ) req = self.request_class.from_response( - response, formdata={'two': '2'}, clickdata={'name': 'clickable2'} + response, formdata={"two": "2"}, clickdata={"name": "clickable2"} ) fs = _qs(req) - self.assertEqual(fs[b'clickable2'], [b'clicked2']) - self.assertFalse(b'clickable1' in fs, fs) - self.assertEqual(fs[b'one'], [b'1']) - self.assertEqual(fs[b'two'], [b'2']) + self.assertEqual(fs[b"clickable2"], [b"clicked2"]) + self.assertFalse(b"clickable1" in fs, fs) + self.assertEqual(fs[b"one"], [b"1"]) + self.assertEqual(fs[b"two"], [b"2"]) def test_from_response_dont_submit_image_as_input(self): response = _buildresponse( @@ -672,10 +776,11 @@ class FormRequestTest(RequestTest): - """) + """ + ) req = self.request_class.from_response(response, dont_click=True) fs = _qs(req) - self.assertEqual(fs, {b'i1': [b'i1v']}) + self.assertEqual(fs, {b"i1": [b"i1v"]}) def test_from_response_dont_submit_reset_as_input(self): response = _buildresponse( @@ -684,20 +789,22 @@ class FormRequestTest(RequestTest): - """) + """ + ) req = self.request_class.from_response(response, dont_click=True) fs = _qs(req) - self.assertEqual(fs, {b'i1': [b'i1v'], b'i2': [b'i2v']}) + self.assertEqual(fs, {b"i1": [b"i1v"], b"i2": [b"i2v"]}) def test_from_response_clickdata_does_not_ignore_image(self): response = _buildresponse( """""") + """ + ) req = self.request_class.from_response(response) fs = _qs(req) - self.assertEqual(fs, {b'i1': [b'i1v'], b'i2': [b'i2v']}) + self.assertEqual(fs, {b"i1": [b"i1v"], b"i2": [b"i2v"]}) def test_from_response_multiple_clickdata(self): response = _buildresponse( @@ -706,14 +813,15 @@ class FormRequestTest(RequestTest): - """) + """ + ) req = self.request_class.from_response( - response, clickdata={'name': 'clickable', 'value': 'clicked2'} + response, clickdata={"name": "clickable", "value": "clicked2"} ) fs = _qs(req) - self.assertEqual(fs[b'clickable'], [b'clicked2']) - self.assertEqual(fs[b'one'], [b'clicked1']) - self.assertEqual(fs[b'two'], [b'clicked2']) + self.assertEqual(fs[b"clickable"], [b"clicked2"]) + self.assertEqual(fs[b"one"], [b"clicked1"]) + self.assertEqual(fs[b"two"], [b"clicked2"]) def test_from_response_unicode_clickdata(self): response = _buildresponse( @@ -722,12 +830,13 @@ class FormRequestTest(RequestTest): - """) + """ + ) req = self.request_class.from_response( - response, clickdata={'name': 'price in \u00a3'} + response, clickdata={"name": "price in \u00a3"} ) fs = _qs(req, to_unicode=True) - self.assertTrue(fs['price in \u00a3']) + self.assertTrue(fs["price in \u00a3"]) def test_from_response_unicode_clickdata_latin1(self): response = _buildresponse( @@ -737,12 +846,13 @@ class FormRequestTest(RequestTest): """, - encoding='latin1') - req = self.request_class.from_response( - response, clickdata={'name': 'price in \u00a5'} + encoding="latin1", ) - fs = _qs(req, to_unicode=True, encoding='latin1') - self.assertTrue(fs['price in \u00a5']) + req = self.request_class.from_response( + response, clickdata={"name": "price in \u00a5"} + ) + fs = _qs(req, to_unicode=True, encoding="latin1") + self.assertTrue(fs["price in \u00a5"]) def test_from_response_multiple_forms_clickdata(self): response = _buildresponse( @@ -754,22 +864,25 @@ class FormRequestTest(RequestTest): - """) + """ + ) req = self.request_class.from_response( - response, formname='form2', clickdata={'name': 'clickable'} + response, formname="form2", clickdata={"name": "clickable"} ) fs = _qs(req) - self.assertEqual(fs[b'clickable'], [b'clicked2']) - self.assertEqual(fs[b'field2'], [b'value2']) - self.assertFalse(b'field1' in fs, fs) + self.assertEqual(fs[b"clickable"], [b"clicked2"]) + self.assertEqual(fs[b"field2"], [b"value2"]) + self.assertFalse(b"field1" in fs, fs) def test_from_response_override_clickable(self): - response = _buildresponse('''''') + response = _buildresponse( + """""" + ) req = self.request_class.from_response( - response, formdata={'clickme': 'two'}, clickdata={'name': 'clickme'} + response, formdata={"clickme": "two"}, clickdata={"name": "clickme"} ) fs = _qs(req) - self.assertEqual(fs[b'clickme'], [b'two']) + self.assertEqual(fs[b"clickme"], [b"two"]) def test_from_response_dont_click(self): response = _buildresponse( @@ -778,11 +891,12 @@ class FormRequestTest(RequestTest): - """) + """ + ) r1 = self.request_class.from_response(response, dont_click=True) fs = _qs(r1) - self.assertFalse(b'clickable1' in fs, fs) - self.assertFalse(b'clickable2' in fs, fs) + self.assertFalse(b"clickable1" in fs, fs) + self.assertFalse(b"clickable2" in fs, fs) def test_from_response_ambiguous_clickdata(self): response = _buildresponse( @@ -792,17 +906,27 @@ class FormRequestTest(RequestTest): - """) - self.assertRaises(ValueError, self.request_class.from_response, - response, clickdata={'type': 'submit'}) + """ + ) + self.assertRaises( + ValueError, + self.request_class.from_response, + response, + clickdata={"type": "submit"}, + ) def test_from_response_non_matching_clickdata(self): response = _buildresponse( """""") - self.assertRaises(ValueError, self.request_class.from_response, - response, clickdata={'nonexistent': 'notme'}) + """ + ) + self.assertRaises( + ValueError, + self.request_class.from_response, + response, + clickdata={"nonexistent": "notme"}, + ) def test_from_response_nr_index_clickdata(self): response = _buildresponse( @@ -810,32 +934,37 @@ class FormRequestTest(RequestTest): - """) - req = self.request_class.from_response(response, clickdata={'nr': 1}) + """ + ) + req = self.request_class.from_response(response, clickdata={"nr": 1}) fs = _qs(req) - self.assertIn(b'clickable2', fs) - self.assertNotIn(b'clickable1', fs) + self.assertIn(b"clickable2", fs) + self.assertNotIn(b"clickable1", fs) def test_from_response_invalid_nr_index_clickdata(self): response = _buildresponse( """ - """) - self.assertRaises(ValueError, self.request_class.from_response, - response, clickdata={'nr': 1}) + """ + ) + self.assertRaises( + ValueError, self.request_class.from_response, response, clickdata={"nr": 1} + ) def test_from_response_errors_noform(self): response = _buildresponse("""""") self.assertRaises(ValueError, self.request_class.from_response, response) def test_from_response_invalid_html5(self): - response = _buildresponse("""""") - req = self.request_class.from_response(response, formdata={'bar': 'buz'}) + response = _buildresponse( + """""" + ) + req = self.request_class.from_response(response, formdata={"bar": "buz"}) fs = _qs(req) - self.assertEqual(fs, {b'foo': [b'xxx'], b'bar': [b'buz']}) + self.assertEqual(fs, {b"foo": [b"xxx"], b"bar": [b"buz"]}) def test_from_response_errors_formnumber(self): response = _buildresponse( @@ -843,20 +972,26 @@ class FormRequestTest(RequestTest): - """) - self.assertRaises(IndexError, self.request_class.from_response, response, formnumber=1) + """ + ) + self.assertRaises( + IndexError, self.request_class.from_response, response, formnumber=1 + ) def test_from_response_noformname(self): response = _buildresponse( """""") - r1 = self.request_class.from_response(response, formdata={'two': '3'}) - self.assertEqual(r1.method, 'POST') - self.assertEqual(r1.headers['Content-type'], b'application/x-www-form-urlencoded') + """ + ) + r1 = self.request_class.from_response(response, formdata={"two": "3"}) + self.assertEqual(r1.method, "POST") + self.assertEqual( + r1.headers["Content-type"], b"application/x-www-form-urlencoded" + ) fs = _qs(r1) - self.assertEqual(fs, {b'one': [b'1'], b'two': [b'3']}) + self.assertEqual(fs, {b"one": [b"1"], b"two": [b"3"]}) def test_from_response_formname_exists(self): response = _buildresponse( @@ -867,24 +1002,26 @@ class FormRequestTest(RequestTest): """) + """ + ) r1 = self.request_class.from_response(response, formname="form2") - self.assertEqual(r1.method, 'POST') + self.assertEqual(r1.method, "POST") fs = _qs(r1) - self.assertEqual(fs, {b'four': [b'4'], b'three': [b'3']}) + self.assertEqual(fs, {b"four": [b"4"], b"three": [b"3"]}) - def test_from_response_formname_notexist(self): + def test_from_response_formname_nonexistent(self): response = _buildresponse( """ """) + """ + ) r1 = self.request_class.from_response(response, formname="form3") - self.assertEqual(r1.method, 'POST') + self.assertEqual(r1.method, "POST") fs = _qs(r1) - self.assertEqual(fs, {b'one': [b'1']}) + self.assertEqual(fs, {b"one": [b"1"]}) def test_from_response_formname_errors_formnumber(self): response = _buildresponse( @@ -893,9 +1030,15 @@ class FormRequestTest(RequestTest): """) - self.assertRaises(IndexError, self.request_class.from_response, - response, formname="form3", formnumber=2) + """ + ) + self.assertRaises( + IndexError, + self.request_class.from_response, + response, + formname="form3", + formnumber=2, + ) def test_from_response_formid_exists(self): response = _buildresponse( @@ -906,13 +1049,14 @@ class FormRequestTest(RequestTest): """) + """ + ) r1 = self.request_class.from_response(response, formid="form2") - self.assertEqual(r1.method, 'POST') + self.assertEqual(r1.method, "POST") fs = _qs(r1) - self.assertEqual(fs, {b'four': [b'4'], b'three': [b'3']}) + self.assertEqual(fs, {b"four": [b"4"], b"three": [b"3"]}) - def test_from_response_formname_notexists_fallback_formid(self): + def test_from_response_formname_nonexistent_fallback_formid(self): response = _buildresponse( """""") - r1 = self.request_class.from_response(response, formname="form3", formid="form2") - self.assertEqual(r1.method, 'POST') + """ + ) + r1 = self.request_class.from_response( + response, formname="form3", formid="form2" + ) + self.assertEqual(r1.method, "POST") fs = _qs(r1) - self.assertEqual(fs, {b'four': [b'4'], b'three': [b'3']}) + self.assertEqual(fs, {b"four": [b"4"], b"three": [b"3"]}) - def test_from_response_formid_notexist(self): + def test_from_response_formid_nonexistent(self): response = _buildresponse( """ """) + """ + ) r1 = self.request_class.from_response(response, formid="form3") - self.assertEqual(r1.method, 'POST') + self.assertEqual(r1.method, "POST") fs = _qs(r1) - self.assertEqual(fs, {b'one': [b'1']}) + self.assertEqual(fs, {b"one": [b"1"]}) def test_from_response_formid_errors_formnumber(self): response = _buildresponse( @@ -947,13 +1095,19 @@ class FormRequestTest(RequestTest): """) - self.assertRaises(IndexError, self.request_class.from_response, - response, formid="form3", formnumber=2) + """ + ) + self.assertRaises( + IndexError, + self.request_class.from_response, + response, + formid="form3", + formnumber=2, + ) def test_from_response_select(self): res = _buildresponse( - '''''') + """ + ) req = self.request_class.from_response(res) fs = _qs(req, to_unicode=True) - self.assertEqual(fs, {'i1': ['i1v2'], 'i2': ['i2v1'], 'i4': ['i4v2', 'i4v3']}) + self.assertEqual(fs, {"i1": ["i1v2"], "i2": ["i2v1"], "i4": ["i4v2", "i4v3"]}) def test_from_response_radio(self): res = _buildresponse( - '''''') + """ + ) req = self.request_class.from_response(res) fs = _qs(req) - self.assertEqual(fs, {b'i1': [b'iv2'], b'i2': [b'on']}) + self.assertEqual(fs, {b"i1": [b"iv2"], b"i2": [b"on"]}) def test_from_response_checkbox(self): res = _buildresponse( - '''''') + """ + ) req = self.request_class.from_response(res) fs = _qs(req) - self.assertEqual(fs, {b'i1': [b'iv2'], b'i2': [b'on']}) + self.assertEqual(fs, {b"i1": [b"iv2"], b"i2": [b"on"]}) def test_from_response_input_text(self): res = _buildresponse( - '''''') + """ + ) req = self.request_class.from_response(res) fs = _qs(req) - self.assertEqual(fs, {b'i1': [b'i1v1'], b'i2': [b''], b'i4': [b'i4v1']}) + self.assertEqual(fs, {b"i1": [b"i1v1"], b"i2": [b""], b"i4": [b"i4v1"]}) def test_from_response_input_hidden(self): res = _buildresponse( - '''''') + """ + ) req = self.request_class.from_response(res) fs = _qs(req) - self.assertEqual(fs, {b'i1': [b'i1v1'], b'i2': [b'']}) + self.assertEqual(fs, {b"i1": [b"i1v1"], b"i2": [b""]}) def test_from_response_input_textarea(self): res = _buildresponse( - '''''') + """ + ) req = self.request_class.from_response(res) fs = _qs(req) - self.assertEqual(fs, {b'i1': [b'i1v'], b'i2': [b''], b'i3': [b'']}) + self.assertEqual(fs, {b"i1": [b"i1v"], b"i2": [b""], b"i3": [b""]}) def test_from_response_descendants(self): res = _buildresponse( - '''''') + """ + ) req = self.request_class.from_response(res) fs = _qs(req) - self.assertEqual(set(fs), {b'h2', b'i2', b'i1', b'i3', b'h1', b'i5', b'i4'}) + self.assertEqual(set(fs), {b"h2", b"i2", b"i1", b"i3", b"h1", b"i5", b"i4"}) def test_from_response_xpath(self): response = _buildresponse( @@ -1081,28 +1242,43 @@ class FormRequestTest(RequestTest): """) - r1 = self.request_class.from_response(response, formxpath="//form[@action='post.php']") + """ + ) + r1 = self.request_class.from_response( + response, formxpath="//form[@action='post.php']" + ) fs = _qs(r1) - self.assertEqual(fs[b'one'], [b'1']) + self.assertEqual(fs[b"one"], [b"1"]) - r1 = self.request_class.from_response(response, formxpath="//form/input[@name='four']") + r1 = self.request_class.from_response( + response, formxpath="//form/input[@name='four']" + ) fs = _qs(r1) - self.assertEqual(fs[b'three'], [b'3']) + self.assertEqual(fs[b"three"], [b"3"]) - self.assertRaises(ValueError, self.request_class.from_response, - response, formxpath="//form/input[@name='abc']") + self.assertRaises( + ValueError, + self.request_class.from_response, + response, + formxpath="//form/input[@name='abc']", + ) def test_from_response_unicode_xpath(self): response = _buildresponse(b'') - r = self.request_class.from_response(response, formxpath="//form[@name='\u044a']") + r = self.request_class.from_response( + response, formxpath="//form[@name='\u044a']" + ) fs = _qs(r) self.assertEqual(fs, {}) xpath = "//form[@name='\u03b1']" - self.assertRaisesRegex(ValueError, re.escape(xpath), - self.request_class.from_response, - response, formxpath=xpath) + self.assertRaisesRegex( + ValueError, + re.escape(xpath), + self.request_class.from_response, + response, + formxpath=xpath, + ) def test_from_response_button_submit(self): response = _buildresponse( @@ -1111,15 +1287,18 @@ class FormRequestTest(RequestTest): """, - url="http://www.example.com/this/list.html") + url="http://www.example.com/this/list.html", + ) req = self.request_class.from_response(response) - self.assertEqual(req.method, 'POST') - self.assertEqual(req.headers['Content-type'], b'application/x-www-form-urlencoded') + self.assertEqual(req.method, "POST") + self.assertEqual( + req.headers["Content-type"], b"application/x-www-form-urlencoded" + ) self.assertEqual(req.url, "http://www.example.com/this/post.php") fs = _qs(req) - self.assertEqual(fs[b'test1'], [b'val1']) - self.assertEqual(fs[b'test2'], [b'val2']) - self.assertEqual(fs[b'button1'], [b'submit1']) + self.assertEqual(fs[b"test1"], [b"val1"]) + self.assertEqual(fs[b"test2"], [b"val2"]) + self.assertEqual(fs[b"button1"], [b"submit1"]) def test_from_response_button_notype(self): response = _buildresponse( @@ -1128,15 +1307,18 @@ class FormRequestTest(RequestTest): """, - url="http://www.example.com/this/list.html") + url="http://www.example.com/this/list.html", + ) req = self.request_class.from_response(response) - self.assertEqual(req.method, 'POST') - self.assertEqual(req.headers['Content-type'], b'application/x-www-form-urlencoded') + self.assertEqual(req.method, "POST") + self.assertEqual( + req.headers["Content-type"], b"application/x-www-form-urlencoded" + ) self.assertEqual(req.url, "http://www.example.com/this/post.php") fs = _qs(req) - self.assertEqual(fs[b'test1'], [b'val1']) - self.assertEqual(fs[b'test2'], [b'val2']) - self.assertEqual(fs[b'button1'], [b'submit1']) + self.assertEqual(fs[b"test1"], [b"val1"]) + self.assertEqual(fs[b"test2"], [b"val2"]) + self.assertEqual(fs[b"button1"], [b"submit1"]) def test_from_response_submit_novalue(self): response = _buildresponse( @@ -1145,15 +1327,18 @@ class FormRequestTest(RequestTest): Submit """, - url="http://www.example.com/this/list.html") + url="http://www.example.com/this/list.html", + ) req = self.request_class.from_response(response) - self.assertEqual(req.method, 'POST') - self.assertEqual(req.headers['Content-type'], b'application/x-www-form-urlencoded') + self.assertEqual(req.method, "POST") + self.assertEqual( + req.headers["Content-type"], b"application/x-www-form-urlencoded" + ) self.assertEqual(req.url, "http://www.example.com/this/post.php") fs = _qs(req) - self.assertEqual(fs[b'test1'], [b'val1']) - self.assertEqual(fs[b'test2'], [b'val2']) - self.assertEqual(fs[b'button1'], [b'']) + self.assertEqual(fs[b"test1"], [b"val1"]) + self.assertEqual(fs[b"test2"], [b"val2"]) + self.assertEqual(fs[b"button1"], [b""]) def test_from_response_button_novalue(self): response = _buildresponse( @@ -1162,15 +1347,18 @@ class FormRequestTest(RequestTest): """, - url="http://www.example.com/this/list.html") + url="http://www.example.com/this/list.html", + ) req = self.request_class.from_response(response) - self.assertEqual(req.method, 'POST') - self.assertEqual(req.headers['Content-type'], b'application/x-www-form-urlencoded') + self.assertEqual(req.method, "POST") + self.assertEqual( + req.headers["Content-type"], b"application/x-www-form-urlencoded" + ) self.assertEqual(req.url, "http://www.example.com/this/post.php") fs = _qs(req) - self.assertEqual(fs[b'test1'], [b'val1']) - self.assertEqual(fs[b'test2'], [b'val2']) - self.assertEqual(fs[b'button1'], [b'']) + self.assertEqual(fs[b"test1"], [b"val1"]) + self.assertEqual(fs[b"test2"], [b"val2"]) + self.assertEqual(fs[b"button1"], [b""]) def test_html_base_form_action(self): response = _buildresponse( @@ -1185,15 +1373,15 @@ class FormRequestTest(RequestTest): """, - url='http://a.com/' + url="http://a.com/", ) req = self.request_class.from_response(response) - self.assertEqual(req.url, 'http://b.com/test_form') + self.assertEqual(req.url, "http://b.com/test_form") def test_spaces_in_action(self): resp = _buildresponse('') req = self.request_class.from_response(resp) - self.assertEqual(req.url, 'http://example.com/path') + self.assertEqual(req.url, "http://example.com/path") def test_from_response_css(self): response = _buildresponse( @@ -1204,44 +1392,53 @@ class FormRequestTest(RequestTest): """) - r1 = self.request_class.from_response(response, formcss="form[action='post.php']") + """ + ) + r1 = self.request_class.from_response( + response, formcss="form[action='post.php']" + ) fs = _qs(r1) - self.assertEqual(fs[b'one'], [b'1']) + self.assertEqual(fs[b"one"], [b"1"]) r1 = self.request_class.from_response(response, formcss="input[name='four']") fs = _qs(r1) - self.assertEqual(fs[b'three'], [b'3']) + self.assertEqual(fs[b"three"], [b"3"]) - self.assertRaises(ValueError, self.request_class.from_response, - response, formcss="input[name='abc']") + self.assertRaises( + ValueError, + self.request_class.from_response, + response, + formcss="input[name='abc']", + ) def test_from_response_valid_form_methods(self): - form_methods = [[method, method] for method in self.request_class.valid_form_methods] - form_methods.append(['UNKNOWN', 'GET']) + form_methods = [ + [method, method] for method in self.request_class.valid_form_methods + ] + form_methods.append(["UNKNOWN", "GET"]) for method, expected in form_methods: response = _buildresponse( f'' + "" ) r = self.request_class.from_response(response) self.assertEqual(r.method, expected) def _buildresponse(body, **kwargs): - kwargs.setdefault('body', body) - kwargs.setdefault('url', 'http://example.com') - kwargs.setdefault('encoding', 'utf-8') + kwargs.setdefault("body", body) + kwargs.setdefault("url", "http://example.com") + kwargs.setdefault("encoding", "utf-8") return HtmlResponse(**kwargs) -def _qs(req, encoding='utf-8', to_unicode=False): - if req.method == 'POST': +def _qs(req, encoding="utf-8", to_unicode=False): + if req.method == "POST": qs = req.body else: - qs = req.url.partition('?')[2] + qs = req.url.partition("?")[2] uqs = unquote_to_bytes(qs) if to_unicode: uqs = uqs.decode(encoding) @@ -1249,40 +1446,42 @@ def _qs(req, encoding='utf-8', to_unicode=False): class XmlRpcRequestTest(RequestTest): - request_class = XmlRpcRequest - default_method = 'POST' - default_headers = {b'Content-Type': [b'text/xml']} + default_method = "POST" + default_headers = {b"Content-Type": [b"text/xml"]} def _test_request(self, **kwargs): - r = self.request_class('http://scrapytest.org/rpc2', **kwargs) - self.assertEqual(r.headers[b'Content-Type'], b'text/xml') - self.assertEqual(r.body, - to_bytes(xmlrpc.client.dumps(**kwargs), - encoding=kwargs.get('encoding', 'utf-8'))) - self.assertEqual(r.method, 'POST') - self.assertEqual(r.encoding, kwargs.get('encoding', 'utf-8')) + r = self.request_class("http://scrapytest.org/rpc2", **kwargs) + self.assertEqual(r.headers[b"Content-Type"], b"text/xml") + self.assertEqual( + r.body, + to_bytes( + xmlrpc.client.dumps(**kwargs), encoding=kwargs.get("encoding", "utf-8") + ), + ) + self.assertEqual(r.method, "POST") + self.assertEqual(r.encoding, kwargs.get("encoding", "utf-8")) self.assertTrue(r.dont_filter, True) def test_xmlrpc_dumps(self): - self._test_request(params=('value',)) - self._test_request(params=('username', 'password'), methodname='login') - self._test_request(params=('response', ), methodresponse='login') - self._test_request(params=('pas£',), encoding='utf-8') + self._test_request(params=("value",)) + self._test_request(params=("username", "password"), methodname="login") + self._test_request(params=("response",), methodresponse="login") + self._test_request(params=("pas£",), encoding="utf-8") self._test_request(params=(None,), allow_none=1) self.assertRaises(TypeError, self._test_request) self.assertRaises(TypeError, self._test_request, params=(None,)) def test_latin1(self): - self._test_request(params=('pas£',), encoding='latin1') + self._test_request(params=("pas£",), encoding="latin1") class JsonRequestTest(RequestTest): request_class = JsonRequest - default_method = 'GET' + default_method = "GET" default_headers = { - b'Content-Type': [b'application/json'], - b'Accept': [b'application/json, text/javascript, */*; q=0.01'], + b"Content-Type": [b"application/json"], + b"Accept": [b"application/json, text/javascript, */*; q=0.01"], } def setUp(self): @@ -1291,14 +1490,14 @@ class JsonRequestTest(RequestTest): def test_data(self): r1 = self.request_class(url="http://www.example.com/") - self.assertEqual(r1.body, b'') + self.assertEqual(r1.body, b"") - body = b'body' + body = b"body" r2 = self.request_class(url="http://www.example.com/", body=body) self.assertEqual(r2.body, body) data = { - 'name': 'value', + "name": "value", } r3 = self.request_class(url="http://www.example.com/", data=data) self.assertEqual(r3.body, to_bytes(json.dumps(data))) @@ -1310,134 +1509,138 @@ class JsonRequestTest(RequestTest): def test_data_method(self): # data is not passed r1 = self.request_class(url="http://www.example.com/") - self.assertEqual(r1.method, 'GET') + self.assertEqual(r1.method, "GET") - body = b'body' + body = b"body" r2 = self.request_class(url="http://www.example.com/", body=body) - self.assertEqual(r2.method, 'GET') + self.assertEqual(r2.method, "GET") data = { - 'name': 'value', + "name": "value", } r3 = self.request_class(url="http://www.example.com/", data=data) - self.assertEqual(r3.method, 'POST') + self.assertEqual(r3.method, "POST") # method passed explicitly - r4 = self.request_class(url="http://www.example.com/", data=data, method='GET') - self.assertEqual(r4.method, 'GET') + r4 = self.request_class(url="http://www.example.com/", data=data, method="GET") + self.assertEqual(r4.method, "GET") r5 = self.request_class(url="http://www.example.com/", data=[]) - self.assertEqual(r5.method, 'POST') + self.assertEqual(r5.method, "POST") def test_body_data(self): - """ passing both body and data should result a warning """ - body = b'body' + """passing both body and data should result a warning""" + body = b"body" data = { - 'name': 'value', + "name": "value", } with warnings.catch_warnings(record=True) as _warnings: r5 = self.request_class(url="http://www.example.com/", body=body, data=data) self.assertEqual(r5.body, body) - self.assertEqual(r5.method, 'GET') + self.assertEqual(r5.method, "GET") self.assertEqual(len(_warnings), 1) - self.assertIn('data will be ignored', str(_warnings[0].message)) + self.assertIn("data will be ignored", str(_warnings[0].message)) def test_empty_body_data(self): - """ passing any body value and data should result a warning """ + """passing any body value and data should result a warning""" data = { - 'name': 'value', + "name": "value", } with warnings.catch_warnings(record=True) as _warnings: - r6 = self.request_class(url="http://www.example.com/", body=b'', data=data) - self.assertEqual(r6.body, b'') - self.assertEqual(r6.method, 'GET') + r6 = self.request_class(url="http://www.example.com/", body=b"", data=data) + self.assertEqual(r6.body, b"") + self.assertEqual(r6.method, "GET") self.assertEqual(len(_warnings), 1) - self.assertIn('data will be ignored', str(_warnings[0].message)) + self.assertIn("data will be ignored", str(_warnings[0].message)) def test_body_none_data(self): data = { - 'name': 'value', + "name": "value", } with warnings.catch_warnings(record=True) as _warnings: r7 = self.request_class(url="http://www.example.com/", body=None, data=data) self.assertEqual(r7.body, to_bytes(json.dumps(data))) - self.assertEqual(r7.method, 'POST') + self.assertEqual(r7.method, "POST") self.assertEqual(len(_warnings), 0) def test_body_data_none(self): with warnings.catch_warnings(record=True) as _warnings: r8 = self.request_class(url="http://www.example.com/", body=None, data=None) - self.assertEqual(r8.method, 'GET') + self.assertEqual(r8.method, "GET") self.assertEqual(len(_warnings), 0) def test_dumps_sort_keys(self): - """ Test that sort_keys=True is passed to json.dumps by default """ + """Test that sort_keys=True is passed to json.dumps by default""" data = { - 'name': 'value', + "name": "value", } - with mock.patch('json.dumps', return_value=b'') as mock_dumps: + with mock.patch("json.dumps", return_value=b"") as mock_dumps: self.request_class(url="http://www.example.com/", data=data) kwargs = mock_dumps.call_args[1] - self.assertEqual(kwargs['sort_keys'], True) + self.assertEqual(kwargs["sort_keys"], True) def test_dumps_kwargs(self): - """ Test that dumps_kwargs are passed to json.dumps """ + """Test that dumps_kwargs are passed to json.dumps""" data = { - 'name': 'value', + "name": "value", } dumps_kwargs = { - 'ensure_ascii': True, - 'allow_nan': True, + "ensure_ascii": True, + "allow_nan": True, } - with mock.patch('json.dumps', return_value=b'') as mock_dumps: - self.request_class(url="http://www.example.com/", data=data, dumps_kwargs=dumps_kwargs) + with mock.patch("json.dumps", return_value=b"") as mock_dumps: + self.request_class( + url="http://www.example.com/", data=data, dumps_kwargs=dumps_kwargs + ) kwargs = mock_dumps.call_args[1] - self.assertEqual(kwargs['ensure_ascii'], True) - self.assertEqual(kwargs['allow_nan'], True) + self.assertEqual(kwargs["ensure_ascii"], True) + self.assertEqual(kwargs["allow_nan"], True) def test_replace_data(self): data1 = { - 'name1': 'value1', + "name1": "value1", } data2 = { - 'name2': 'value2', + "name2": "value2", } r1 = self.request_class(url="http://www.example.com/", data=data1) r2 = r1.replace(data=data2) self.assertEqual(r2.body, to_bytes(json.dumps(data2))) def test_replace_sort_keys(self): - """ Test that replace provides sort_keys=True to json.dumps """ + """Test that replace provides sort_keys=True to json.dumps""" data1 = { - 'name1': 'value1', + "name1": "value1", } data2 = { - 'name2': 'value2', + "name2": "value2", } r1 = self.request_class(url="http://www.example.com/", data=data1) - with mock.patch('json.dumps', return_value=b'') as mock_dumps: + with mock.patch("json.dumps", return_value=b"") as mock_dumps: r1.replace(data=data2) kwargs = mock_dumps.call_args[1] - self.assertEqual(kwargs['sort_keys'], True) + self.assertEqual(kwargs["sort_keys"], True) def test_replace_dumps_kwargs(self): - """ Test that dumps_kwargs are provided to json.dumps when replace is called """ + """Test that dumps_kwargs are provided to json.dumps when replace is called""" data1 = { - 'name1': 'value1', + "name1": "value1", } data2 = { - 'name2': 'value2', + "name2": "value2", } dumps_kwargs = { - 'ensure_ascii': True, - 'allow_nan': True, + "ensure_ascii": True, + "allow_nan": True, } - r1 = self.request_class(url="http://www.example.com/", data=data1, dumps_kwargs=dumps_kwargs) - with mock.patch('json.dumps', return_value=b'') as mock_dumps: + r1 = self.request_class( + url="http://www.example.com/", data=data1, dumps_kwargs=dumps_kwargs + ) + with mock.patch("json.dumps", return_value=b"") as mock_dumps: r1.replace(data=data2) kwargs = mock_dumps.call_args[1] - self.assertEqual(kwargs['ensure_ascii'], True) - self.assertEqual(kwargs['allow_nan'], True) + self.assertEqual(kwargs["ensure_ascii"], True) + self.assertEqual(kwargs["allow_nan"], True) def tearDown(self): warnings.resetwarnings() diff --git a/tests/test_http_response.py b/tests/test_http_response.py index 80c0279cc..bfa08136f 100644 --- a/tests/test_http_response.py +++ b/tests/test_http_response.py @@ -8,8 +8,14 @@ from w3lib import __version__ as w3lib_version from w3lib.encoding import resolve_encoding from scrapy.exceptions import NotSupported -from scrapy.http import (Headers, HtmlResponse, Request, Response, - TextResponse, XmlResponse) +from scrapy.http import ( + Headers, + HtmlResponse, + Request, + Response, + TextResponse, + XmlResponse, +) from scrapy.link import Link from scrapy.selector import Selector from scrapy.utils.python import to_unicode @@ -17,21 +23,38 @@ from tests import get_testdata class BaseResponseTest(unittest.TestCase): - response_class = Response def test_init(self): # Response requires url in the constructor self.assertRaises(Exception, self.response_class) - self.assertTrue(isinstance(self.response_class('http://example.com/'), self.response_class)) + self.assertTrue( + isinstance(self.response_class("http://example.com/"), self.response_class) + ) self.assertRaises(TypeError, self.response_class, b"http://example.com") + self.assertRaises( + TypeError, self.response_class, url="http://example.com", body={} + ) # body can be str or None - self.assertTrue(isinstance(self.response_class('http://example.com/', body=b''), self.response_class)) - self.assertTrue(isinstance(self.response_class('http://example.com/', body=b'body'), self.response_class)) + self.assertTrue( + isinstance( + self.response_class("http://example.com/", body=b""), + self.response_class, + ) + ) + self.assertTrue( + isinstance( + self.response_class("http://example.com/", body=b"body"), + self.response_class, + ) + ) # test presence of all optional parameters self.assertTrue( isinstance( - self.response_class('http://example.com/', body=b'', headers={}, status=200), self.response_class + self.response_class( + "http://example.com/", body=b"", headers={}, status=200 + ), + self.response_class, ) ) @@ -52,15 +75,20 @@ class BaseResponseTest(unittest.TestCase): r = self.response_class("http://www.example.com", status=301) self.assertEqual(r.status, 301) - r = self.response_class("http://www.example.com", status='301') + r = self.response_class("http://www.example.com", status="301") self.assertEqual(r.status, 301) - self.assertRaises(ValueError, self.response_class, "http://example.com", status='lala200') + self.assertRaises( + ValueError, + self.response_class, + "http://example.com", + status="lala200", + ) def test_copy(self): """Test Response copy""" r1 = self.response_class("http://www.example.com", body=b"Some body") - r1.flags.append('cached') + r1.flags.append("cached") r2 = r1.copy() self.assertEqual(r1.status, r2.status) @@ -71,29 +99,37 @@ class BaseResponseTest(unittest.TestCase): self.assertEqual(r1.flags, r2.flags) # make sure headers attribute is shallow copied - assert r1.headers is not r2.headers, "headers must be a shallow copy, not identical" + assert ( + r1.headers is not r2.headers + ), "headers must be a shallow copy, not identical" self.assertEqual(r1.headers, r2.headers) def test_copy_meta(self): req = Request("http://www.example.com") - req.meta['foo'] = 'bar' - r1 = self.response_class("http://www.example.com", body=b"Some body", request=req) + req.meta["foo"] = "bar" + r1 = self.response_class( + "http://www.example.com", body=b"Some body", request=req + ) assert r1.meta is req.meta def test_copy_cb_kwargs(self): req = Request("http://www.example.com") - req.cb_kwargs['foo'] = 'bar' - r1 = self.response_class("http://www.example.com", body=b"Some body", request=req) + req.cb_kwargs["foo"] = "bar" + r1 = self.response_class( + "http://www.example.com", body=b"Some body", request=req + ) assert r1.cb_kwargs is req.cb_kwargs def test_unavailable_meta(self): r1 = self.response_class("http://www.example.com", body=b"Some body") - with self.assertRaisesRegex(AttributeError, r'Response\.meta not available'): + with self.assertRaisesRegex(AttributeError, r"Response\.meta not available"): r1.meta def test_unavailable_cb_kwargs(self): r1 = self.response_class("http://www.example.com", body=b"Some body") - with self.assertRaisesRegex(AttributeError, r'Response\.cb_kwargs not available'): + with self.assertRaisesRegex( + AttributeError, r"Response\.cb_kwargs not available" + ): r1.cb_kwargs def test_copy_inherited_classes(self): @@ -102,7 +138,7 @@ class BaseResponseTest(unittest.TestCase): class CustomResponse(self.response_class): pass - r1 = CustomResponse('http://www.example.com') + r1 = CustomResponse("http://www.example.com") r2 = r1.copy() assert isinstance(r2, CustomResponse) @@ -112,16 +148,16 @@ class BaseResponseTest(unittest.TestCase): hdrs = Headers({"key": "value"}) r1 = self.response_class("http://www.example.com") r2 = r1.replace(status=301, body=b"New body", headers=hdrs) - assert r1.body == b'' + assert r1.body == b"" self.assertEqual(r1.url, r2.url) self.assertEqual((r1.status, r2.status), (200, 301)) - self.assertEqual((r1.body, r2.body), (b'', b"New body")) + self.assertEqual((r1.body, r2.body), (b"", b"New body")) self.assertEqual((r1.headers, r2.headers), ({}, hdrs)) # Empty attributes (which may fail if not compared properly) - r3 = self.response_class("http://www.example.com", flags=['cached']) - r4 = r3.replace(body=b'', flags=[]) - self.assertEqual(r4.body, b'') + r3 = self.response_class("http://www.example.com", flags=["cached"]) + r4 = r3.replace(body=b"", flags=[]) + self.assertEqual(r4.body, b"") self.assertEqual(r4.flags, []) def _assert_response_values(self, response, encoding, body): @@ -143,40 +179,40 @@ class BaseResponseTest(unittest.TestCase): def test_immutable_attributes(self): r = self.response_class("http://example.com") - self.assertRaises(AttributeError, setattr, r, 'url', 'http://example2.com') - self.assertRaises(AttributeError, setattr, r, 'body', 'xxx') + self.assertRaises(AttributeError, setattr, r, "url", "http://example2.com") + self.assertRaises(AttributeError, setattr, r, "body", "xxx") def test_urljoin(self): """Test urljoin shortcut (only for existence, since behavior equals urljoin)""" - joined = self.response_class('http://www.example.com').urljoin('/test') - absolute = 'http://www.example.com/test' + joined = self.response_class("http://www.example.com").urljoin("/test") + absolute = "http://www.example.com/test" self.assertEqual(joined, absolute) def test_shortcut_attributes(self): - r = self.response_class("http://example.com", body=b'hello') + r = self.response_class("http://example.com", body=b"hello") if self.response_class == Response: msg = "Response content isn't text" - self.assertRaisesRegex(AttributeError, msg, getattr, r, 'text') - self.assertRaisesRegex(NotSupported, msg, r.css, 'body') - self.assertRaisesRegex(NotSupported, msg, r.xpath, '//body') + self.assertRaisesRegex(AttributeError, msg, getattr, r, "text") + self.assertRaisesRegex(NotSupported, msg, r.css, "body") + self.assertRaisesRegex(NotSupported, msg, r.xpath, "//body") + self.assertRaisesRegex(NotSupported, msg, r.jmespath, "body") else: r.text - r.css('body') - r.xpath('//body') + r.css("body") + r.xpath("//body") # Response.follow def test_follow_url_absolute(self): - self._assert_followed_url('http://foo.example.com', - 'http://foo.example.com') + self._assert_followed_url("http://foo.example.com", "http://foo.example.com") def test_follow_url_relative(self): - self._assert_followed_url('foo', - 'http://example.com/foo') + self._assert_followed_url("foo", "http://example.com/foo") def test_follow_link(self): - self._assert_followed_url(Link('http://example.com/foo'), - 'http://example.com/foo') + self._assert_followed_url( + Link("http://example.com/foo"), "http://example.com/foo" + ) def test_follow_None_url(self): r = self.response_class("http://example.com") @@ -188,8 +224,7 @@ class BaseResponseTest(unittest.TestCase): strict=True, ) def test_follow_whitespace_url(self): - self._assert_followed_url('foo ', - 'http://example.com/foo') + self._assert_followed_url("foo ", "http://example.com/foo") @mark.xfail( parse_version(w3lib_version) < parse_version("2.1.1"), @@ -197,37 +232,42 @@ class BaseResponseTest(unittest.TestCase): strict=True, ) def test_follow_whitespace_link(self): - self._assert_followed_url(Link('http://example.com/foo '), - 'http://example.com/foo') + self._assert_followed_url( + Link("http://example.com/foo "), "http://example.com/foo" + ) def test_follow_flags(self): - res = self.response_class('http://example.com/') - fol = res.follow('http://example.com/', flags=['cached', 'allowed']) - self.assertEqual(fol.flags, ['cached', 'allowed']) + res = self.response_class("http://example.com/") + fol = res.follow("http://example.com/", flags=["cached", "allowed"]) + self.assertEqual(fol.flags, ["cached", "allowed"]) # Response.follow_all def test_follow_all_absolute(self): - url_list = ['http://example.org', 'http://www.example.org', - 'http://example.com', 'http://www.example.com'] + url_list = [ + "http://example.org", + "http://www.example.org", + "http://example.com", + "http://www.example.com", + ] self._assert_followed_all_urls(url_list, url_list) def test_follow_all_relative(self): - relative = ['foo', 'bar', 'foo/bar', 'bar/foo'] + relative = ["foo", "bar", "foo/bar", "bar/foo"] absolute = [ - 'http://example.com/foo', - 'http://example.com/bar', - 'http://example.com/foo/bar', - 'http://example.com/bar/foo', + "http://example.com/foo", + "http://example.com/bar", + "http://example.com/foo/bar", + "http://example.com/bar/foo", ] self._assert_followed_all_urls(relative, absolute) def test_follow_all_links(self): absolute = [ - 'http://example.com/foo', - 'http://example.com/bar', - 'http://example.com/foo/bar', - 'http://example.com/bar/foo', + "http://example.com/foo", + "http://example.com/bar", + "http://example.com/foo/bar", + "http://example.com/bar/foo", ] links = map(Link, absolute) self._assert_followed_all_urls(links, absolute) @@ -254,36 +294,36 @@ class BaseResponseTest(unittest.TestCase): list(r.follow_all(urls=[None])) def test_follow_all_whitespace(self): - relative = ['foo ', 'bar ', 'foo/bar ', 'bar/foo '] + relative = ["foo ", "bar ", "foo/bar ", "bar/foo "] absolute = [ - 'http://example.com/foo%20', - 'http://example.com/bar%20', - 'http://example.com/foo/bar%20', - 'http://example.com/bar/foo%20', + "http://example.com/foo%20", + "http://example.com/bar%20", + "http://example.com/foo/bar%20", + "http://example.com/bar/foo%20", ] self._assert_followed_all_urls(relative, absolute) def test_follow_all_whitespace_links(self): absolute = [ - 'http://example.com/foo ', - 'http://example.com/bar ', - 'http://example.com/foo/bar ', - 'http://example.com/bar/foo ', + "http://example.com/foo ", + "http://example.com/bar ", + "http://example.com/foo/bar ", + "http://example.com/bar/foo ", ] links = map(Link, absolute) - expected = [u.replace(' ', '%20') for u in absolute] + expected = [u.replace(" ", "%20") for u in absolute] self._assert_followed_all_urls(links, expected) def test_follow_all_flags(self): - re = self.response_class('http://www.example.com/') + re = self.response_class("http://www.example.com/") urls = [ - 'http://www.example.com/', - 'http://www.example.com/2', - 'http://www.example.com/foo', + "http://www.example.com/", + "http://www.example.com/2", + "http://www.example.com/foo", ] - fol = re.follow_all(urls, flags=['cached', 'allowed']) + fol = re.follow_all(urls, flags=["cached", "allowed"]) for req in fol: - self.assertEqual(req.flags, ['cached', 'allowed']) + self.assertEqual(req.flags, ["cached", "allowed"]) def _assert_followed_url(self, follow_obj, target_url, response=None): if response is None: @@ -301,23 +341,24 @@ class BaseResponseTest(unittest.TestCase): yield req def _links_response(self): - body = get_testdata('link_extractor', 'linkextractor.html') - resp = self.response_class('http://example.com/index', body=body) + body = get_testdata("link_extractor", "linkextractor.html") + resp = self.response_class("http://example.com/index", body=body) return resp def _links_response_no_href(self): - body = get_testdata('link_extractor', 'linkextractor_no_href.html') - resp = self.response_class('http://example.com/index', body=body) + body = get_testdata("link_extractor", "linkextractor_no_href.html") + resp = self.response_class("http://example.com/index", body=body) return resp class TextResponseTest(BaseResponseTest): - response_class = TextResponse def test_replace(self): super().test_replace() - r1 = self.response_class("http://www.example.com", body="hello", encoding="cp852") + r1 = self.response_class( + "http://www.example.com", body="hello", encoding="cp852" + ) r2 = r1.replace(url="http://www.example.com/other") r3 = r1.replace(url="http://www.example.com/other", encoding="latin1") @@ -333,135 +374,208 @@ class TextResponseTest(BaseResponseTest): self._assert_response_encoding(resp, self.response_class._DEFAULT_ENCODING) # make sure urls are converted to str - resp = self.response_class(url="http://www.example.com/", encoding='utf-8') + resp = self.response_class(url="http://www.example.com/", encoding="utf-8") assert isinstance(resp.url, str) - resp = self.response_class(url="http://www.example.com/price/\xa3", encoding='utf-8') - self.assertEqual(resp.url, to_unicode(b'http://www.example.com/price/\xc2\xa3')) - resp = self.response_class(url="http://www.example.com/price/\xa3", encoding='latin-1') - self.assertEqual(resp.url, 'http://www.example.com/price/\xa3') - resp = self.response_class("http://www.example.com/price/\xa3", - headers={"Content-type": ["text/html; charset=utf-8"]}) - self.assertEqual(resp.url, to_unicode(b'http://www.example.com/price/\xc2\xa3')) - resp = self.response_class("http://www.example.com/price/\xa3", - headers={"Content-type": ["text/html; charset=iso-8859-1"]}) - self.assertEqual(resp.url, 'http://www.example.com/price/\xa3') + resp = self.response_class( + url="http://www.example.com/price/\xa3", encoding="utf-8" + ) + self.assertEqual(resp.url, to_unicode(b"http://www.example.com/price/\xc2\xa3")) + resp = self.response_class( + url="http://www.example.com/price/\xa3", encoding="latin-1" + ) + self.assertEqual(resp.url, "http://www.example.com/price/\xa3") + resp = self.response_class( + "http://www.example.com/price/\xa3", + headers={"Content-type": ["text/html; charset=utf-8"]}, + ) + self.assertEqual(resp.url, to_unicode(b"http://www.example.com/price/\xc2\xa3")) + resp = self.response_class( + "http://www.example.com/price/\xa3", + headers={"Content-type": ["text/html; charset=iso-8859-1"]}, + ) + self.assertEqual(resp.url, "http://www.example.com/price/\xa3") def test_unicode_body(self): - unicode_string = ('\u043a\u0438\u0440\u0438\u043b\u043b\u0438\u0447\u0435\u0441\u043a\u0438\u0439 ' - '\u0442\u0435\u043a\u0441\u0442') - self.assertRaises(TypeError, self.response_class, 'http://www.example.com', body='unicode body') + unicode_string = ( + "\u043a\u0438\u0440\u0438\u043b\u043b\u0438\u0447\u0435\u0441\u043a\u0438\u0439 " + "\u0442\u0435\u043a\u0441\u0442" + ) + self.assertRaises( + TypeError, + self.response_class, + "http://www.example.com", + body="unicode body", + ) - original_string = unicode_string.encode('cp1251') - r1 = self.response_class('http://www.example.com', body=original_string, encoding='cp1251') + original_string = unicode_string.encode("cp1251") + r1 = self.response_class( + "http://www.example.com", body=original_string, encoding="cp1251" + ) # check response.text self.assertTrue(isinstance(r1.text, str)) self.assertEqual(r1.text, unicode_string) def test_encoding(self): - r1 = self.response_class("http://www.example.com", body=b"\xc2\xa3", - headers={"Content-type": ["text/html; charset=utf-8"]}) - r2 = self.response_class("http://www.example.com", encoding='utf-8', body="\xa3") - r3 = self.response_class("http://www.example.com", body=b"\xa3", - headers={"Content-type": ["text/html; charset=iso-8859-1"]}) + r1 = self.response_class( + "http://www.example.com", + body=b"\xc2\xa3", + headers={"Content-type": ["text/html; charset=utf-8"]}, + ) + r2 = self.response_class( + "http://www.example.com", encoding="utf-8", body="\xa3" + ) + r3 = self.response_class( + "http://www.example.com", + body=b"\xa3", + headers={"Content-type": ["text/html; charset=iso-8859-1"]}, + ) r4 = self.response_class("http://www.example.com", body=b"\xa2\xa3") - r5 = self.response_class("http://www.example.com", body=b"\xc2\xa3", - headers={"Content-type": ["text/html; charset=None"]}) - r6 = self.response_class("http://www.example.com", body=b"\xa8D", - headers={"Content-type": ["text/html; charset=gb2312"]}) - r7 = self.response_class("http://www.example.com", body=b"\xa8D", - headers={"Content-type": ["text/html; charset=gbk"]}) - r8 = self.response_class("http://www.example.com", body=codecs.BOM_UTF8 + b"\xc2\xa3", - headers={"Content-type": ["text/html; charset=cp1251"]}) + r5 = self.response_class( + "http://www.example.com", + body=b"\xc2\xa3", + headers={"Content-type": ["text/html; charset=None"]}, + ) + r6 = self.response_class( + "http://www.example.com", + body=b"\xa8D", + headers={"Content-type": ["text/html; charset=gb2312"]}, + ) + r7 = self.response_class( + "http://www.example.com", + body=b"\xa8D", + headers={"Content-type": ["text/html; charset=gbk"]}, + ) + r8 = self.response_class( + "http://www.example.com", + body=codecs.BOM_UTF8 + b"\xc2\xa3", + headers={"Content-type": ["text/html; charset=cp1251"]}, + ) + r9 = self.response_class( + "http://www.example.com", + body=b"\x80", + headers={ + "Content-type": [b"application/x-download; filename=\x80dummy.txt"] + }, + ) self.assertEqual(r1._headers_encoding(), "utf-8") self.assertEqual(r2._headers_encoding(), None) - self.assertEqual(r2._declared_encoding(), 'utf-8') - self._assert_response_encoding(r2, 'utf-8') + self.assertEqual(r2._declared_encoding(), "utf-8") + self._assert_response_encoding(r2, "utf-8") self.assertEqual(r3._headers_encoding(), "cp1252") self.assertEqual(r3._declared_encoding(), "cp1252") self.assertEqual(r4._headers_encoding(), None) self.assertEqual(r5._headers_encoding(), None) self.assertEqual(r8._headers_encoding(), "cp1251") + self.assertEqual(r9._headers_encoding(), None) self.assertEqual(r8._declared_encoding(), "utf-8") + self.assertEqual(r9._declared_encoding(), None) self._assert_response_encoding(r5, "utf-8") self._assert_response_encoding(r8, "utf-8") - assert r4._body_inferred_encoding() is not None and r4._body_inferred_encoding() != 'ascii' - self._assert_response_values(r1, 'utf-8', "\xa3") - self._assert_response_values(r2, 'utf-8', "\xa3") - self._assert_response_values(r3, 'iso-8859-1', "\xa3") - self._assert_response_values(r6, 'gb18030', "\u2015") - self._assert_response_values(r7, 'gb18030', "\u2015") + self._assert_response_encoding(r9, "cp1252") + assert ( + r4._body_inferred_encoding() is not None + and r4._body_inferred_encoding() != "ascii" + ) + self._assert_response_values(r1, "utf-8", "\xa3") + self._assert_response_values(r2, "utf-8", "\xa3") + self._assert_response_values(r3, "iso-8859-1", "\xa3") + self._assert_response_values(r6, "gb18030", "\u2015") + self._assert_response_values(r7, "gb18030", "\u2015") + self._assert_response_values(r9, "cp1252", "€") # TextResponse (and subclasses) must be passed a encoding when instantiating with unicode bodies - self.assertRaises(TypeError, self.response_class, "http://www.example.com", body="\xa3") + self.assertRaises( + TypeError, + self.response_class, + "http://www.example.com", + body="\xa3", + ) def test_declared_encoding_invalid(self): """Check that unknown declared encodings are ignored""" - r = self.response_class("http://www.example.com", - headers={"Content-type": ["text/html; charset=UNKNOWN"]}, - body=b"\xc2\xa3") + r = self.response_class( + "http://www.example.com", + headers={"Content-type": ["text/html; charset=UNKNOWN"]}, + body=b"\xc2\xa3", + ) self.assertEqual(r._declared_encoding(), None) - self._assert_response_values(r, 'utf-8', "\xa3") + self._assert_response_values(r, "utf-8", "\xa3") def test_utf16(self): """Test utf-16 because UnicodeDammit is known to have problems with""" - r = self.response_class("http://www.example.com", - body=b'\xff\xfeh\x00i\x00', - encoding='utf-16') - self._assert_response_values(r, 'utf-16', "hi") + r = self.response_class( + "http://www.example.com", + body=b"\xff\xfeh\x00i\x00", + encoding="utf-16", + ) + self._assert_response_values(r, "utf-16", "hi") def test_invalid_utf8_encoded_body_with_valid_utf8_BOM(self): - r6 = self.response_class("http://www.example.com", - headers={"Content-type": ["text/html; charset=utf-8"]}, - body=b"\xef\xbb\xbfWORD\xe3\xab") - self.assertEqual(r6.encoding, 'utf-8') - self.assertIn(r6.text, { - 'WORD\ufffd\ufffd', # w3lib < 1.19.0 - 'WORD\ufffd', # w3lib >= 1.19.0 - }) + r6 = self.response_class( + "http://www.example.com", + headers={"Content-type": ["text/html; charset=utf-8"]}, + body=b"\xef\xbb\xbfWORD\xe3\xab", + ) + self.assertEqual(r6.encoding, "utf-8") + self.assertIn( + r6.text, + { + "WORD\ufffd\ufffd", # w3lib < 1.19.0 + "WORD\ufffd", # w3lib >= 1.19.0 + }, + ) def test_bom_is_removed_from_body(self): # Inferring encoding from body also cache decoded body as sideeffect, # this test tries to ensure that calling response.encoding and - # response.text in indistint order doesn't affect final + # response.text in indistinct order doesn't affect final + # response.text in indistinct order doesn't affect final # values for encoding and decoded body. - url = 'http://example.com' + url = "http://example.com" body = b"\xef\xbb\xbfWORD" headers = {"Content-type": ["text/html; charset=utf-8"]} # Test response without content-type and BOM encoding response = self.response_class(url, body=body) - self.assertEqual(response.encoding, 'utf-8') - self.assertEqual(response.text, 'WORD') + self.assertEqual(response.encoding, "utf-8") + self.assertEqual(response.text, "WORD") response = self.response_class(url, body=body) - self.assertEqual(response.text, 'WORD') - self.assertEqual(response.encoding, 'utf-8') + self.assertEqual(response.text, "WORD") + self.assertEqual(response.encoding, "utf-8") # Body caching sideeffect isn't triggered when encoding is declared in # content-type header but BOM still need to be removed from decoded # body response = self.response_class(url, headers=headers, body=body) - self.assertEqual(response.encoding, 'utf-8') - self.assertEqual(response.text, 'WORD') + self.assertEqual(response.encoding, "utf-8") + self.assertEqual(response.text, "WORD") response = self.response_class(url, headers=headers, body=body) - self.assertEqual(response.text, 'WORD') - self.assertEqual(response.encoding, 'utf-8') + self.assertEqual(response.text, "WORD") + self.assertEqual(response.encoding, "utf-8") def test_replace_wrong_encoding(self): """Test invalid chars are replaced properly""" - r = self.response_class("http://www.example.com", encoding='utf-8', body=b'PREFIX\xe3\xabSUFFIX') + r = self.response_class( + "http://www.example.com", + encoding="utf-8", + body=b"PREFIX\xe3\xabSUFFIX", + ) # XXX: Policy for replacing invalid chars may suffer minor variations # but it should always contain the unicode replacement char ('\ufffd') - assert '\ufffd' in r.text, repr(r.text) - assert 'PREFIX' in r.text, repr(r.text) - assert 'SUFFIX' in r.text, repr(r.text) + assert "\ufffd" in r.text, repr(r.text) + assert "PREFIX" in r.text, repr(r.text) + assert "SUFFIX" in r.text, repr(r.text) # Do not destroy html tags due to encoding bugs - r = self.response_class("http://example.com", encoding='utf-8', - body=b'\xf0value') - assert 'value' in r.text, repr(r.text) + r = self.response_class( + "http://example.com", + encoding="utf-8", + body=b"\xf0value", + ) + assert "value" in r.text, repr(r.text) # FIXME: This test should pass once we stop using BeautifulSoup's UnicodeDammit in TextResponse # r = self.response_class("http://www.example.com", body=b'PREFIX\xe3\xabSUFFIX') @@ -472,22 +586,15 @@ class TextResponseTest(BaseResponseTest): response = self.response_class("http://www.example.com", body=body) self.assertIsInstance(response.selector, Selector) - self.assertEqual(response.selector.type, 'html') + self.assertEqual(response.selector.type, "html") self.assertIs(response.selector, response.selector) # property is cached self.assertIs(response.selector.response, response) self.assertEqual( - response.selector.xpath("//title/text()").getall(), - ['Some page'] - ) - self.assertEqual( - response.selector.css("title::text").getall(), - ['Some page'] - ) - self.assertEqual( - response.selector.re("Some (.*)"), - ['page'] + response.selector.xpath("//title/text()").getall(), ["Some page"] ) + self.assertEqual(response.selector.css("title::text").getall(), ["Some page"]) + self.assertEqual(response.selector.re("Some (.*)"), ["page"]) def test_selector_shortcuts(self): body = b"A nice paragraph.
" + body = b'A nice paragraph.
' response = self.response_class("http://www.example.com", body=body) self.assertEqual( - response.xpath("normalize-space(//p[@class=$pclass])", pclass="content").getall(), - response.xpath("normalize-space(//p[@class=\"content\"])").getall(), + response.xpath( + "normalize-space(//p[@class=$pclass])", pclass="content" + ).getall(), + response.xpath('normalize-space(//p[@class="content"])').getall(), ) self.assertEqual( response.xpath( "//title[count(following::p[@class=$pclass])=$pcount]/text()", - pclass="content", pcount=1, + pclass="content", + pcount=1, + ).getall(), + response.xpath( + '//title[count(following::p[@class="content"])=1]/text()' ).getall(), - response.xpath("//title[count(following::p[@class=\"content\"])=1]/text()").getall(), ) def test_urljoin_with_base_url(self): """Test urljoin shortcut which also evaluates base-url.""" body = b'
- """)
+ """,
+ )
def test_init_method(self):
l = TestItemLoader()
@@ -273,28 +295,28 @@ class SelectortemLoaderTest(unittest.TestCase):
def test_init_method_errors(self):
l = TestItemLoader()
- self.assertRaises(RuntimeError, l.add_xpath, 'url', '//a/@href')
- self.assertRaises(RuntimeError, l.replace_xpath, 'url', '//a/@href')
- self.assertRaises(RuntimeError, l.get_xpath, '//a/@href')
- self.assertRaises(RuntimeError, l.add_css, 'name', '#name::text')
- self.assertRaises(RuntimeError, l.replace_css, 'name', '#name::text')
- self.assertRaises(RuntimeError, l.get_css, '#name::text')
+ self.assertRaises(RuntimeError, l.add_xpath, "url", "//a/@href")
+ self.assertRaises(RuntimeError, l.replace_xpath, "url", "//a/@href")
+ self.assertRaises(RuntimeError, l.get_xpath, "//a/@href")
+ self.assertRaises(RuntimeError, l.add_css, "name", "#name::text")
+ self.assertRaises(RuntimeError, l.replace_css, "name", "#name::text")
+ self.assertRaises(RuntimeError, l.get_css, "#name::text")
def test_init_method_with_selector(self):
sel = Selector(text="body
', mimetype='text/html', - _callback=self._catch_mail_sent) + mailsender.send( + to=["test@scrapy.org"], + subject="subject", + body="body
", + mimetype="text/html", + _callback=self._catch_mail_sent, + ) - msg = self.catched_msg['msg'] - self.assertEqual(msg.get_payload(), 'body
') - self.assertEqual(msg.get('Content-Type'), 'text/html') + msg = self.catched_msg["msg"] + self.assertEqual(msg.get_payload(), "body
") + self.assertEqual(msg.get("Content-Type"), "text/html") def test_send_attach(self): attach = BytesIO() - attach.write(b'content') + attach.write(b"content") attach.seek(0) - attachs = [('attachment', 'text/plain', attach)] + attachs = [("attachment", "text/plain", attach)] mailsender = MailSender(debug=True) - mailsender.send(to=['test@scrapy.org'], subject='subject', body='body', - attachs=attachs, _callback=self._catch_mail_sent) + mailsender.send( + to=["test@scrapy.org"], + subject="subject", + body="body", + attachs=attachs, + _callback=self._catch_mail_sent, + ) assert self.catched_msg - self.assertEqual(self.catched_msg['to'], ['test@scrapy.org']) - self.assertEqual(self.catched_msg['subject'], 'subject') - self.assertEqual(self.catched_msg['body'], 'body') + self.assertEqual(self.catched_msg["to"], ["test@scrapy.org"]) + self.assertEqual(self.catched_msg["subject"], "subject") + self.assertEqual(self.catched_msg["body"], "body") - msg = self.catched_msg['msg'] - self.assertEqual(msg['to'], 'test@scrapy.org') - self.assertEqual(msg['subject'], 'subject') + msg = self.catched_msg["msg"] + self.assertEqual(msg["to"], "test@scrapy.org") + self.assertEqual(msg["subject"], "subject") payload = msg.get_payload() assert isinstance(payload, list) self.assertEqual(len(payload), 2) text, attach = payload - self.assertEqual(text.get_payload(decode=True), b'body') - self.assertEqual(text.get_charset(), Charset('us-ascii')) - self.assertEqual(attach.get_payload(decode=True), b'content') + self.assertEqual(text.get_payload(decode=True), b"body") + self.assertEqual(text.get_charset(), Charset("us-ascii")) + self.assertEqual(attach.get_payload(decode=True), b"content") def _catch_mail_sent(self, **kwargs): self.catched_msg = dict(**kwargs) def test_send_utf8(self): - subject = 'sübjèçt' - body = 'bödÿ-àéïöñß' + subject = "sübjèçt" + body = "bödÿ-àéïöñß" mailsender = MailSender(debug=True) - mailsender.send(to=['test@scrapy.org'], subject=subject, body=body, - charset='utf-8', _callback=self._catch_mail_sent) + mailsender.send( + to=["test@scrapy.org"], + subject=subject, + body=body, + charset="utf-8", + _callback=self._catch_mail_sent, + ) assert self.catched_msg - self.assertEqual(self.catched_msg['subject'], subject) - self.assertEqual(self.catched_msg['body'], body) + self.assertEqual(self.catched_msg["subject"], subject) + self.assertEqual(self.catched_msg["body"], body) - msg = self.catched_msg['msg'] - self.assertEqual(msg['subject'], subject) - self.assertEqual(msg.get_payload(), body) - self.assertEqual(msg.get_charset(), Charset('utf-8')) - self.assertEqual(msg.get('Content-Type'), 'text/plain; charset="utf-8"') + msg = self.catched_msg["msg"] + self.assertEqual(msg["subject"], subject) + self.assertEqual(msg.get_payload(decode=True).decode("utf-8"), body) + self.assertEqual(msg.get_charset(), Charset("utf-8")) + self.assertEqual(msg.get("Content-Type"), 'text/plain; charset="utf-8"') def test_send_attach_utf8(self): - subject = 'sübjèçt' - body = 'bödÿ-àéïöñß' + subject = "sübjèçt" + body = "bödÿ-àéïöñß" attach = BytesIO() - attach.write(body.encode('utf-8')) + attach.write(body.encode("utf-8")) attach.seek(0) - attachs = [('attachment', 'text/plain', attach)] + attachs = [("attachment", "text/plain", attach)] mailsender = MailSender(debug=True) - mailsender.send(to=['test@scrapy.org'], subject=subject, body=body, - attachs=attachs, charset='utf-8', - _callback=self._catch_mail_sent) + mailsender.send( + to=["test@scrapy.org"], + subject=subject, + body=body, + attachs=attachs, + charset="utf-8", + _callback=self._catch_mail_sent, + ) assert self.catched_msg - self.assertEqual(self.catched_msg['subject'], subject) - self.assertEqual(self.catched_msg['body'], body) + self.assertEqual(self.catched_msg["subject"], subject) + self.assertEqual(self.catched_msg["body"], body) - msg = self.catched_msg['msg'] - self.assertEqual(msg['subject'], subject) - self.assertEqual(msg.get_charset(), Charset('utf-8')) - self.assertEqual(msg.get('Content-Type'), - 'multipart/mixed; charset="utf-8"') + msg = self.catched_msg["msg"] + self.assertEqual(msg["subject"], subject) + self.assertEqual(msg.get_charset(), Charset("utf-8")) + self.assertEqual(msg.get("Content-Type"), 'multipart/mixed; charset="utf-8"') payload = msg.get_payload() assert isinstance(payload, list) self.assertEqual(len(payload), 2) text, attach = payload - self.assertEqual(text.get_payload(decode=True).decode('utf-8'), body) - self.assertEqual(text.get_charset(), Charset('utf-8')) - self.assertEqual(attach.get_payload(decode=True).decode('utf-8'), body) + self.assertEqual(text.get_payload(decode=True).decode("utf-8"), body) + self.assertEqual(text.get_charset(), Charset("utf-8")) + self.assertEqual(attach.get_payload(decode=True).decode("utf-8"), body) + + def test_create_sender_factory_with_host(self): + mailsender = MailSender(debug=False, smtphost="smtp.testhost.com") + + factory = mailsender._create_sender_factory( + to_addrs=["test@scrapy.org"], msg="test", d=defer.Deferred() + ) + + context = factory.buildProtocol("test@scrapy.org").context + if twisted_version >= Version("twisted", 21, 2, 0): + self.assertIsInstance(context, ClientTLSOptions) + else: + self.assertIsInstance(context, ClientContextFactory) if __name__ == "__main__": diff --git a/tests/test_middleware.py b/tests/test_middleware.py index 8651431b5..a42c7b3d1 100644 --- a/tests/test_middleware.py +++ b/tests/test_middleware.py @@ -1,12 +1,11 @@ from twisted.trial import unittest -from scrapy.settings import Settings from scrapy.exceptions import NotConfigured from scrapy.middleware import MiddlewareManager +from scrapy.settings import Settings class M1: - def open_spider(self, spider): pass @@ -18,7 +17,6 @@ class M1: class M2: - def open_spider(self, spider): pass @@ -29,13 +27,11 @@ class M2: class M3: - def process(self, response, request, spider): pass class MOff: - def open_spider(self, spider): pass @@ -43,41 +39,43 @@ class MOff: pass def __init__(self): - raise NotConfigured + raise NotConfigured("foo") class TestMiddlewareManager(MiddlewareManager): - @classmethod def _get_mwlist_from_settings(cls, settings): return [M1, MOff, M3] def _add_middleware(self, mw): super()._add_middleware(mw) - if hasattr(mw, 'process'): - self.methods['process'].append(mw.process) + if hasattr(mw, "process"): + self.methods["process"].append(mw.process) class MiddlewareManagerTest(unittest.TestCase): - def test_init(self): m1, m2, m3 = M1(), M2(), M3() mwman = TestMiddlewareManager(m1, m2, m3) - self.assertEqual(list(mwman.methods['open_spider']), [m1.open_spider, m2.open_spider]) - self.assertEqual(list(mwman.methods['close_spider']), [m2.close_spider, m1.close_spider]) - self.assertEqual(list(mwman.methods['process']), [m1.process, m3.process]) + self.assertEqual( + list(mwman.methods["open_spider"]), [m1.open_spider, m2.open_spider] + ) + self.assertEqual( + list(mwman.methods["close_spider"]), [m2.close_spider, m1.close_spider] + ) + self.assertEqual(list(mwman.methods["process"]), [m1.process, m3.process]) def test_methods(self): mwman = TestMiddlewareManager(M1(), M2(), M3()) self.assertEqual( - [x.__self__.__class__ for x in mwman.methods['open_spider']], - [M1, M2]) + [x.__self__.__class__ for x in mwman.methods["open_spider"]], [M1, M2] + ) self.assertEqual( - [x.__self__.__class__ for x in mwman.methods['close_spider']], - [M2, M1]) + [x.__self__.__class__ for x in mwman.methods["close_spider"]], [M2, M1] + ) self.assertEqual( - [x.__self__.__class__ for x in mwman.methods['process']], - [M1, M3]) + [x.__self__.__class__ for x in mwman.methods["process"]], [M1, M3] + ) def test_enabled(self): m1, m2, m3 = M1(), M2(), M3() diff --git a/tests/test_pipeline_crawl.py b/tests/test_pipeline_crawl.py index 39bca7bf2..c41ab483f 100644 --- a/tests/test_pipeline_crawl.py +++ b/tests/test_pipeline_crawl.py @@ -1,19 +1,21 @@ import shutil from pathlib import Path +from typing import Optional, Set from testfixtures import LogCapture from twisted.internet import defer from twisted.trial.unittest import TestCase from w3lib.url import add_or_replace_parameter -from scrapy.crawler import CrawlerRunner from scrapy import signals +from scrapy.crawler import CrawlerRunner +from scrapy.utils.misc import load_object from tests.mockserver import MockServer from tests.spiders import SimpleSpider class MediaDownloadSpider(SimpleSpider): - name = 'mediadownload' + name = "mediadownload" def _process_url(self, url): return url @@ -27,34 +29,38 @@ class MediaDownloadSpider(SimpleSpider): self._process_url(response.urljoin(href)) for href in response.xpath( '//table[thead/tr/th="Filename"]/tbody//a/@href' - ).getall()], + ).getall() + ], } yield item class BrokenLinksMediaDownloadSpider(MediaDownloadSpider): - name = 'brokenmedia' + name = "brokenmedia" def _process_url(self, url): - return url + '.foo' + return url + ".foo" class RedirectedMediaDownloadSpider(MediaDownloadSpider): - name = 'redirectedmedia' + name = "redirectedmedia" def _process_url(self, url): - return add_or_replace_parameter(self.mockserver.url('/redirect-to'), 'goto', url) + return add_or_replace_parameter( + self.mockserver.url("/redirect-to"), "goto", url + ) class FileDownloadCrawlTestCase(TestCase): - pipeline_class = 'scrapy.pipelines.files.FilesPipeline' - store_setting_key = 'FILES_STORE' - media_key = 'files' - media_urls_key = 'file_urls' - expected_checksums = { - '5547178b89448faf0015a13f904c936e', - 'c2281c83670e31d8aaab7cb642b824db', - 'ed3f6538dc15d4d9179dae57319edc5f'} + pipeline_class = "scrapy.pipelines.files.FilesPipeline" + store_setting_key = "FILES_STORE" + media_key = "files" + media_urls_key = "file_urls" + expected_checksums: Optional[Set[str]] = { + "5547178b89448faf0015a13f904c936e", + "c2281c83670e31d8aaab7cb642b824db", + "ed3f6538dc15d4d9179dae57319edc5f", + } def setUp(self): self.mockserver = MockServer() @@ -64,8 +70,8 @@ class FileDownloadCrawlTestCase(TestCase): self.tmpmediastore = Path(self.mktemp()) self.tmpmediastore.mkdir() self.settings = { - 'REQUEST_FINGERPRINTER_IMPLEMENTATION': '2.7', - 'ITEM_PIPELINES': {self.pipeline_class: 1}, + "REQUEST_FINGERPRINTER_IMPLEMENTATION": "2.7", + "ITEM_PIPELINES": {self.pipeline_class: 1}, self.store_setting_key: str(self.tmpmediastore), } self.runner = CrawlerRunner(self.settings) @@ -91,43 +97,46 @@ class FileDownloadCrawlTestCase(TestCase): self.assertIn(self.media_key, items[0]) # check that logs show the expected number of successful file downloads - file_dl_success = 'File (downloaded): Downloaded file from' + file_dl_success = "File (downloaded): Downloaded file from" self.assertEqual(logs.count(file_dl_success), 3) # check that the images/files status is `downloaded` for item in items: for i in item[self.media_key]: - self.assertEqual(i['status'], 'downloaded') + self.assertEqual(i["status"], "downloaded") # check that the images/files checksums are what we know they should be if self.expected_checksums is not None: checksums = set( - i['checksum'] - for item in items - for i in item[self.media_key] + i["checksum"] for item in items for i in item[self.media_key] ) self.assertEqual(checksums, self.expected_checksums) # check that the image files where actually written to the media store for item in items: for i in item[self.media_key]: - self.assertTrue((self.tmpmediastore / i['path']).exists()) + self.assertTrue((self.tmpmediastore / i["path"]).exists()) def _assert_files_download_failure(self, crawler, items, code, logs): - # check that the item does NOT have the "images/files" field populated self.assertEqual(len(items), 1) self.assertIn(self.media_key, items[0]) self.assertFalse(items[0][self.media_key]) # check that there was 1 successful fetch and 3 other responses with non-200 code - self.assertEqual(crawler.stats.get_value('downloader/request_method_count/GET'), 4) - self.assertEqual(crawler.stats.get_value('downloader/response_count'), 4) - self.assertEqual(crawler.stats.get_value('downloader/response_status_count/200'), 1) - self.assertEqual(crawler.stats.get_value(f'downloader/response_status_count/{code}'), 3) + self.assertEqual( + crawler.stats.get_value("downloader/request_method_count/GET"), 4 + ) + self.assertEqual(crawler.stats.get_value("downloader/response_count"), 4) + self.assertEqual( + crawler.stats.get_value("downloader/response_status_count/200"), 1 + ) + self.assertEqual( + crawler.stats.get_value(f"downloader/response_status_count/{code}"), 3 + ) # check that logs do show the failure on the file downloads - file_dl_failure = f'File (code: {code}): Error downloading file from' + file_dl_failure = f"File (code: {code}): Error downloading file from" self.assertEqual(logs.count(file_dl_failure), 3) # check that no files were written to the media store @@ -140,7 +149,8 @@ class FileDownloadCrawlTestCase(TestCase): yield crawler.crawl( self.mockserver.url("/files/images/"), media_key=self.media_key, - media_urls_key=self.media_urls_key) + media_urls_key=self.media_urls_key, + ) self._assert_files_downloaded(self.items, str(log)) @defer.inlineCallbacks @@ -150,7 +160,8 @@ class FileDownloadCrawlTestCase(TestCase): yield crawler.crawl( self.mockserver.url("/files/images/"), media_key=self.media_key, - media_urls_key=self.media_urls_key) + media_urls_key=self.media_urls_key, + ) self._assert_files_download_failure(crawler, self.items, 404, str(log)) @defer.inlineCallbacks @@ -161,13 +172,14 @@ class FileDownloadCrawlTestCase(TestCase): self.mockserver.url("/files/images/"), media_key=self.media_key, media_urls_key=self.media_urls_key, - mockserver=self.mockserver) + mockserver=self.mockserver, + ) self._assert_files_download_failure(crawler, self.items, 302, str(log)) @defer.inlineCallbacks def test_download_media_redirected_allowed(self): settings = dict(self.settings) - settings.update({'MEDIA_ALLOW_REDIRECTS': True}) + settings.update({"MEDIA_ALLOW_REDIRECTS": True}) runner = CrawlerRunner(settings) crawler = self._create_crawler(RedirectedMediaDownloadSpider, runner=runner) with LogCapture() as log: @@ -175,27 +187,55 @@ class FileDownloadCrawlTestCase(TestCase): self.mockserver.url("/files/images/"), media_key=self.media_key, media_urls_key=self.media_urls_key, - mockserver=self.mockserver) + mockserver=self.mockserver, + ) self._assert_files_downloaded(self.items, str(log)) - self.assertEqual(crawler.stats.get_value('downloader/response_status_count/302'), 3) + self.assertEqual( + crawler.stats.get_value("downloader/response_status_count/302"), 3 + ) + + @defer.inlineCallbacks + def test_download_media_file_path_error(self): + cls = load_object(self.pipeline_class) + + class ExceptionRaisingMediaPipeline(cls): + def file_path(self, request, response=None, info=None, *, item=None): + return 1 / 0 + + settings = { + **self.settings, + "ITEM_PIPELINES": {ExceptionRaisingMediaPipeline: 1}, + } + runner = CrawlerRunner(settings) + crawler = self._create_crawler(MediaDownloadSpider, runner=runner) + with LogCapture() as log: + yield crawler.crawl( + self.mockserver.url("/files/images/"), + media_key=self.media_key, + media_urls_key=self.media_urls_key, + mockserver=self.mockserver, + ) + self.assertIn("ZeroDivisionError", str(log)) +skip_pillow: Optional[str] try: from PIL import Image # noqa: imported just to check for the import error except ImportError: - skip_pillow = 'Missing Python Imaging Library, install https://pypi.python.org/pypi/Pillow' + skip_pillow = ( + "Missing Python Imaging Library, install https://pypi.python.org/pypi/Pillow" + ) else: skip_pillow = None class ImageDownloadCrawlTestCase(FileDownloadCrawlTestCase): - skip = skip_pillow - pipeline_class = 'scrapy.pipelines.images.ImagesPipeline' - store_setting_key = 'IMAGES_STORE' - media_key = 'images' - media_urls_key = 'image_urls' + pipeline_class = "scrapy.pipelines.images.ImagesPipeline" + store_setting_key = "IMAGES_STORE" + media_key = "images" + media_urls_key = "image_urls" # somehow checksums for images are different for Python 3.3 expected_checksums = None diff --git a/tests/test_pipeline_files.py b/tests/test_pipeline_files.py index 4acd29bf7..e7000e314 100644 --- a/tests/test_pipeline_files.py +++ b/tests/test_pipeline_files.py @@ -1,3 +1,4 @@ +import dataclasses import os import random import time @@ -6,9 +7,9 @@ from io import BytesIO from pathlib import Path from shutil import rmtree from tempfile import mkdtemp +from typing import Dict, List from unittest import mock from urllib.parse import urlparse -import dataclasses import attr from itemadapter import ItemAdapter @@ -32,18 +33,15 @@ from scrapy.utils.test import ( get_gcs_content_and_delete, skip_if_no_boto, ) +from tests.mockserver import MockFTPServer - -def _mocked_download_func(request, info): - response = request.meta.get('response') - return response() if callable(response) else response +from .test_pipeline_media import _mocked_download_func class FilesPipelineTestCase(unittest.TestCase): - def setUp(self): self.tempdir = mkdtemp() - settings_dict = {'FILES_STORE': self.tempdir} + settings_dict = {"FILES_STORE": self.tempdir} crawler = get_crawler(spidercls=None, settings_dict=settings_dict) self.pipeline = FilesPipeline.from_crawler(crawler) self.pipeline.download_func = _mocked_download_func @@ -56,41 +54,70 @@ class FilesPipelineTestCase(unittest.TestCase): file_path = self.pipeline.file_path self.assertEqual( file_path(Request("https://dev.mydeco.com/mydeco.pdf")), - 'full/c9b564df929f4bc635bdd19fde4f3d4847c757c5.pdf') + "full/c9b564df929f4bc635bdd19fde4f3d4847c757c5.pdf", + ) self.assertEqual( - file_path(Request("http://www.maddiebrown.co.uk///catalogue-items//image_54642_12175_95307.txt")), - 'full/4ce274dd83db0368bafd7e406f382ae088e39219.txt') + file_path( + Request( + "http://www.maddiebrown.co.uk///catalogue-items//image_54642_12175_95307.txt" + ) + ), + "full/4ce274dd83db0368bafd7e406f382ae088e39219.txt", + ) self.assertEqual( - file_path(Request("https://dev.mydeco.com/two/dirs/with%20spaces%2Bsigns.doc")), - 'full/94ccc495a17b9ac5d40e3eabf3afcb8c2c9b9e1a.doc') + file_path( + Request("https://dev.mydeco.com/two/dirs/with%20spaces%2Bsigns.doc") + ), + "full/94ccc495a17b9ac5d40e3eabf3afcb8c2c9b9e1a.doc", + ) self.assertEqual( - file_path(Request("http://www.dfsonline.co.uk/get_prod_image.php?img=status_0907_mdm.jpg")), - 'full/4507be485f38b0da8a0be9eb2e1dfab8a19223f2.jpg') + file_path( + Request( + "http://www.dfsonline.co.uk/get_prod_image.php?img=status_0907_mdm.jpg" + ) + ), + "full/4507be485f38b0da8a0be9eb2e1dfab8a19223f2.jpg", + ) self.assertEqual( file_path(Request("http://www.dorma.co.uk/images/product_details/2532/")), - 'full/97ee6f8a46cbbb418ea91502fd24176865cf39b2') + "full/97ee6f8a46cbbb418ea91502fd24176865cf39b2", + ) self.assertEqual( file_path(Request("http://www.dorma.co.uk/images/product_details/2532")), - 'full/244e0dd7d96a3b7b01f54eded250c9e272577aa1') + "full/244e0dd7d96a3b7b01f54eded250c9e272577aa1", + ) self.assertEqual( - file_path(Request("http://www.dorma.co.uk/images/product_details/2532"), - response=Response("http://www.dorma.co.uk/images/product_details/2532"), - info=object()), - 'full/244e0dd7d96a3b7b01f54eded250c9e272577aa1') + file_path( + Request("http://www.dorma.co.uk/images/product_details/2532"), + response=Response("http://www.dorma.co.uk/images/product_details/2532"), + info=object(), + ), + "full/244e0dd7d96a3b7b01f54eded250c9e272577aa1", + ) self.assertEqual( - file_path(Request("http://www.dfsonline.co.uk/get_prod_image.php?img=status_0907_mdm.jpg.bohaha")), - 'full/76c00cef2ef669ae65052661f68d451162829507') + file_path( + Request( + "http://www.dfsonline.co.uk/get_prod_image.php?img=status_0907_mdm.jpg.bohaha" + ) + ), + "full/76c00cef2ef669ae65052661f68d451162829507", + ) self.assertEqual( - file_path(Request("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAR0AAACxCAMAAADOHZloAAACClBMVEX/\ - //+F0tzCwMK76ZKQ21AMqr7oAAC96JvD5aWM2kvZ78J0N7fmAAC46Y4Ap7y")), - 'full/178059cbeba2e34120a67f2dc1afc3ecc09b61cb.png') + file_path( + Request( + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAR0AAACxCAMAAADOHZloAAACClBMVEX/\ + //+F0tzCwMK76ZKQ21AMqr7oAAC96JvD5aWM2kvZ78J0N7fmAAC46Y4Ap7y" + ) + ), + "full/178059cbeba2e34120a67f2dc1afc3ecc09b61cb.png", + ) def test_fs_store(self): assert isinstance(self.pipeline.store, FSFilesStore) self.assertEqual(self.pipeline.store.basedir, self.tempdir) - path = 'some/image/key.jpg' - fullpath = Path(self.tempdir, 'some', 'image', 'key.jpg') + path = "some/image/key.jpg" + fullpath = Path(self.tempdir, "some", "image", "key.jpg") self.assertEqual(self.pipeline.store._get_filesystem_path(path), fullpath) @defer.inlineCallbacks @@ -98,18 +125,24 @@ class FilesPipelineTestCase(unittest.TestCase): item_url = "http://example.com/file.pdf" item = _create_item_with_files(item_url) patchers = [ - mock.patch.object(FilesPipeline, 'inc_stats', return_value=True), - mock.patch.object(FSFilesStore, 'stat_file', return_value={ - 'checksum': 'abc', 'last_modified': time.time()}), - mock.patch.object(FilesPipeline, 'get_media_requests', - return_value=[_prepare_request_object(item_url)]) + mock.patch.object(FilesPipeline, "inc_stats", return_value=True), + mock.patch.object( + FSFilesStore, + "stat_file", + return_value={"checksum": "abc", "last_modified": time.time()}, + ), + mock.patch.object( + FilesPipeline, + "get_media_requests", + return_value=[_prepare_request_object(item_url)], + ), ] for p in patchers: p.start() result = yield self.pipeline.process_item(item, None) - self.assertEqual(result['files'][0]['checksum'], 'abc') - self.assertEqual(result['files'][0]['status'], 'uptodate') + self.assertEqual(result["files"][0]["checksum"], "abc") + self.assertEqual(result["files"][0]["status"], "uptodate") for p in patchers: p.stop() @@ -119,19 +152,28 @@ class FilesPipelineTestCase(unittest.TestCase): item_url = "http://example.com/file2.pdf" item = _create_item_with_files(item_url) patchers = [ - mock.patch.object(FSFilesStore, 'stat_file', return_value={ - 'checksum': 'abc', - 'last_modified': time.time() - (self.pipeline.expires * 60 * 60 * 24 * 2)}), - mock.patch.object(FilesPipeline, 'get_media_requests', - return_value=[_prepare_request_object(item_url)]), - mock.patch.object(FilesPipeline, 'inc_stats', return_value=True) + mock.patch.object( + FSFilesStore, + "stat_file", + return_value={ + "checksum": "abc", + "last_modified": time.time() + - (self.pipeline.expires * 60 * 60 * 24 * 2), + }, + ), + mock.patch.object( + FilesPipeline, + "get_media_requests", + return_value=[_prepare_request_object(item_url)], + ), + mock.patch.object(FilesPipeline, "inc_stats", return_value=True), ] for p in patchers: p.start() result = yield self.pipeline.process_item(item, None) - self.assertNotEqual(result['files'][0]['checksum'], 'abc') - self.assertEqual(result['files'][0]['status'], 'downloaded') + self.assertNotEqual(result["files"][0]["checksum"], "abc") + self.assertEqual(result["files"][0]["status"], "downloaded") for p in patchers: p.stop() @@ -141,19 +183,28 @@ class FilesPipelineTestCase(unittest.TestCase): item_url = "http://example.com/file3.pdf" item = _create_item_with_files(item_url) patchers = [ - mock.patch.object(FilesPipeline, 'inc_stats', return_value=True), - mock.patch.object(FSFilesStore, 'stat_file', return_value={ - 'checksum': 'abc', - 'last_modified': time.time() - (self.pipeline.expires * 60 * 60 * 24 * 2)}), - mock.patch.object(FilesPipeline, 'get_media_requests', - return_value=[_prepare_request_object(item_url, flags=['cached'])]) + mock.patch.object(FilesPipeline, "inc_stats", return_value=True), + mock.patch.object( + FSFilesStore, + "stat_file", + return_value={ + "checksum": "abc", + "last_modified": time.time() + - (self.pipeline.expires * 60 * 60 * 24 * 2), + }, + ), + mock.patch.object( + FilesPipeline, + "get_media_requests", + return_value=[_prepare_request_object(item_url, flags=["cached"])], + ), ] for p in patchers: p.start() result = yield self.pipeline.process_item(item, None) - self.assertNotEqual(result['files'][0]['checksum'], 'abc') - self.assertEqual(result['files'][0]['status'], 'cached') + self.assertNotEqual(result["files"][0]["checksum"], "abc") + self.assertEqual(result["files"][0]["status"], "cached") for p in patchers: p.stop() @@ -162,48 +213,62 @@ class FilesPipelineTestCase(unittest.TestCase): """ Custom file path based on item data, overriding default implementation """ + class CustomFilesPipeline(FilesPipeline): def file_path(self, request, response=None, info=None, item=None): return f'full/{item.get("path")}' - file_path = CustomFilesPipeline.from_settings(Settings({'FILES_STORE': self.tempdir})).file_path - item = dict(path='path-to-store-file') + file_path = CustomFilesPipeline.from_settings( + Settings({"FILES_STORE": self.tempdir}) + ).file_path + item = dict(path="path-to-store-file") request = Request("http://example.com") - self.assertEqual(file_path(request, item=item), 'full/path-to-store-file') + self.assertEqual(file_path(request, item=item), "full/path-to-store-file") class FilesPipelineTestCaseFieldsMixin: + def setUp(self): + self.tempdir = mkdtemp() + + def tearDown(self): + rmtree(self.tempdir) def test_item_fields_default(self): - url = 'http://www.example.com/files/1.txt' - item = self.item_class(name='item1', file_urls=[url]) - pipeline = FilesPipeline.from_settings(Settings({'FILES_STORE': 's3://example/files/'})) + url = "http://www.example.com/files/1.txt" + item = self.item_class(name="item1", file_urls=[url]) + pipeline = FilesPipeline.from_settings(Settings({"FILES_STORE": self.tempdir})) requests = list(pipeline.get_media_requests(item, None)) self.assertEqual(requests[0].url, url) - results = [(True, {'url': url})] + results = [(True, {"url": url})] item = pipeline.item_completed(results, item, None) files = ItemAdapter(item).get("files") self.assertEqual(files, [results[0][1]]) self.assertIsInstance(item, self.item_class) def test_item_fields_override_settings(self): - url = 'http://www.example.com/files/1.txt' - item = self.item_class(name='item1', custom_file_urls=[url]) - pipeline = FilesPipeline.from_settings(Settings({ - 'FILES_STORE': 's3://example/files/', - 'FILES_URLS_FIELD': 'custom_file_urls', - 'FILES_RESULT_FIELD': 'custom_files' - })) + url = "http://www.example.com/files/1.txt" + item = self.item_class(name="item1", custom_file_urls=[url]) + pipeline = FilesPipeline.from_settings( + Settings( + { + "FILES_STORE": self.tempdir, + "FILES_URLS_FIELD": "custom_file_urls", + "FILES_RESULT_FIELD": "custom_files", + } + ) + ) requests = list(pipeline.get_media_requests(item, None)) self.assertEqual(requests[0].url, url) - results = [(True, {'url': url})] + results = [(True, {"url": url})] item = pipeline.item_completed(results, item, None) custom_files = ItemAdapter(item).get("custom_files") self.assertEqual(custom_files, [results[0][1]]) self.assertIsInstance(item, self.item_class) -class FilesPipelineTestCaseFieldsDict(FilesPipelineTestCaseFieldsMixin, unittest.TestCase): +class FilesPipelineTestCaseFieldsDict( + FilesPipelineTestCaseFieldsMixin, unittest.TestCase +): item_class = dict @@ -217,7 +282,9 @@ class FilesPipelineTestItem(Item): custom_files = Field() -class FilesPipelineTestCaseFieldsItem(FilesPipelineTestCaseFieldsMixin, unittest.TestCase): +class FilesPipelineTestCaseFieldsItem( + FilesPipelineTestCaseFieldsMixin, unittest.TestCase +): item_class = FilesPipelineTestItem @@ -232,7 +299,9 @@ class FilesPipelineTestDataClass: custom_files: list = dataclasses.field(default_factory=list) -class FilesPipelineTestCaseFieldsDataClass(FilesPipelineTestCaseFieldsMixin, unittest.TestCase): +class FilesPipelineTestCaseFieldsDataClass( + FilesPipelineTestCaseFieldsMixin, unittest.TestCase +): item_class = FilesPipelineTestDataClass @@ -240,14 +309,16 @@ class FilesPipelineTestCaseFieldsDataClass(FilesPipelineTestCaseFieldsMixin, uni class FilesPipelineTestAttrsItem: name = attr.ib(default="") # default fields - file_urls = attr.ib(default=lambda: []) - files = attr.ib(default=lambda: []) + file_urls: List[str] = attr.ib(default=lambda: []) + files: List[Dict[str, str]] = attr.ib(default=lambda: []) # overridden fields - custom_file_urls = attr.ib(default=lambda: []) - custom_files = attr.ib(default=lambda: []) + custom_file_urls: List[str] = attr.ib(default=lambda: []) + custom_files: List[Dict[str, str]] = attr.ib(default=lambda: []) -class FilesPipelineTestCaseFieldsAttrsItem(FilesPipelineTestCaseFieldsMixin, unittest.TestCase): +class FilesPipelineTestCaseFieldsAttrsItem( + FilesPipelineTestCaseFieldsMixin, unittest.TestCase +): item_class = FilesPipelineTestAttrsItem @@ -255,12 +326,12 @@ class FilesPipelineTestCaseCustomSettings(unittest.TestCase): default_cls_settings = { "EXPIRES": 90, "FILES_URLS_FIELD": "file_urls", - "FILES_RESULT_FIELD": "files" + "FILES_RESULT_FIELD": "files", } file_cls_attr_settings_map = { ("EXPIRES", "FILES_EXPIRES", "expires"), ("FILES_URLS_FIELD", "FILES_URLS_FIELD", "files_urls_field"), - ("FILES_RESULT_FIELD", "FILES_RESULT_FIELD", "files_result_field") + ("FILES_RESULT_FIELD", "FILES_RESULT_FIELD", "files_result_field"), } def setUp(self): @@ -270,7 +341,6 @@ class FilesPipelineTestCaseCustomSettings(unittest.TestCase): rmtree(self.tempdir) def _generate_fake_settings(self, prefix=None): - def random_string(): return "".join([chr(random.randint(97, 123)) for _ in range(10)]) @@ -278,15 +348,17 @@ class FilesPipelineTestCaseCustomSettings(unittest.TestCase): "FILES_EXPIRES": random.randint(100, 1000), "FILES_URLS_FIELD": random_string(), "FILES_RESULT_FIELD": random_string(), - "FILES_STORE": self.tempdir + "FILES_STORE": self.tempdir, } if not prefix: return settings - return {prefix.upper() + "_" + k if k != "FILES_STORE" else k: v for k, v in settings.items()} + return { + prefix.upper() + "_" + k if k != "FILES_STORE" else k: v + for k, v in settings.items() + } def _generate_fake_pipeline(self): - class UserDefinedFilePipeline(FilesPipeline): EXPIRES = 1001 FILES_URLS_FIELD = "alfa" @@ -339,10 +411,13 @@ class FilesPipelineTestCaseCustomSettings(unittest.TestCase): If there are no settings for subclass and no subclass attributes, pipeline should use attributes of base class. """ + class UserDefinedFilesPipeline(FilesPipeline): pass - user_pipeline = UserDefinedFilesPipeline.from_settings(Settings({"FILES_STORE": self.tempdir})) + user_pipeline = UserDefinedFilesPipeline.from_settings( + Settings({"FILES_STORE": self.tempdir}) + ) for pipe_attr, settings_attr, pipe_ins_attr in self.file_cls_attr_settings_map: # Values from settings for custom pipeline should be set on pipeline instance. custom_value = self.default_cls_settings.get(pipe_attr.upper()) @@ -353,6 +428,7 @@ class FilesPipelineTestCaseCustomSettings(unittest.TestCase): If there are custom settings for subclass and NO class attributes, pipeline should use custom settings. """ + class UserDefinedFilesPipeline(FilesPipeline): pass @@ -374,7 +450,11 @@ class FilesPipelineTestCaseCustomSettings(unittest.TestCase): prefix = pipeline_cls.__name__.upper() settings = self._generate_fake_settings(prefix=prefix) user_pipeline = pipeline_cls.from_settings(Settings(settings)) - for pipe_cls_attr, settings_attr, pipe_inst_attr in self.file_cls_attr_settings_map: + for ( + pipe_cls_attr, + settings_attr, + pipe_inst_attr, + ) in self.file_cls_attr_settings_map: custom_value = settings.get(prefix + "_" + settings_attr) self.assertNotEqual(custom_value, self.default_cls_settings[pipe_cls_attr]) self.assertEqual(getattr(user_pipeline, pipe_inst_attr), custom_value) @@ -384,9 +464,16 @@ class FilesPipelineTestCaseCustomSettings(unittest.TestCase): DEFAULT_FILES_RESULT_FIELD = "this" DEFAULT_FILES_URLS_FIELD = "that" - pipeline = UserDefinedFilesPipeline.from_settings(Settings({"FILES_STORE": self.tempdir})) - self.assertEqual(pipeline.files_result_field, "this") - self.assertEqual(pipeline.files_urls_field, "that") + pipeline = UserDefinedFilesPipeline.from_settings( + Settings({"FILES_STORE": self.tempdir}) + ) + self.assertEqual( + pipeline.files_result_field, + UserDefinedFilesPipeline.DEFAULT_FILES_RESULT_FIELD, + ) + self.assertEqual( + pipeline.files_urls_field, UserDefinedFilesPipeline.DEFAULT_FILES_URLS_FIELD + ) def test_user_defined_subclass_default_key_names(self): """Test situation when user defines subclass of FilesPipeline, @@ -402,37 +489,52 @@ class FilesPipelineTestCaseCustomSettings(unittest.TestCase): for pipe_attr, settings_attr, pipe_inst_attr in self.file_cls_attr_settings_map: expected_value = settings.get(settings_attr) - self.assertEqual(getattr(pipeline_cls, pipe_inst_attr), - expected_value) + self.assertEqual(getattr(pipeline_cls, pipe_inst_attr), expected_value) + + def test_file_pipeline_using_pathlike_objects(self): + class CustomFilesPipelineWithPathLikeDir(FilesPipeline): + def file_path(self, request, response=None, info=None, *, item=None): + return Path("subdir") / Path(request.url).name + + pipeline = CustomFilesPipelineWithPathLikeDir.from_settings( + Settings({"FILES_STORE": Path("./Temp")}) + ) + request = Request("http://example.com/image01.jpg") + self.assertEqual(pipeline.file_path(request), Path("subdir/image01.jpg")) + + def test_files_store_constructor_with_pathlike_object(self): + path = Path("./FileDir") + fs_store = FSFilesStore(path) + self.assertEqual(fs_store.basedir, str(path)) class TestS3FilesStore(unittest.TestCase): - @defer.inlineCallbacks def test_persist(self): skip_if_no_boto() - bucket = 'mybucket' - key = 'export.csv' - uri = f's3://{bucket}/{key}' + bucket = "mybucket" + key = "export.csv" + uri = f"s3://{bucket}/{key}" buffer = mock.MagicMock() - meta = {'foo': 'bar'} - path = '' - content_type = 'image/png' + meta = {"foo": "bar"} + path = "" + content_type = "image/png" store = S3FilesStore(uri) from botocore.stub import Stubber + with Stubber(store.s3_client) as stub: stub.add_response( - 'put_object', + "put_object", expected_params={ - 'ACL': S3FilesStore.POLICY, - 'Body': buffer, - 'Bucket': bucket, - 'CacheControl': S3FilesStore.HEADERS['Cache-Control'], - 'ContentType': content_type, - 'Key': key, - 'Metadata': meta, + "ACL": S3FilesStore.POLICY, + "Body": buffer, + "Bucket": bucket, + "CacheControl": S3FilesStore.HEADERS["Cache-Control"], + "ContentType": content_type, + "Key": key, + "Metadata": meta, }, service_response={}, ) @@ -442,7 +544,7 @@ class TestS3FilesStore(unittest.TestCase): buffer, info=None, meta=meta, - headers={'Content-Type': content_type}, + headers={"Content-Type": content_type}, ) stub.assert_no_pending_responses() @@ -451,40 +553,41 @@ class TestS3FilesStore(unittest.TestCase): [ mock.call.seek(0), # The call to read does not happen with Stubber - ] + ], ) @defer.inlineCallbacks def test_stat(self): skip_if_no_boto() - bucket = 'mybucket' - key = 'export.csv' - uri = f's3://{bucket}/{key}' - checksum = '3187896a9657a28163abb31667df64c8' + bucket = "mybucket" + key = "export.csv" + uri = f"s3://{bucket}/{key}" + checksum = "3187896a9657a28163abb31667df64c8" last_modified = datetime(2019, 12, 1) store = S3FilesStore(uri) from botocore.stub import Stubber + with Stubber(store.s3_client) as stub: stub.add_response( - 'head_object', + "head_object", expected_params={ - 'Bucket': bucket, - 'Key': key, + "Bucket": bucket, + "Key": key, }, service_response={ - 'ETag': f'"{checksum}"', - 'LastModified': last_modified, + "ETag": f'"{checksum}"', + "LastModified": last_modified, }, ) - file_stats = yield store.stat_file('', info=None) + file_stats = yield store.stat_file("", info=None) self.assertEqual( file_stats, { - 'checksum': checksum, - 'last_modified': last_modified.timestamp(), + "checksum": checksum, + "last_modified": last_modified.timestamp(), }, ) @@ -495,27 +598,27 @@ class TestGCSFilesStore(unittest.TestCase): @defer.inlineCallbacks def test_persist(self): assert_gcs_environ() - uri = os.environ.get('GCS_TEST_FILE_URI') + uri = os.environ.get("GCS_TEST_FILE_URI") if not uri: raise unittest.SkipTest("No GCS URI available for testing") data = b"TestGCSFilesStore: \xe2\x98\x83" buf = BytesIO(data) - meta = {'foo': 'bar'} - path = 'full/filename' + meta = {"foo": "bar"} + path = "full/filename" store = GCSFilesStore(uri) - store.POLICY = 'authenticatedRead' - expected_policy = {'role': 'READER', 'entity': 'allAuthenticatedUsers'} + store.POLICY = "authenticatedRead" + expected_policy = {"role": "READER", "entity": "allAuthenticatedUsers"} yield store.persist_file(path, buf, info=None, meta=meta, headers=None) s = yield store.stat_file(path, info=None) - self.assertIn('last_modified', s) - self.assertIn('checksum', s) - self.assertEqual(s['checksum'], 'zc2oVgXkbQr2EQdSdw3OPA==') + self.assertIn("last_modified", s) + self.assertIn("checksum", s) + self.assertEqual(s["checksum"], "cdcda85605e46d0af6110752770dce3c") u = urlparse(uri) content, acl, blob = get_gcs_content_and_delete(u.hostname, u.path[1:] + path) self.assertEqual(content, data) - self.assertEqual(blob.metadata, {'foo': 'bar'}) + self.assertEqual(blob.metadata, {"foo": "bar"}) self.assertEqual(blob.cache_control, GCSFilesStore.CACHE_CONTROL) - self.assertEqual(blob.content_type, 'application/octet-stream') + self.assertEqual(blob.content_type, "application/octet-stream") self.assertIn(expected_policy, acl) @defer.inlineCallbacks @@ -525,17 +628,19 @@ class TestGCSFilesStore(unittest.TestCase): """ assert_gcs_environ() try: - import google.cloud.storage # noqa + import google.cloud.storage # noqa except ModuleNotFoundError: raise unittest.SkipTest("google-cloud-storage is not installed") else: - with mock.patch('google.cloud.storage') as _: - with mock.patch('scrapy.pipelines.files.time') as _: - uri = 'gs://my_bucket/my_prefix/' + with mock.patch("google.cloud.storage") as _: + with mock.patch("scrapy.pipelines.files.time") as _: + uri = "gs://my_bucket/my_prefix/" store = GCSFilesStore(uri) store.bucket = mock.Mock() - path = 'full/my_data.txt' - yield store.persist_file(path, mock.Mock(), info=None, meta=None, headers=None) + path = "full/my_data.txt" + yield store.persist_file( + path, mock.Mock(), info=None, meta=None, headers=None + ) yield store.stat_file(path, info=None) expected_blob_path = store.prefix + path store.bucket.blob.assert_called_with(expected_blob_path) @@ -545,26 +650,29 @@ class TestGCSFilesStore(unittest.TestCase): class TestFTPFileStore(unittest.TestCase): @defer.inlineCallbacks def test_persist(self): - uri = os.environ.get('FTP_TEST_FILE_URI') - if not uri: - raise unittest.SkipTest("No FTP URI available for testing") data = b"TestFTPFilesStore: \xe2\x98\x83" buf = BytesIO(data) - meta = {'foo': 'bar'} - path = 'full/filename' - store = FTPFilesStore(uri) - empty_dict = yield store.stat_file(path, info=None) - self.assertEqual(empty_dict, {}) - yield store.persist_file(path, buf, info=None, meta=meta, headers=None) - stat = yield store.stat_file(path, info=None) - self.assertIn('last_modified', stat) - self.assertIn('checksum', stat) - self.assertEqual(stat['checksum'], 'd113d66b2ec7258724a268bd88eef6b6') - path = f'{store.basedir}/{path}' - content = get_ftp_content_and_delete( - path, store.host, store.port, - store.username, store.password, store.USE_ACTIVE_MODE) - self.assertEqual(data.decode(), content) + meta = {"foo": "bar"} + path = "full/filename" + with MockFTPServer() as ftp_server: + store = FTPFilesStore(ftp_server.url("/")) + empty_dict = yield store.stat_file(path, info=None) + self.assertEqual(empty_dict, {}) + yield store.persist_file(path, buf, info=None, meta=meta, headers=None) + stat = yield store.stat_file(path, info=None) + self.assertIn("last_modified", stat) + self.assertIn("checksum", stat) + self.assertEqual(stat["checksum"], "d113d66b2ec7258724a268bd88eef6b6") + path = f"{store.basedir}/{path}" + content = get_ftp_content_and_delete( + path, + store.host, + store.port, + store.username, + store.password, + store.USE_ACTIVE_MODE, + ) + self.assertEqual(data, content) class ItemWithFiles(Item): @@ -574,15 +682,12 @@ class ItemWithFiles(Item): def _create_item_with_files(*files): item = ItemWithFiles() - item['file_urls'] = files + item["file_urls"] = files return item def _prepare_request_object(item_url, flags=None): return Request( item_url, - meta={'response': Response(item_url, status=200, body=b'data', flags=flags)}) - - -if __name__ == "__main__": - unittest.main() + meta={"response": Response(item_url, status=200, body=b"data", flags=flags)}, + ) diff --git a/tests/test_pipeline_images.py b/tests/test_pipeline_images.py index f98d40fda..2e2e06b89 100644 --- a/tests/test_pipeline_images.py +++ b/tests/test_pipeline_images.py @@ -5,6 +5,7 @@ import random import warnings from shutil import rmtree from tempfile import mkdtemp +from typing import Dict, List, Optional from unittest.mock import patch import attr @@ -18,31 +19,27 @@ from scrapy.pipelines.images import ImageException, ImagesPipeline, NoimagesDrop from scrapy.settings import Settings from scrapy.utils.python import to_bytes - +skip_pillow: Optional[str] try: from PIL import Image except ImportError: - skip_pillow = 'Missing Python Imaging Library, install https://pypi.python.org/pypi/Pillow' + skip_pillow = ( + "Missing Python Imaging Library, install https://pypi.python.org/pypi/Pillow" + ) else: - encoders = {'jpeg_encoder', 'jpeg_decoder'} - if not encoders.issubset(set(Image.core.__dict__)): - skip_pillow = 'Missing JPEG encoders' + encoders = {"jpeg_encoder", "jpeg_decoder"} + if not encoders.issubset(set(Image.core.__dict__)): # type: ignore[attr-defined] + skip_pillow = "Missing JPEG encoders" else: skip_pillow = None -def _mocked_download_func(request, info): - response = request.meta.get('response') - return response() if callable(response) else response - - class ImagesPipelineTestCase(unittest.TestCase): - skip = skip_pillow def setUp(self): self.tempdir = mkdtemp() - self.pipeline = ImagesPipeline(self.tempdir, download_func=_mocked_download_func) + self.pipeline = ImagesPipeline(self.tempdir) def tearDown(self): rmtree(self.tempdir) @@ -51,43 +48,75 @@ class ImagesPipelineTestCase(unittest.TestCase): file_path = self.pipeline.file_path self.assertEqual( file_path(Request("https://dev.mydeco.com/mydeco.gif")), - 'full/3fd165099d8e71b8a48b2683946e64dbfad8b52d.jpg') + "full/3fd165099d8e71b8a48b2683946e64dbfad8b52d.jpg", + ) self.assertEqual( - file_path(Request("http://www.maddiebrown.co.uk///catalogue-items//image_54642_12175_95307.jpg")), - 'full/0ffcd85d563bca45e2f90becd0ca737bc58a00b2.jpg') + file_path( + Request( + "http://www.maddiebrown.co.uk///catalogue-items//image_54642_12175_95307.jpg" + ) + ), + "full/0ffcd85d563bca45e2f90becd0ca737bc58a00b2.jpg", + ) self.assertEqual( - file_path(Request("https://dev.mydeco.com/two/dirs/with%20spaces%2Bsigns.gif")), - 'full/b250e3a74fff2e4703e310048a5b13eba79379d2.jpg') + file_path( + Request("https://dev.mydeco.com/two/dirs/with%20spaces%2Bsigns.gif") + ), + "full/b250e3a74fff2e4703e310048a5b13eba79379d2.jpg", + ) self.assertEqual( - file_path(Request("http://www.dfsonline.co.uk/get_prod_image.php?img=status_0907_mdm.jpg")), - 'full/4507be485f38b0da8a0be9eb2e1dfab8a19223f2.jpg') + file_path( + Request( + "http://www.dfsonline.co.uk/get_prod_image.php?img=status_0907_mdm.jpg" + ) + ), + "full/4507be485f38b0da8a0be9eb2e1dfab8a19223f2.jpg", + ) self.assertEqual( file_path(Request("http://www.dorma.co.uk/images/product_details/2532/")), - 'full/97ee6f8a46cbbb418ea91502fd24176865cf39b2.jpg') + "full/97ee6f8a46cbbb418ea91502fd24176865cf39b2.jpg", + ) self.assertEqual( file_path(Request("http://www.dorma.co.uk/images/product_details/2532")), - 'full/244e0dd7d96a3b7b01f54eded250c9e272577aa1.jpg') + "full/244e0dd7d96a3b7b01f54eded250c9e272577aa1.jpg", + ) self.assertEqual( - file_path(Request("http://www.dorma.co.uk/images/product_details/2532"), - response=Response("http://www.dorma.co.uk/images/product_details/2532"), - info=object()), - 'full/244e0dd7d96a3b7b01f54eded250c9e272577aa1.jpg') + file_path( + Request("http://www.dorma.co.uk/images/product_details/2532"), + response=Response("http://www.dorma.co.uk/images/product_details/2532"), + info=object(), + ), + "full/244e0dd7d96a3b7b01f54eded250c9e272577aa1.jpg", + ) def test_thumbnail_name(self): thumb_path = self.pipeline.thumb_path - name = '50' - self.assertEqual(thumb_path(Request("file:///tmp/foo.jpg"), name), - 'thumbs/50/38a86208c36e59d4404db9e37ce04be863ef0335.jpg') - self.assertEqual(thumb_path(Request("file://foo.png"), name), - 'thumbs/50/e55b765eba0ec7348e50a1df496040449071b96a.jpg') - self.assertEqual(thumb_path(Request("file:///tmp/foo"), name), - 'thumbs/50/0329ad83ebb8e93ea7c7906d46e9ed55f7349a50.jpg') - self.assertEqual(thumb_path(Request("file:///tmp/some.name/foo"), name), - 'thumbs/50/850233df65a5b83361798f532f1fc549cd13cbe9.jpg') - self.assertEqual(thumb_path(Request("file:///tmp/some.name/foo"), name, - response=Response("file:///tmp/some.name/foo"), - info=object()), - 'thumbs/50/850233df65a5b83361798f532f1fc549cd13cbe9.jpg') + name = "50" + self.assertEqual( + thumb_path(Request("file:///tmp/foo.jpg"), name), + "thumbs/50/38a86208c36e59d4404db9e37ce04be863ef0335.jpg", + ) + self.assertEqual( + thumb_path(Request("file://foo.png"), name), + "thumbs/50/e55b765eba0ec7348e50a1df496040449071b96a.jpg", + ) + self.assertEqual( + thumb_path(Request("file:///tmp/foo"), name), + "thumbs/50/0329ad83ebb8e93ea7c7906d46e9ed55f7349a50.jpg", + ) + self.assertEqual( + thumb_path(Request("file:///tmp/some.name/foo"), name), + "thumbs/50/850233df65a5b83361798f532f1fc549cd13cbe9.jpg", + ) + self.assertEqual( + thumb_path( + Request("file:///tmp/some.name/foo"), + name, + response=Response("file:///tmp/some.name/foo"), + info=object(), + ), + "thumbs/50/850233df65a5b83361798f532f1fc549cd13cbe9.jpg", + ) def test_thumbnail_name_from_item(self): """ @@ -95,23 +124,27 @@ class ImagesPipelineTestCase(unittest.TestCase): """ class CustomImagesPipeline(ImagesPipeline): - def thumb_path(self, request, thumb_id, response=None, info=None, item=None): + def thumb_path( + self, request, thumb_id, response=None, info=None, item=None + ): return f"thumb/{thumb_id}/{item.get('path')}" - thumb_path = CustomImagesPipeline.from_settings(Settings( - {'IMAGES_STORE': self.tempdir} - )).thumb_path - item = dict(path='path-to-store-file') + thumb_path = CustomImagesPipeline.from_settings( + Settings({"IMAGES_STORE": self.tempdir}) + ).thumb_path + item = dict(path="path-to-store-file") request = Request("http://example.com") - self.assertEqual(thumb_path(request, 'small', item=item), 'thumb/small/path-to-store-file') + self.assertEqual( + thumb_path(request, "small", item=item), "thumb/small/path-to-store-file" + ) def test_get_images_exception(self): self.pipeline.min_width = 100 self.pipeline.min_height = 100 - _, buf1 = _create_image('JPEG', 'RGB', (50, 50), (0, 0, 0)) - _, buf2 = _create_image('JPEG', 'RGB', (150, 50), (0, 0, 0)) - _, buf3 = _create_image('JPEG', 'RGB', (50, 150), (0, 0, 0)) + _, buf1 = _create_image("JPEG", "RGB", (50, 50), (0, 0, 0)) + _, buf2 = _create_image("JPEG", "RGB", (150, 50), (0, 0, 0)) + _, buf3 = _create_image("JPEG", "RGB", (50, 150), (0, 0, 0)) resp1 = Response(url="https://dev.mydeco.com/mydeco.gif", body=buf1.getvalue()) resp2 = Response(url="https://dev.mydeco.com/mydeco.gif", body=buf2.getvalue()) @@ -128,121 +161,154 @@ class ImagesPipelineTestCase(unittest.TestCase): def test_get_images_new(self): self.pipeline.min_width = 0 self.pipeline.min_height = 0 - self.pipeline.thumbs = {'small': (20, 20)} + self.pipeline.thumbs = {"small": (20, 20)} - orig_im, buf = _create_image('JPEG', 'RGB', (50, 50), (0, 0, 0)) - orig_thumb, orig_thumb_buf = _create_image('JPEG', 'RGB', (20, 20), (0, 0, 0)) + orig_im, buf = _create_image("JPEG", "RGB", (50, 50), (0, 0, 0)) + orig_thumb, orig_thumb_buf = _create_image("JPEG", "RGB", (20, 20), (0, 0, 0)) resp = Response(url="https://dev.mydeco.com/mydeco.gif", body=buf.getvalue()) req = Request(url="https://dev.mydeco.com/mydeco.gif") - get_images_gen = self.pipeline.get_images(response=resp, request=req, info=object()) + get_images_gen = self.pipeline.get_images( + response=resp, request=req, info=object() + ) path, new_im, new_buf = next(get_images_gen) - self.assertEqual(path, 'full/3fd165099d8e71b8a48b2683946e64dbfad8b52d.jpg') + self.assertEqual(path, "full/3fd165099d8e71b8a48b2683946e64dbfad8b52d.jpg") self.assertEqual(orig_im, new_im) self.assertEqual(buf.getvalue(), new_buf.getvalue()) thumb_path, thumb_img, thumb_buf = next(get_images_gen) - self.assertEqual(thumb_path, 'thumbs/small/3fd165099d8e71b8a48b2683946e64dbfad8b52d.jpg') + self.assertEqual( + thumb_path, "thumbs/small/3fd165099d8e71b8a48b2683946e64dbfad8b52d.jpg" + ) self.assertEqual(thumb_img, thumb_img) self.assertEqual(orig_thumb_buf.getvalue(), thumb_buf.getvalue()) def test_get_images_old(self): - self.pipeline.thumbs = {'small': (20, 20)} - orig_im, buf = _create_image('JPEG', 'RGB', (50, 50), (0, 0, 0)) + self.pipeline.thumbs = {"small": (20, 20)} + orig_im, buf = _create_image("JPEG", "RGB", (50, 50), (0, 0, 0)) resp = Response(url="https://dev.mydeco.com/mydeco.gif", body=buf.getvalue()) req = Request(url="https://dev.mydeco.com/mydeco.gif") def overridden_convert_image(image, size=None): - im, buf = _create_image('JPEG', 'RGB', (50, 50), (0, 0, 0)) + im, buf = _create_image("JPEG", "RGB", (50, 50), (0, 0, 0)) return im, buf - with patch.object(self.pipeline, 'convert_image', overridden_convert_image): + with patch.object(self.pipeline, "convert_image", overridden_convert_image): with warnings.catch_warnings(record=True) as w: - warnings.simplefilter('always') - get_images_gen = self.pipeline.get_images(response=resp, request=req, info=object()) + warnings.simplefilter("always") + get_images_gen = self.pipeline.get_images( + response=resp, request=req, info=object() + ) path, new_im, new_buf = next(get_images_gen) - self.assertEqual(path, 'full/3fd165099d8e71b8a48b2683946e64dbfad8b52d.jpg') + self.assertEqual( + path, "full/3fd165099d8e71b8a48b2683946e64dbfad8b52d.jpg" + ) self.assertEqual(orig_im.mode, new_im.mode) self.assertEqual(orig_im.getcolors(), new_im.getcolors()) self.assertEqual(buf.getvalue(), new_buf.getvalue()) thumb_path, thumb_img, thumb_buf = next(get_images_gen) - self.assertEqual(thumb_path, 'thumbs/small/3fd165099d8e71b8a48b2683946e64dbfad8b52d.jpg') + self.assertEqual( + thumb_path, + "thumbs/small/3fd165099d8e71b8a48b2683946e64dbfad8b52d.jpg", + ) self.assertEqual(orig_im.mode, thumb_img.mode) self.assertEqual(orig_im.getcolors(), thumb_img.getcolors()) self.assertEqual(buf.getvalue(), thumb_buf.getvalue()) - expected_warning_msg = ('.convert_image() method overriden in a deprecated way, ' - 'overriden method does not accept response_body argument.') - self.assertEqual(len([warning for warning in w if expected_warning_msg in str(warning.message)]), 1) + expected_warning_msg = ( + ".convert_image() method overridden in a deprecated way, " + "overridden method does not accept response_body argument." + ) + self.assertEqual( + len( + [ + warning + for warning in w + if expected_warning_msg in str(warning.message) + ] + ), + 1, + ) def test_convert_image_old(self): # tests for old API with warnings.catch_warnings(record=True) as w: - warnings.simplefilter('always') + warnings.simplefilter("always") SIZE = (100, 100) - # straigh forward case: RGB and JPEG + # straight forward case: RGB and JPEG COLOUR = (0, 127, 255) - im, _ = _create_image('JPEG', 'RGB', SIZE, COLOUR) + im, _ = _create_image("JPEG", "RGB", SIZE, COLOUR) converted, _ = self.pipeline.convert_image(im) - self.assertEqual(converted.mode, 'RGB') + self.assertEqual(converted.mode, "RGB") self.assertEqual(converted.getcolors(), [(10000, COLOUR)]) # check that thumbnail keep image ratio thumbnail, _ = self.pipeline.convert_image(converted, size=(10, 25)) - self.assertEqual(thumbnail.mode, 'RGB') + self.assertEqual(thumbnail.mode, "RGB") self.assertEqual(thumbnail.size, (10, 10)) # transparency case: RGBA and PNG COLOUR = (0, 127, 255, 50) - im, _ = _create_image('PNG', 'RGBA', SIZE, COLOUR) + im, _ = _create_image("PNG", "RGBA", SIZE, COLOUR) converted, _ = self.pipeline.convert_image(im) - self.assertEqual(converted.mode, 'RGB') + self.assertEqual(converted.mode, "RGB") self.assertEqual(converted.getcolors(), [(10000, (205, 230, 255))]) # transparency case with palette: P and PNG COLOUR = (0, 127, 255, 50) - im, _ = _create_image('PNG', 'RGBA', SIZE, COLOUR) - im = im.convert('P') + im, _ = _create_image("PNG", "RGBA", SIZE, COLOUR) + im = im.convert("P") converted, _ = self.pipeline.convert_image(im) - self.assertEqual(converted.mode, 'RGB') + self.assertEqual(converted.mode, "RGB") self.assertEqual(converted.getcolors(), [(10000, (205, 230, 255))]) - # ensure that we recieved deprecation warnings - expected_warning_msg = '.convert_image() method called in a deprecated way' - self.assertTrue(len([warning for warning in w if expected_warning_msg in str(warning.message)]) == 4) + # ensure that we received deprecation warnings + expected_warning_msg = ".convert_image() method called in a deprecated way" + self.assertTrue( + len( + [ + warning + for warning in w + if expected_warning_msg in str(warning.message) + ] + ) + == 4 + ) def test_convert_image_new(self): # tests for new API SIZE = (100, 100) - # straigh forward case: RGB and JPEG + # straight forward case: RGB and JPEG COLOUR = (0, 127, 255) - im, buf = _create_image('JPEG', 'RGB', SIZE, COLOUR) + im, buf = _create_image("JPEG", "RGB", SIZE, COLOUR) converted, converted_buf = self.pipeline.convert_image(im, response_body=buf) - self.assertEqual(converted.mode, 'RGB') + self.assertEqual(converted.mode, "RGB") self.assertEqual(converted.getcolors(), [(10000, COLOUR)]) # check that we don't convert JPEGs again self.assertEqual(converted_buf, buf) # check that thumbnail keep image ratio - thumbnail, _ = self.pipeline.convert_image(converted, size=(10, 25), response_body=converted_buf) - self.assertEqual(thumbnail.mode, 'RGB') + thumbnail, _ = self.pipeline.convert_image( + converted, size=(10, 25), response_body=converted_buf + ) + self.assertEqual(thumbnail.mode, "RGB") self.assertEqual(thumbnail.size, (10, 10)) # transparency case: RGBA and PNG COLOUR = (0, 127, 255, 50) - im, buf = _create_image('PNG', 'RGBA', SIZE, COLOUR) + im, buf = _create_image("PNG", "RGBA", SIZE, COLOUR) converted, _ = self.pipeline.convert_image(im, response_body=buf) - self.assertEqual(converted.mode, 'RGB') + self.assertEqual(converted.mode, "RGB") self.assertEqual(converted.getcolors(), [(10000, (205, 230, 255))]) # transparency case with palette: P and PNG COLOUR = (0, 127, 255, 50) - im, buf = _create_image('PNG', 'RGBA', SIZE, COLOUR) - im = im.convert('P') + im, buf = _create_image("PNG", "RGBA", SIZE, COLOUR) + im = im.convert("P") converted, _ = self.pipeline.convert_image(im, response_body=buf) - self.assertEqual(converted.mode, 'RGB') + self.assertEqual(converted.mode, "RGB") self.assertEqual(converted.getcolors(), [(10000, (205, 230, 255))]) @@ -252,47 +318,54 @@ class DeprecatedImagesPipeline(ImagesPipeline): def image_key(self, url): image_guid = hashlib.sha1(to_bytes(url)).hexdigest() - return f'empty/{image_guid}.jpg' + return f"empty/{image_guid}.jpg" def thumb_key(self, url, thumb_id): thumb_guid = hashlib.sha1(to_bytes(url)).hexdigest() - return f'thumbsup/{thumb_id}/{thumb_guid}.jpg' + return f"thumbsup/{thumb_id}/{thumb_guid}.jpg" class ImagesPipelineTestCaseFieldsMixin: - skip = skip_pillow def test_item_fields_default(self): - url = 'http://www.example.com/images/1.jpg' - item = self.item_class(name='item1', image_urls=[url]) - pipeline = ImagesPipeline.from_settings(Settings({'IMAGES_STORE': 's3://example/images/'})) + url = "http://www.example.com/images/1.jpg" + item = self.item_class(name="item1", image_urls=[url]) + pipeline = ImagesPipeline.from_settings( + Settings({"IMAGES_STORE": "s3://example/images/"}) + ) requests = list(pipeline.get_media_requests(item, None)) self.assertEqual(requests[0].url, url) - results = [(True, {'url': url})] + results = [(True, {"url": url})] item = pipeline.item_completed(results, item, None) images = ItemAdapter(item).get("images") self.assertEqual(images, [results[0][1]]) self.assertIsInstance(item, self.item_class) def test_item_fields_override_settings(self): - url = 'http://www.example.com/images/1.jpg' - item = self.item_class(name='item1', custom_image_urls=[url]) - pipeline = ImagesPipeline.from_settings(Settings({ - 'IMAGES_STORE': 's3://example/images/', - 'IMAGES_URLS_FIELD': 'custom_image_urls', - 'IMAGES_RESULT_FIELD': 'custom_images' - })) + url = "http://www.example.com/images/1.jpg" + item = self.item_class(name="item1", custom_image_urls=[url]) + pipeline = ImagesPipeline.from_settings( + Settings( + { + "IMAGES_STORE": "s3://example/images/", + "IMAGES_URLS_FIELD": "custom_image_urls", + "IMAGES_RESULT_FIELD": "custom_images", + } + ) + ) requests = list(pipeline.get_media_requests(item, None)) self.assertEqual(requests[0].url, url) - results = [(True, {'url': url})] + results = [(True, {"url": url})] item = pipeline.item_completed(results, item, None) custom_images = ItemAdapter(item).get("custom_images") self.assertEqual(custom_images, [results[0][1]]) self.assertIsInstance(item, self.item_class) -class ImagesPipelineTestCaseFieldsDict(ImagesPipelineTestCaseFieldsMixin, unittest.TestCase): +class ImagesPipelineTestCaseFieldsDict( + ImagesPipelineTestCaseFieldsMixin, unittest.TestCase +): item_class = dict @@ -306,7 +379,9 @@ class ImagesPipelineTestItem(Item): custom_images = Field() -class ImagesPipelineTestCaseFieldsItem(ImagesPipelineTestCaseFieldsMixin, unittest.TestCase): +class ImagesPipelineTestCaseFieldsItem( + ImagesPipelineTestCaseFieldsMixin, unittest.TestCase +): item_class = ImagesPipelineTestItem @@ -321,7 +396,9 @@ class ImagesPipelineTestDataClass: custom_images: list = dataclasses.field(default_factory=list) -class ImagesPipelineTestCaseFieldsDataClass(ImagesPipelineTestCaseFieldsMixin, unittest.TestCase): +class ImagesPipelineTestCaseFieldsDataClass( + ImagesPipelineTestCaseFieldsMixin, unittest.TestCase +): item_class = ImagesPipelineTestDataClass @@ -329,19 +406,20 @@ class ImagesPipelineTestCaseFieldsDataClass(ImagesPipelineTestCaseFieldsMixin, u class ImagesPipelineTestAttrsItem: name = attr.ib(default="") # default fields - image_urls = attr.ib(default=lambda: []) - images = attr.ib(default=lambda: []) + image_urls: List[str] = attr.ib(default=lambda: []) + images: List[Dict[str, str]] = attr.ib(default=lambda: []) # overridden fields - custom_image_urls = attr.ib(default=lambda: []) - custom_images = attr.ib(default=lambda: []) + custom_image_urls: List[str] = attr.ib(default=lambda: []) + custom_images: List[Dict[str, str]] = attr.ib(default=lambda: []) -class ImagesPipelineTestCaseFieldsAttrsItem(ImagesPipelineTestCaseFieldsMixin, unittest.TestCase): +class ImagesPipelineTestCaseFieldsAttrsItem( + ImagesPipelineTestCaseFieldsMixin, unittest.TestCase +): item_class = ImagesPipelineTestAttrsItem class ImagesPipelineTestCaseCustomSettings(unittest.TestCase): - skip = skip_pillow img_cls_attribute_names = [ @@ -351,7 +429,7 @@ class ImagesPipelineTestCaseCustomSettings(unittest.TestCase): ("MIN_HEIGHT", "IMAGES_MIN_HEIGHT"), ("IMAGES_URLS_FIELD", "IMAGES_URLS_FIELD"), ("IMAGES_RESULT_FIELD", "IMAGES_RESULT_FIELD"), - ("THUMBS", "IMAGES_THUMBS") + ("THUMBS", "IMAGES_THUMBS"), ] # This should match what is defined in ImagesPipeline. @@ -360,8 +438,8 @@ class ImagesPipelineTestCaseCustomSettings(unittest.TestCase): MIN_HEIGHT=0, EXPIRES=90, THUMBS={}, - IMAGES_URLS_FIELD='image_urls', - IMAGES_RESULT_FIELD='images' + IMAGES_URLS_FIELD="image_urls", + IMAGES_RESULT_FIELD="images", ) def setUp(self): @@ -387,26 +465,30 @@ class ImagesPipelineTestCaseCustomSettings(unittest.TestCase): "IMAGES_MIN_WIDTH": random.randint(1, 1000), "IMAGES_MIN_HEIGHT": random.randint(1, 1000), "IMAGES_THUMBS": { - 'small': (random.randint(1, 1000), random.randint(1, 1000)), - 'big': (random.randint(1, 1000), random.randint(1, 1000)) - } + "small": (random.randint(1, 1000), random.randint(1, 1000)), + "big": (random.randint(1, 1000), random.randint(1, 1000)), + }, } if not prefix: return settings - return {prefix.upper() + "_" + k if k != "IMAGES_STORE" else k: v for k, v in settings.items()} + return { + prefix.upper() + "_" + k if k != "IMAGES_STORE" else k: v + for k, v in settings.items() + } def _generate_fake_pipeline_subclass(self): """ :return: ImagePipeline class will all uppercase attributes set. """ + class UserDefinedImagePipeline(ImagesPipeline): # Values should be in different range than fake_settings. MIN_WIDTH = random.randint(1000, 2000) MIN_HEIGHT = random.randint(1000, 2000) THUMBS = { - 'small': (random.randint(1000, 2000), random.randint(1000, 2000)), - 'big': (random.randint(1000, 2000), random.randint(1000, 2000)) + "small": (random.randint(1000, 2000), random.randint(1000, 2000)), + "big": (random.randint(1000, 2000), random.randint(1000, 2000)), } EXPIRES = random.randint(1000, 2000) IMAGES_URLS_FIELD = "field_one" @@ -427,7 +509,9 @@ class ImagesPipelineTestCaseCustomSettings(unittest.TestCase): expected_default_value = self.default_pipeline_settings.get(pipe_attr) custom_value = custom_settings.get(settings_attr) self.assertNotEqual(expected_default_value, custom_value) - self.assertEqual(getattr(default_sts_pipe, pipe_attr.lower()), expected_default_value) + self.assertEqual( + getattr(default_sts_pipe, pipe_attr.lower()), expected_default_value + ) self.assertEqual(getattr(user_sts_pipe, pipe_attr.lower()), custom_value) def test_subclass_attrs_preserved_default_settings(self): @@ -464,10 +548,13 @@ class ImagesPipelineTestCaseCustomSettings(unittest.TestCase): If there are no settings for subclass and no subclass attributes, pipeline should use attributes of base class. """ + class UserDefinedImagePipeline(ImagesPipeline): pass - user_pipeline = UserDefinedImagePipeline.from_settings(Settings({"IMAGES_STORE": self.tempdir})) + user_pipeline = UserDefinedImagePipeline.from_settings( + Settings({"IMAGES_STORE": self.tempdir}) + ) for pipe_attr, settings_attr in self.img_cls_attribute_names: # Values from settings for custom pipeline should be set on pipeline instance. custom_value = self.default_pipeline_settings.get(pipe_attr.upper()) @@ -478,6 +565,7 @@ class ImagesPipelineTestCaseCustomSettings(unittest.TestCase): If there are custom settings for subclass and NO class attributes, pipeline should use custom settings. """ + class UserDefinedImagePipeline(ImagesPipeline): pass @@ -508,9 +596,18 @@ class ImagesPipelineTestCaseCustomSettings(unittest.TestCase): class UserDefinedImagePipeline(ImagesPipeline): DEFAULT_IMAGES_URLS_FIELD = "something" DEFAULT_IMAGES_RESULT_FIELD = "something_else" - pipeline = UserDefinedImagePipeline.from_settings(Settings({"IMAGES_STORE": self.tempdir})) - self.assertEqual(pipeline.images_result_field, "something_else") - self.assertEqual(pipeline.images_urls_field, "something") + + pipeline = UserDefinedImagePipeline.from_settings( + Settings({"IMAGES_STORE": self.tempdir}) + ) + self.assertEqual( + pipeline.images_result_field, + UserDefinedImagePipeline.DEFAULT_IMAGES_RESULT_FIELD, + ) + self.assertEqual( + pipeline.images_urls_field, + UserDefinedImagePipeline.DEFAULT_IMAGES_URLS_FIELD, + ) def test_user_defined_subclass_default_key_names(self): """Test situation when user defines subclass of ImagePipeline, @@ -526,12 +623,10 @@ class ImagesPipelineTestCaseCustomSettings(unittest.TestCase): for pipe_attr, settings_attr in self.img_cls_attribute_names: expected_value = settings.get(settings_attr) - self.assertEqual(getattr(pipeline_cls, pipe_attr.lower()), - expected_value) + self.assertEqual(getattr(pipeline_cls, pipe_attr.lower()), expected_value) class NoimagesDropTestCase(unittest.TestCase): - def test_deprecation_warning(self): arg = str() with warnings.catch_warnings(record=True) as w: @@ -539,8 +634,10 @@ class NoimagesDropTestCase(unittest.TestCase): self.assertEqual(len(w), 1) self.assertEqual(w[0].category, ScrapyDeprecationWarning) with warnings.catch_warnings(record=True) as w: + class SubclassedNoimagesDrop(NoimagesDrop): pass + SubclassedNoimagesDrop(arg) self.assertEqual(len(w), 1) self.assertEqual(w[0].category, ScrapyDeprecationWarning) @@ -551,7 +648,3 @@ def _create_image(format, *a, **kw): Image.new(*a, **kw).save(buf, format) buf.seek(0) return Image.open(buf), buf - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_pipeline_media.py b/tests/test_pipeline_media.py index 0a94ae699..820484565 100644 --- a/tests/test_pipeline_media.py +++ b/tests/test_pipeline_media.py @@ -1,46 +1,47 @@ -from typing import Optional import io +from typing import Optional from testfixtures import LogCapture -from twisted.trial import unittest -from twisted.python.failure import Failure from twisted.internet import reactor from twisted.internet.defer import Deferred, inlineCallbacks +from twisted.python.failure import Failure +from twisted.trial import unittest from scrapy import signals from scrapy.http import Request, Response -from scrapy.settings import Settings -from scrapy.spiders import Spider +from scrapy.http.request import NO_CALLBACK from scrapy.pipelines.files import FileException from scrapy.pipelines.images import ImagesPipeline from scrapy.pipelines.media import MediaPipeline -from scrapy.utils.deprecate import ScrapyDeprecationWarning +from scrapy.settings import Settings +from scrapy.spiders import Spider from scrapy.utils.log import failure_to_exc_info from scrapy.utils.signal import disconnect_all from scrapy.utils.test import get_crawler - try: from PIL import Image # noqa: imported just to check for the import error except ImportError: - skip_pillow: Optional[str] = 'Missing Python Imaging Library, install https://pypi.python.org/pypi/Pillow' + skip_pillow: Optional[ + str + ] = "Missing Python Imaging Library, install https://pypi.python.org/pypi/Pillow" else: skip_pillow = None def _mocked_download_func(request, info): - response = request.meta.get('response') + assert request.callback is NO_CALLBACK + response = request.meta.get("response") return response() if callable(response) else response class BaseMediaPipelineTestCase(unittest.TestCase): - pipeline_class = MediaPipeline settings = None def setUp(self): spider_cls = Spider - self.spider = spider_cls('media.com') + self.spider = spider_cls("media.com") crawler = get_crawler(spider_cls, self.settings) self.pipe = self.pipeline_class.from_crawler(crawler) self.pipe.download_func = _mocked_download_func @@ -50,29 +51,29 @@ class BaseMediaPipelineTestCase(unittest.TestCase): def tearDown(self): for name, signal in vars(signals).items(): - if not name.startswith('_'): + if not name.startswith("_"): disconnect_all(signal) def test_default_media_to_download(self): - request = Request('http://url') + request = Request("http://url") assert self.pipe.media_to_download(request, self.info) is None def test_default_get_media_requests(self): - item = dict(name='name') + item = dict(name="name") assert self.pipe.get_media_requests(item, self.info) is None def test_default_media_downloaded(self): - request = Request('http://url') - response = Response('http://url', body=b'') + request = Request("http://url") + response = Response("http://url", body=b"") assert self.pipe.media_downloaded(response, request, self.info) is response def test_default_media_failed(self): - request = Request('http://url') + request = Request("http://url") fail = Failure(Exception()) assert self.pipe.media_failed(fail, request, self.info) is fail def test_default_item_completed(self): - item = dict(name='name') + item = dict(name="name") assert self.pipe.item_completed([], item, self.info) is item # Check that failures are logged by default @@ -85,7 +86,7 @@ class BaseMediaPipelineTestCase(unittest.TestCase): assert new_item is item assert len(log.records) == 1 record = log.records[0] - assert record.levelname == 'ERROR' + assert record.levelname == "ERROR" self.assertTupleEqual(record.exc_info, failure_to_exc_info(fail)) # disable failure logging and check again @@ -97,14 +98,14 @@ class BaseMediaPipelineTestCase(unittest.TestCase): @inlineCallbacks def test_default_process_item(self): - item = dict(name='name') + item = dict(name="name") new_item = yield self.pipe.process_item(item, self.spider) assert new_item is item def test_modify_media_request(self): - request = Request('http://url') + request = Request("http://url") self.pipe._modify_media_request(request) - assert request.meta == {'handle_httpstatus_all': True} + assert request.meta == {"handle_httpstatus_all": True} def test_should_remove_req_res_references_before_caching_the_results(self): """Regression test case to prevent a memory leak in the Media Pipeline. @@ -134,8 +135,8 @@ class BaseMediaPipelineTestCase(unittest.TestCase): Chaining (https://www.python.org/dev/peps/pep-3134/). """ # Create sample pair of Request and Response objects - request = Request('http://url') - response = Response('http://url', body=b'', request=request) + request = Request("http://url") + response = Response("http://url", body=b"", request=request) # Simulate the Media Pipeline behavior to produce a Twisted Failure try: @@ -146,7 +147,7 @@ class BaseMediaPipelineTestCase(unittest.TestCase): try: # Simulate the media_downloaded callback raising a FileException # This usually happens when the status code is not 200 OK - raise FileException('download-error') + raise FileException("download-error") except Exception as exc: file_exc = exc # Simulate Twisted capturing the FileException @@ -171,116 +172,136 @@ class BaseMediaPipelineTestCase(unittest.TestCase): # ... encapsulating the original FileException ... self.assertEqual(info.downloaded[fp].value, file_exc) # ... but it should not store the StopIteration exception on its context - context = getattr(info.downloaded[fp].value, '__context__', None) + context = getattr(info.downloaded[fp].value, "__context__", None) self.assertIsNone(context) class MockedMediaPipeline(MediaPipeline): - def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._mockcalled = [] def download(self, request, info): - self._mockcalled.append('download') + self._mockcalled.append("download") return super().download(request, info) def media_to_download(self, request, info, *, item=None): - self._mockcalled.append('media_to_download') - if 'result' in request.meta: - return request.meta.get('result') + self._mockcalled.append("media_to_download") + if "result" in request.meta: + return request.meta.get("result") return super().media_to_download(request, info) def get_media_requests(self, item, info): - self._mockcalled.append('get_media_requests') - return item.get('requests') + self._mockcalled.append("get_media_requests") + return item.get("requests") def media_downloaded(self, response, request, info, *, item=None): - self._mockcalled.append('media_downloaded') + self._mockcalled.append("media_downloaded") return super().media_downloaded(response, request, info) def media_failed(self, failure, request, info): - self._mockcalled.append('media_failed') + self._mockcalled.append("media_failed") return super().media_failed(failure, request, info) def item_completed(self, results, item, info): - self._mockcalled.append('item_completed') + self._mockcalled.append("item_completed") item = super().item_completed(results, item, info) - item['results'] = results + item["results"] = results return item class MediaPipelineTestCase(BaseMediaPipelineTestCase): - pipeline_class = MockedMediaPipeline def _callback(self, result): - self.pipe._mockcalled.append('request_callback') + self.pipe._mockcalled.append("request_callback") return result def _errback(self, result): - self.pipe._mockcalled.append('request_errback') + self.pipe._mockcalled.append("request_errback") return result @inlineCallbacks def test_result_succeed(self): - rsp = Response('http://url1') - req = Request('http://url1', meta=dict(response=rsp), - callback=self._callback, errback=self._errback) + rsp = Response("http://url1") + req = Request( + "http://url1", + meta=dict(response=rsp), + callback=self._callback, + errback=self._errback, + ) item = dict(requests=req) new_item = yield self.pipe.process_item(item, self.spider) - self.assertEqual(new_item['results'], [(True, rsp)]) + self.assertEqual(new_item["results"], [(True, rsp)]) self.assertEqual( self.pipe._mockcalled, - ['get_media_requests', 'media_to_download', 'media_downloaded', 'request_callback', 'item_completed']) + [ + "get_media_requests", + "media_to_download", + "media_downloaded", + "request_callback", + "item_completed", + ], + ) @inlineCallbacks def test_result_failure(self): self.pipe.LOG_FAILED_RESULTS = False fail = Failure(Exception()) - req = Request('http://url1', meta=dict(response=fail), - callback=self._callback, errback=self._errback) + req = Request( + "http://url1", + meta=dict(response=fail), + callback=self._callback, + errback=self._errback, + ) item = dict(requests=req) new_item = yield self.pipe.process_item(item, self.spider) - self.assertEqual(new_item['results'], [(False, fail)]) + self.assertEqual(new_item["results"], [(False, fail)]) self.assertEqual( self.pipe._mockcalled, - ['get_media_requests', 'media_to_download', 'media_failed', 'request_errback', 'item_completed']) + [ + "get_media_requests", + "media_to_download", + "media_failed", + "request_errback", + "item_completed", + ], + ) @inlineCallbacks def test_mix_of_success_and_failure(self): self.pipe.LOG_FAILED_RESULTS = False - rsp1 = Response('http://url1') - req1 = Request('http://url1', meta=dict(response=rsp1)) + rsp1 = Response("http://url1") + req1 = Request("http://url1", meta=dict(response=rsp1)) fail = Failure(Exception()) - req2 = Request('http://url2', meta=dict(response=fail)) + req2 = Request("http://url2", meta=dict(response=fail)) item = dict(requests=[req1, req2]) new_item = yield self.pipe.process_item(item, self.spider) - self.assertEqual(new_item['results'], [(True, rsp1), (False, fail)]) + self.assertEqual(new_item["results"], [(True, rsp1), (False, fail)]) m = self.pipe._mockcalled # only once - self.assertEqual(m[0], 'get_media_requests') # first hook called - self.assertEqual(m.count('get_media_requests'), 1) - self.assertEqual(m.count('item_completed'), 1) - self.assertEqual(m[-1], 'item_completed') # last hook called + self.assertEqual(m[0], "get_media_requests") # first hook called + self.assertEqual(m.count("get_media_requests"), 1) + self.assertEqual(m.count("item_completed"), 1) + self.assertEqual(m[-1], "item_completed") # last hook called # twice, one per request - self.assertEqual(m.count('media_to_download'), 2) + self.assertEqual(m.count("media_to_download"), 2) # one to handle success and other for failure - self.assertEqual(m.count('media_downloaded'), 1) - self.assertEqual(m.count('media_failed'), 1) + self.assertEqual(m.count("media_downloaded"), 1) + self.assertEqual(m.count("media_failed"), 1) @inlineCallbacks def test_get_media_requests(self): # returns single Request (without callback) - req = Request('http://url') + req = Request("http://url") item = dict(requests=req) # pass a single item new_item = yield self.pipe.process_item(item, self.spider) assert new_item is item self.assertIn(self.fingerprint(req), self.info.downloaded) # returns iterable of Requests - req1 = Request('http://url1') - req2 = Request('http://url2') + req1 = Request("http://url1") + req2 = Request("http://url2") item = dict(requests=iter([req1, req2])) new_item = yield self.pipe.process_item(item, self.spider) assert new_item is item @@ -289,30 +310,34 @@ class MediaPipelineTestCase(BaseMediaPipelineTestCase): @inlineCallbacks def test_results_are_cached_across_multiple_items(self): - rsp1 = Response('http://url1') - req1 = Request('http://url1', meta=dict(response=rsp1)) + rsp1 = Response("http://url1") + req1 = Request("http://url1", meta=dict(response=rsp1)) item = dict(requests=req1) new_item = yield self.pipe.process_item(item, self.spider) self.assertTrue(new_item is item) - self.assertEqual(new_item['results'], [(True, rsp1)]) + self.assertEqual(new_item["results"], [(True, rsp1)]) # rsp2 is ignored, rsp1 must be in results because request fingerprints are the same - req2 = Request(req1.url, meta=dict(response=Response('http://donot.download.me'))) + req2 = Request( + req1.url, meta=dict(response=Response("http://donot.download.me")) + ) item = dict(requests=req2) new_item = yield self.pipe.process_item(item, self.spider) self.assertTrue(new_item is item) self.assertEqual(self.fingerprint(req1), self.fingerprint(req2)) - self.assertEqual(new_item['results'], [(True, rsp1)]) + self.assertEqual(new_item["results"], [(True, rsp1)]) @inlineCallbacks def test_results_are_cached_for_requests_of_single_item(self): - rsp1 = Response('http://url1') - req1 = Request('http://url1', meta=dict(response=rsp1)) - req2 = Request(req1.url, meta=dict(response=Response('http://donot.download.me'))) + rsp1 = Response("http://url1") + req1 = Request("http://url1", meta=dict(response=rsp1)) + req2 = Request( + req1.url, meta=dict(response=Response("http://donot.download.me")) + ) item = dict(requests=[req1, req2]) new_item = yield self.pipe.process_item(item, self.spider) self.assertTrue(new_item is item) - self.assertEqual(new_item['results'], [(True, rsp1), (True, rsp1)]) + self.assertEqual(new_item["results"], [(True, rsp1), (True, rsp1)]) @inlineCallbacks def test_wait_if_request_is_downloading(self): @@ -324,227 +349,122 @@ class MediaPipelineTestCase(BaseMediaPipelineTestCase): self.assertEqual(len(self.info.waiting[fp]), 2) return response - rsp1 = Response('http://url') + rsp1 = Response("http://url") def rsp1_func(): dfd = Deferred().addCallback(_check_downloading) - reactor.callLater(.1, dfd.callback, rsp1) + reactor.callLater(0.1, dfd.callback, rsp1) return dfd def rsp2_func(): - self.fail('it must cache rsp1 result and must not try to redownload') + self.fail("it must cache rsp1 result and must not try to redownload") - req1 = Request('http://url', meta=dict(response=rsp1_func)) + req1 = Request("http://url", meta=dict(response=rsp1_func)) req2 = Request(req1.url, meta=dict(response=rsp2_func)) item = dict(requests=[req1, req2]) new_item = yield self.pipe.process_item(item, self.spider) - self.assertEqual(new_item['results'], [(True, rsp1), (True, rsp1)]) + self.assertEqual(new_item["results"], [(True, rsp1), (True, rsp1)]) @inlineCallbacks def test_use_media_to_download_result(self): - req = Request('http://url', meta=dict(result='ITSME', response=self.fail)) + req = Request("http://url", meta=dict(result="ITSME", response=self.fail)) item = dict(requests=req) new_item = yield self.pipe.process_item(item, self.spider) - self.assertEqual(new_item['results'], [(True, 'ITSME')]) + self.assertEqual(new_item["results"], [(True, "ITSME")]) self.assertEqual( self.pipe._mockcalled, - ['get_media_requests', 'media_to_download', 'item_completed']) + ["get_media_requests", "media_to_download", "item_completed"], + ) class MockedMediaPipelineDeprecatedMethods(ImagesPipeline): - def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._mockcalled = [] def get_media_requests(self, item, info): - item_url = item['image_urls'][0] + item_url = item["image_urls"][0] output_img = io.BytesIO() - img = Image.new('RGB', (60, 30), color='red') - img.save(output_img, format='JPEG') + img = Image.new("RGB", (60, 30), color="red") + img.save(output_img, format="JPEG") return Request( item_url, - meta={'response': Response(item_url, status=200, body=output_img.getvalue())} + meta={ + "response": Response(item_url, status=200, body=output_img.getvalue()) + }, ) def inc_stats(self, *args, **kwargs): return True def media_to_download(self, request, info): - self._mockcalled.append('media_to_download') + self._mockcalled.append("media_to_download") return super().media_to_download(request, info) def media_downloaded(self, response, request, info): - self._mockcalled.append('media_downloaded') + self._mockcalled.append("media_downloaded") return super().media_downloaded(response, request, info) def file_downloaded(self, response, request, info): - self._mockcalled.append('file_downloaded') + self._mockcalled.append("file_downloaded") return super().file_downloaded(response, request, info) def file_path(self, request, response=None, info=None): - self._mockcalled.append('file_path') + self._mockcalled.append("file_path") return super().file_path(request, response, info) def thumb_path(self, request, thumb_id, response=None, info=None): - self._mockcalled.append('thumb_path') + self._mockcalled.append("thumb_path") return super().thumb_path(request, thumb_id, response, info) def get_images(self, response, request, info): - self._mockcalled.append('get_images') + self._mockcalled.append("get_images") return super().get_images(response, request, info) def image_downloaded(self, response, request, info): - self._mockcalled.append('image_downloaded') + self._mockcalled.append("image_downloaded") return super().image_downloaded(response, request, info) -class MediaPipelineDeprecatedMethodsTestCase(unittest.TestCase): - skip = skip_pillow - - def setUp(self): - settings_dict = { - 'IMAGES_STORE': 'store-uri', - 'IMAGES_THUMBS': {'small': (50, 50)}, - } - crawler = get_crawler(spidercls=None, settings_dict=settings_dict) - self.pipe = MockedMediaPipelineDeprecatedMethods.from_crawler(crawler) - self.pipe.download_func = _mocked_download_func - self.pipe.open_spider(None) - self.item = dict(image_urls=['http://picsum.photos/id/1014/200/300'], images=[]) - - def _assert_method_called_with_warnings(self, method, message, warnings): - self.assertIn(method, self.pipe._mockcalled) - warningShown = False - for warning in warnings: - if warning['message'] == message and warning['category'] == ScrapyDeprecationWarning: - warningShown = True - self.assertTrue(warningShown) - - @inlineCallbacks - def test_media_to_download_called(self): - yield self.pipe.process_item(self.item, None) - warnings = self.flushWarnings([MediaPipeline._compatible]) - message = ( - 'media_to_download(self, request, info) is deprecated, ' - 'please use media_to_download(self, request, info, *, item=None)' - ) - self._assert_method_called_with_warnings('media_to_download', message, warnings) - - @inlineCallbacks - def test_media_downloaded_called(self): - yield self.pipe.process_item(self.item, None) - warnings = self.flushWarnings([MediaPipeline._compatible]) - message = ( - 'media_downloaded(self, response, request, info) is deprecated, ' - 'please use media_downloaded(self, response, request, info, *, item=None)' - ) - self._assert_method_called_with_warnings('media_downloaded', message, warnings) - - @inlineCallbacks - def test_file_downloaded_called(self): - yield self.pipe.process_item(self.item, None) - warnings = self.flushWarnings([MediaPipeline._compatible]) - message = ( - 'file_downloaded(self, response, request, info) is deprecated, ' - 'please use file_downloaded(self, response, request, info, *, item=None)' - ) - self._assert_method_called_with_warnings('file_downloaded', message, warnings) - - @inlineCallbacks - def test_file_path_called(self): - yield self.pipe.process_item(self.item, None) - warnings = self.flushWarnings([MediaPipeline._compatible]) - message = ( - 'file_path(self, request, response=None, info=None) is deprecated, ' - 'please use file_path(self, request, response=None, info=None, *, item=None)' - ) - self._assert_method_called_with_warnings('file_path', message, warnings) - - @inlineCallbacks - def test_thumb_path_called(self): - yield self.pipe.process_item(self.item, None) - warnings = self.flushWarnings([MediaPipeline._compatible]) - message = ( - 'thumb_path(self, request, thumb_id, response=None, info=None) is deprecated, ' - 'please use thumb_path(self, request, thumb_id, response=None, info=None, *, item=None)' - ) - self._assert_method_called_with_warnings('thumb_path', message, warnings) - - @inlineCallbacks - def test_get_images_called(self): - yield self.pipe.process_item(self.item, None) - warnings = self.flushWarnings([MediaPipeline._compatible]) - message = ( - 'get_images(self, response, request, info) is deprecated, ' - 'please use get_images(self, response, request, info, *, item=None)' - ) - self._assert_method_called_with_warnings('get_images', message, warnings) - - @inlineCallbacks - def test_image_downloaded_called(self): - yield self.pipe.process_item(self.item, None) - warnings = self.flushWarnings([MediaPipeline._compatible]) - message = ( - 'image_downloaded(self, response, request, info) is deprecated, ' - 'please use image_downloaded(self, response, request, info, *, item=None)' - ) - self._assert_method_called_with_warnings('image_downloaded', message, warnings) - - class MediaPipelineAllowRedirectSettingsTestCase(unittest.TestCase): - def _assert_request_no3xx(self, pipeline_class, settings): pipe = pipeline_class(settings=Settings(settings)) - request = Request('http://url') + request = Request("http://url") pipe._modify_media_request(request) - self.assertIn('handle_httpstatus_list', request.meta) + self.assertIn("handle_httpstatus_list", request.meta) for status, check in [ - (200, True), - - # These are the status codes we want - # the downloader to handle itself - (301, False), - (302, False), - (302, False), - (307, False), - (308, False), - - # we still want to get 4xx and 5xx - (400, True), - (404, True), - (500, True)]: + (200, True), + # These are the status codes we want + # the downloader to handle itself + (301, False), + (302, False), + (302, False), + (307, False), + (308, False), + # we still want to get 4xx and 5xx + (400, True), + (404, True), + (500, True), + ]: if check: - self.assertIn(status, request.meta['handle_httpstatus_list']) + self.assertIn(status, request.meta["handle_httpstatus_list"]) else: - self.assertNotIn(status, request.meta['handle_httpstatus_list']) + self.assertNotIn(status, request.meta["handle_httpstatus_list"]) def test_standard_setting(self): - self._assert_request_no3xx( - MediaPipeline, - { - 'MEDIA_ALLOW_REDIRECTS': True - }) + self._assert_request_no3xx(MediaPipeline, {"MEDIA_ALLOW_REDIRECTS": True}) def test_subclass_standard_setting(self): - class UserDefinedPipeline(MediaPipeline): pass - self._assert_request_no3xx( - UserDefinedPipeline, - { - 'MEDIA_ALLOW_REDIRECTS': True - }) + self._assert_request_no3xx(UserDefinedPipeline, {"MEDIA_ALLOW_REDIRECTS": True}) def test_subclass_specific_setting(self): - class UserDefinedPipeline(MediaPipeline): pass self._assert_request_no3xx( - UserDefinedPipeline, - { - 'USERDEFINEDPIPELINE_MEDIA_ALLOW_REDIRECTS': True - }) + UserDefinedPipeline, {"USERDEFINEDPIPELINE_MEDIA_ALLOW_REDIRECTS": True} + ) diff --git a/tests/test_pipelines.py b/tests/test_pipelines.py index 8e432b913..5ab288c1a 100644 --- a/tests/test_pipelines.py +++ b/tests/test_pipelines.py @@ -5,22 +5,21 @@ from twisted.internet import defer from twisted.internet.defer import Deferred from twisted.trial import unittest -from scrapy import Spider, signals, Request -from scrapy.utils.defer import maybe_deferred_to_future, deferred_to_future +from scrapy import Request, Spider, signals +from scrapy.utils.defer import deferred_to_future, maybe_deferred_to_future from scrapy.utils.test import get_crawler, get_from_asyncio_queue - from tests.mockserver import MockServer class SimplePipeline: def process_item(self, item, spider): - item['pipeline_passed'] = True + item["pipeline_passed"] = True return item class DeferredPipeline: def cb(self, item): - item['pipeline_passed'] = True + item["pipeline_passed"] = True return item def process_item(self, item, spider): @@ -34,9 +33,10 @@ class AsyncDefPipeline: async def process_item(self, item, spider): d = Deferred() from twisted.internet import reactor + reactor.callLater(0, d.callback, None) await maybe_deferred_to_future(d) - item['pipeline_passed'] = True + item["pipeline_passed"] = True return item @@ -44,10 +44,11 @@ class AsyncDefAsyncioPipeline: async def process_item(self, item, spider): d = Deferred() from twisted.internet import reactor + reactor.callLater(0, d.callback, None) await deferred_to_future(d) await asyncio.sleep(0.2) - item['pipeline_passed'] = await get_from_asyncio_queue(True) + item["pipeline_passed"] = await get_from_asyncio_queue(True) return item @@ -55,23 +56,24 @@ class AsyncDefNotAsyncioPipeline: async def process_item(self, item, spider): d1 = Deferred() from twisted.internet import reactor + reactor.callLater(0, d1.callback, None) await d1 d2 = Deferred() reactor.callLater(0, d2.callback, None) await maybe_deferred_to_future(d2) - item['pipeline_passed'] = True + item["pipeline_passed"] = True return item class ItemSpider(Spider): - name = 'itemspider' + name = "itemspider" def start_requests(self): - yield Request(self.mockserver.url('/status?n=200')) + yield Request(self.mockserver.url("/status?n=200")) def parse(self, response): - return {'field': 42} + return {"field": 42} class PipelineTestCase(unittest.TestCase): @@ -84,12 +86,12 @@ class PipelineTestCase(unittest.TestCase): def _on_item_scraped(self, item): self.assertIsInstance(item, dict) - self.assertTrue(item.get('pipeline_passed')) + self.assertTrue(item.get("pipeline_passed")) self.items.append(item) def _create_crawler(self, pipeline_class): settings = { - 'ITEM_PIPELINES': {pipeline_class: 1}, + "ITEM_PIPELINES": {pipeline_class: 1}, } crawler = get_crawler(ItemSpider, settings) crawler.signals.connect(self._on_item_scraped, signals.item_scraped) diff --git a/tests/test_pqueues.py b/tests/test_pqueues.py index ec55033d1..1584014b8 100644 --- a/tests/test_pqueues.py +++ b/tests/test_pqueues.py @@ -4,11 +4,10 @@ import unittest import queuelib from scrapy.http.request import Request -from scrapy.pqueues import ScrapyPriorityQueue, DownloaderAwarePriorityQueue +from scrapy.pqueues import DownloaderAwarePriorityQueue, ScrapyPriorityQueue from scrapy.spiders import Spider from scrapy.squeues import FifoMemoryQueue from scrapy.utils.test import get_crawler - from tests.test_scheduler import MockDownloader, MockEngine @@ -19,7 +18,9 @@ class PriorityQueueTest(unittest.TestCase): def test_queue_push_pop_one(self): temp_dir = tempfile.mkdtemp() - queue = ScrapyPriorityQueue.from_crawler(self.crawler, FifoMemoryQueue, temp_dir) + queue = ScrapyPriorityQueue.from_crawler( + self.crawler, FifoMemoryQueue, temp_dir + ) self.assertIsNone(queue.pop()) self.assertEqual(len(queue), 0) req1 = Request("https://example.org/1", priority=1) @@ -35,9 +36,14 @@ class PriorityQueueTest(unittest.TestCase): if hasattr(queuelib.queue.FifoMemoryQueue, "peek"): raise unittest.SkipTest("queuelib.queue.FifoMemoryQueue.peek is defined") temp_dir = tempfile.mkdtemp() - queue = ScrapyPriorityQueue.from_crawler(self.crawler, FifoMemoryQueue, temp_dir) + queue = ScrapyPriorityQueue.from_crawler( + self.crawler, FifoMemoryQueue, temp_dir + ) queue.push(Request("https://example.org")) - with self.assertRaises(NotImplementedError, msg="The underlying queue class does not implement 'peek'"): + with self.assertRaises( + NotImplementedError, + msg="The underlying queue class does not implement 'peek'", + ): queue.peek() queue.close() @@ -45,7 +51,9 @@ class PriorityQueueTest(unittest.TestCase): if not hasattr(queuelib.queue.FifoMemoryQueue, "peek"): raise unittest.SkipTest("queuelib.queue.FifoMemoryQueue.peek is undefined") temp_dir = tempfile.mkdtemp() - queue = ScrapyPriorityQueue.from_crawler(self.crawler, FifoMemoryQueue, temp_dir) + queue = ScrapyPriorityQueue.from_crawler( + self.crawler, FifoMemoryQueue, temp_dir + ) self.assertEqual(len(queue), 0) self.assertIsNone(queue.peek()) req1 = Request("https://example.org/1") @@ -67,7 +75,9 @@ class PriorityQueueTest(unittest.TestCase): def test_queue_push_pop_priorities(self): temp_dir = tempfile.mkdtemp() - queue = ScrapyPriorityQueue.from_crawler(self.crawler, FifoMemoryQueue, temp_dir, [-1, -2, -3]) + queue = ScrapyPriorityQueue.from_crawler( + self.crawler, FifoMemoryQueue, temp_dir, [-1, -2, -3] + ) self.assertIsNone(queue.pop()) self.assertEqual(len(queue), 0) req1 = Request("https://example.org/1", priority=1) @@ -119,7 +129,10 @@ class DownloaderAwarePriorityQueueTest(unittest.TestCase): if hasattr(queuelib.queue.FifoMemoryQueue, "peek"): raise unittest.SkipTest("queuelib.queue.FifoMemoryQueue.peek is defined") self.queue.push(Request("https://example.org")) - with self.assertRaises(NotImplementedError, msg="The underlying queue class does not implement 'peek'"): + with self.assertRaises( + NotImplementedError, + msg="The underlying queue class does not implement 'peek'", + ): self.queue.peek() def test_peek(self): diff --git a/tests/test_proxy_connect.py b/tests/test_proxy_connect.py index ea7701b5d..dc0a82086 100644 --- a/tests/test_proxy_connect.py +++ b/tests/test_proxy_connect.py @@ -3,44 +3,51 @@ import os import re import sys from pathlib import Path -from subprocess import Popen, PIPE +from subprocess import PIPE, Popen from urllib.parse import urlsplit, urlunsplit + from testfixtures import LogCapture from twisted.internet import defer from twisted.trial.unittest import TestCase from scrapy.http import Request from scrapy.utils.test import get_crawler - from tests.mockserver import MockServer from tests.spiders import SimpleSpider, SingleRequestSpider class MitmProxy: - auth_user = 'scrapy' - auth_pass = 'scrapy' + auth_user = "scrapy" + auth_pass = "scrapy" def start(self): - from scrapy.utils.test import get_testenv script = """ import sys from mitmproxy.tools.main import mitmdump sys.argv[0] = "mitmdump" sys.exit(mitmdump()) """ - cert_path = Path(__file__).parent.resolve() / 'keys' / 'mitmproxy-ca.pem' - self.proc = Popen([sys.executable, - '-c', script, - '--listen-host', '127.0.0.1', - '--listen-port', '0', - '--proxyauth', f'{self.auth_user}:{self.auth_pass}', - '--certs', str(cert_path), - '--ssl-insecure', - ], - stdout=PIPE, env=get_testenv()) - line = self.proc.stdout.readline().decode('utf-8') - host_port = re.search(r'listening at http://([^:]+:\d+)', line).group(1) - address = f'http://{self.auth_user}:{self.auth_pass}@{host_port}' + cert_path = Path(__file__).parent.resolve() / "keys" / "mitmproxy-ca.pem" + self.proc = Popen( + [ + sys.executable, + "-c", + script, + "--listen-host", + "127.0.0.1", + "--listen-port", + "0", + "--proxyauth", + f"{self.auth_user}:{self.auth_pass}", + "--certs", + str(cert_path), + "--ssl-insecure", + ], + stdout=PIPE, + ) + line = self.proc.stdout.readline().decode("utf-8") + host_port = re.search(r"listening at http://([^:]+:\d+)", line).group(1) + address = f"http://{self.auth_user}:{self.auth_pass}@{host_port}" return address def stop(self): @@ -50,17 +57,16 @@ sys.exit(mitmdump()) def _wrong_credentials(proxy_url): bad_auth_proxy = list(urlsplit(proxy_url)) - bad_auth_proxy[1] = bad_auth_proxy[1].replace('scrapy:scrapy@', 'wrong:wronger@') + bad_auth_proxy[1] = bad_auth_proxy[1].replace("scrapy:scrapy@", "wrong:wronger@") return urlunsplit(bad_auth_proxy) class ProxyConnectTestCase(TestCase): - def setUp(self): try: import mitmproxy # noqa: F401 except ImportError: - self.skipTest('mitmproxy is not installed') + self.skipTest("mitmproxy is not installed") self.mockserver = MockServer() self.mockserver.__enter__() @@ -68,8 +74,8 @@ class ProxyConnectTestCase(TestCase): self._proxy = MitmProxy() proxy_url = self._proxy.start() - os.environ['https_proxy'] = proxy_url - os.environ['http_proxy'] = proxy_url + os.environ["https_proxy"] = proxy_url + os.environ["http_proxy"] = proxy_url def tearDown(self): self.mockserver.__exit__(None, None, None) @@ -85,7 +91,7 @@ class ProxyConnectTestCase(TestCase): @defer.inlineCallbacks def test_https_tunnel_auth_error(self): - os.environ['https_proxy'] = _wrong_credentials(os.environ['https_proxy']) + os.environ["https_proxy"] = _wrong_credentials(os.environ["https_proxy"]) crawler = get_crawler(SimpleSpider) with LogCapture() as log: yield crawler.crawl(self.mockserver.url("/status?n=200", is_secure=True)) @@ -100,13 +106,13 @@ class ProxyConnectTestCase(TestCase): with LogCapture() as log: yield crawler.crawl(seed=request) self._assert_got_response_code(200, log) - echo = json.loads(crawler.spider.meta['responses'][0].text) - self.assertTrue('Proxy-Authorization' not in echo['headers']) + echo = json.loads(crawler.spider.meta["responses"][0].text) + self.assertTrue("Proxy-Authorization" not in echo["headers"]) def _assert_got_response_code(self, code, log): print(log) - self.assertEqual(str(log).count(f'Crawled ({code})'), 1) + self.assertEqual(str(log).count(f"Crawled ({code})"), 1) def _assert_got_tunnel_error(self, log): print(log) - self.assertIn('TunnelError', str(log)) + self.assertIn("TunnelError", str(log)) diff --git a/tests/test_request_attribute_binding.py b/tests/test_request_attribute_binding.py index 15e400327..d65d74206 100644 --- a/tests/test_request_attribute_binding.py +++ b/tests/test_request_attribute_binding.py @@ -1,22 +1,19 @@ +from testfixtures import LogCapture from twisted.internet import defer from twisted.trial.unittest import TestCase -from testfixtures import LogCapture - from scrapy import Request, signals from scrapy.http.response import Response from scrapy.utils.test import get_crawler - from tests.mockserver import MockServer from tests.spiders import SingleRequestSpider - -OVERRIDEN_URL = "https://example.org" +OVERRIDDEN_URL = "https://example.org" class ProcessResponseMiddleware: def process_response(self, request, response, spider): - return response.replace(request=Request(OVERRIDEN_URL)) + return response.replace(request=Request(OVERRIDDEN_URL)) class RaiseExceptionRequestMiddleware: @@ -30,7 +27,7 @@ class CatchExceptionOverrideRequestMiddleware: return Response( url="http://localhost/", body=b"Caught " + exception.__class__.__name__.encode("utf-8"), - request=Request(OVERRIDEN_URL), + request=Request(OVERRIDDEN_URL), ) @@ -52,7 +49,7 @@ class AlternativeCallbacksSpider(SingleRequestSpider): class AlternativeCallbacksMiddleware: def process_response(self, request, response, spider): new_request = request.replace( - url=OVERRIDEN_URL, + url=OVERRIDDEN_URL, callback=spider.alt_callback, cb_kwargs={"foo": "bar"}, ) @@ -60,7 +57,6 @@ class AlternativeCallbacksMiddleware: class CrawlTestCase(TestCase): - def setUp(self): self.mockserver = MockServer() self.mockserver.__enter__() @@ -90,11 +86,14 @@ class CrawlTestCase(TestCase): @defer.inlineCallbacks def test_downloader_middleware_raise_exception(self): url = self.mockserver.url("/status?n=200") - crawler = get_crawler(SingleRequestSpider, { - "DOWNLOADER_MIDDLEWARES": { - RaiseExceptionRequestMiddleware: 590, + crawler = get_crawler( + SingleRequestSpider, + { + "DOWNLOADER_MIDDLEWARES": { + RaiseExceptionRequestMiddleware: 590, + }, }, - }) + ) yield crawler.crawl(seed=url, mockserver=self.mockserver) failure = crawler.spider.meta["failure"] self.assertEqual(failure.request.url, url) @@ -116,24 +115,31 @@ class CrawlTestCase(TestCase): signal_params["request"] = request url = self.mockserver.url("/status?n=200") - crawler = get_crawler(SingleRequestSpider, { - "DOWNLOADER_MIDDLEWARES": { - ProcessResponseMiddleware: 595, - } - }) + crawler = get_crawler( + SingleRequestSpider, + { + "DOWNLOADER_MIDDLEWARES": { + ProcessResponseMiddleware: 595, + } + }, + ) crawler.signals.connect(signal_handler, signal=signals.response_received) with LogCapture() as log: yield crawler.crawl(seed=url, mockserver=self.mockserver) response = crawler.spider.meta["responses"][0] - self.assertEqual(response.request.url, OVERRIDEN_URL) + self.assertEqual(response.request.url, OVERRIDDEN_URL) self.assertEqual(signal_params["response"].url, url) - self.assertEqual(signal_params["request"].url, OVERRIDEN_URL) + self.assertEqual(signal_params["request"].url, OVERRIDDEN_URL) log.check_present( - ("scrapy.core.engine", "DEBUG", f"Crawled (200)
Hello

Hello

Hello

Hello

Hello
an Jos\xe9 de
', - encoding='utf-8') - Selector(r1).xpath('//text()').getall() + r1 = TextResponse( + "http://www.example.com", + body=b"an Jos\xe9 de
", + encoding="utf-8", + ) + Selector(r1).xpath("//text()").getall() def test_weakref_slots(self): """Check that classes are using slots and are weak-referenceable""" - x = Selector(text='') + x = Selector(text="") weakref.ref(x) - assert not hasattr(x, '__dict__'), f"{x.__class__.__name__} does not use __slots__" + assert not hasattr( + x, "__dict__" + ), f"{x.__class__.__name__} does not use __slots__" def test_selector_bad_args(self): - with self.assertRaisesRegex(ValueError, 'received both response and text'): - Selector(TextResponse(url='http://example.com', body=b''), text='') + with self.assertRaisesRegex(ValueError, "received both response and text"): + Selector(TextResponse(url="http://example.com", body=b""), text="") + + +class JMESPathTestCase(unittest.TestCase): + @pytest.mark.skipif( + not PARSEL_18_PLUS, reason="parsel < 1.8 doesn't support jmespath" + ) + def test_json_has_html(self) -> None: + """Sometimes the information is returned in a json wrapper""" + + body = """ + { + "content": [ + { + "name": "A", + "value": "a" + }, + { + "name": { + "age": 18 + }, + "value": "b" + }, + { + "name": "C", + "value": "c" + }, + { + "name": "D", + "value": "some text
') + sel = Selector(text="some text
") self.assertRaises(ValueError, q.push, sel) class FifoDiskQueueTestMixin: - def test_serialize(self): q = self.queue() - q.push('a') + q.push("a") q.push(123) - q.push({'a': 'dict'}) - self.assertEqual(q.pop(), 'a') + q.push({"a": "dict"}) + self.assertEqual(q.pop(), "a") self.assertEqual(q.pop(), 123) - self.assertEqual(q.pop(), {'a': 'dict'}) + self.assertEqual(q.pop(), {"a": "dict"}) test_nonserializable_object = nonserializable_object_test @@ -73,7 +73,6 @@ class ChunkSize4MarshalFifoDiskQueueTest(MarshalFifoDiskQueueTest): class PickleFifoDiskQueueTest(t.FifoDiskQueueTest, FifoDiskQueueTestMixin): - chunksize = 100000 def queue(self): @@ -81,7 +80,7 @@ class PickleFifoDiskQueueTest(t.FifoDiskQueueTest, FifoDiskQueueTestMixin): def test_serialize_item(self): q = self.queue() - i = TestItem(name='foo') + i = TestItem(name="foo") q.push(i) i2 = q.pop() assert isinstance(i2, TestItem) @@ -94,17 +93,17 @@ class PickleFifoDiskQueueTest(t.FifoDiskQueueTest, FifoDiskQueueTestMixin): loader2 = q.pop() assert isinstance(loader2, TestLoader) assert loader2.default_item_class is TestItem - self.assertEqual(loader2.name_out('x'), 'xx') + self.assertEqual(loader2.name_out("x"), "xx") def test_serialize_request_recursive(self): q = self.queue() - r = Request('http://www.example.com') - r.meta['request'] = r + r = Request("http://www.example.com") + r.meta["request"] = r q.push(r) r2 = q.pop() assert isinstance(r2, Request) self.assertEqual(r.url, r2.url) - assert r2.meta['request'] is r2 + assert r2.meta["request"] is r2 def test_non_pickable_object(self): q = self.queue() @@ -115,7 +114,7 @@ class PickleFifoDiskQueueTest(t.FifoDiskQueueTest, FifoDiskQueueTestMixin): self.assertIsInstance(exc.__context__, pickle.PicklingError) else: self.assertIsInstance(exc.__context__, AttributeError) - sel = Selector(text='some text
') + sel = Selector(text="some text
") try: q.push(sel) except ValueError as exc: @@ -139,33 +138,30 @@ class ChunkSize4PickleFifoDiskQueueTest(PickleFifoDiskQueueTest): class LifoDiskQueueTestMixin: - def test_serialize(self): q = self.queue() - q.push('a') + q.push("a") q.push(123) - q.push({'a': 'dict'}) - self.assertEqual(q.pop(), {'a': 'dict'}) + q.push({"a": "dict"}) + self.assertEqual(q.pop(), {"a": "dict"}) self.assertEqual(q.pop(), 123) - self.assertEqual(q.pop(), 'a') + self.assertEqual(q.pop(), "a") test_nonserializable_object = nonserializable_object_test class MarshalLifoDiskQueueTest(t.LifoDiskQueueTest, LifoDiskQueueTestMixin): - def queue(self): return _MarshalLifoSerializationDiskQueue(self.qpath) class PickleLifoDiskQueueTest(t.LifoDiskQueueTest, LifoDiskQueueTestMixin): - def queue(self): return _PickleLifoSerializationDiskQueue(self.qpath) def test_serialize_item(self): q = self.queue() - i = TestItem(name='foo') + i = TestItem(name="foo") q.push(i) i2 = q.pop() assert isinstance(i2, TestItem) @@ -178,14 +174,14 @@ class PickleLifoDiskQueueTest(t.LifoDiskQueueTest, LifoDiskQueueTestMixin): loader2 = q.pop() assert isinstance(loader2, TestLoader) assert loader2.default_item_class is TestItem - self.assertEqual(loader2.name_out('x'), 'xx') + self.assertEqual(loader2.name_out("x"), "xx") def test_serialize_request_recursive(self): q = self.queue() - r = Request('http://www.example.com') - r.meta['request'] = r + r = Request("http://www.example.com") + r.meta["request"] = r q.push(r) r2 = q.pop() assert isinstance(r2, Request) self.assertEqual(r.url, r2.url) - assert r2.meta['request'] is r2 + assert r2.meta["request"] is r2 diff --git a/tests/test_squeues_request.py b/tests/test_squeues_request.py index 22dafb2d2..b444c32b7 100644 --- a/tests/test_squeues_request.py +++ b/tests/test_squeues_request.py @@ -4,16 +4,16 @@ import unittest import queuelib -from scrapy.squeues import ( - PickleFifoDiskQueue, - PickleLifoDiskQueue, - MarshalFifoDiskQueue, - MarshalLifoDiskQueue, - FifoMemoryQueue, - LifoMemoryQueue, -) from scrapy.http import Request from scrapy.spiders import Spider +from scrapy.squeues import ( + FifoMemoryQueue, + LifoMemoryQueue, + MarshalFifoDiskQueue, + MarshalLifoDiskQueue, + PickleFifoDiskQueue, + PickleLifoDiskQueue, +) from scrapy.utils.test import get_crawler """ @@ -69,7 +69,10 @@ class RequestQueueTestMixin: req = Request("http://www.example.com") q.push(req) self.assertEqual(len(q), 1) - with self.assertRaises(NotImplementedError, msg="The underlying queue class does not implement 'peek'"): + with self.assertRaises( + NotImplementedError, + msg="The underlying queue class does not implement 'peek'", + ): q.peek() self.assertEqual(q.pop().url, req.url) self.assertEqual(len(q), 0) @@ -117,7 +120,10 @@ class FifoQueueMixin(RequestQueueTestMixin): q.push(req1) q.push(req2) q.push(req3) - with self.assertRaises(NotImplementedError, msg="The underlying queue class does not implement 'peek'"): + with self.assertRaises( + NotImplementedError, + msg="The underlying queue class does not implement 'peek'", + ): q.peek() self.assertEqual(len(q), 3) self.assertEqual(q.pop().url, req1.url) @@ -170,7 +176,10 @@ class LifoQueueMixin(RequestQueueTestMixin): q.push(req1) q.push(req2) q.push(req3) - with self.assertRaises(NotImplementedError, msg="The underlying queue class does not implement 'peek'"): + with self.assertRaises( + NotImplementedError, + msg="The underlying queue class does not implement 'peek'", + ): q.peek() self.assertEqual(len(q), 3) self.assertEqual(q.pop().url, req3.url) @@ -195,12 +204,16 @@ class PickleLifoDiskQueueRequestTest(LifoQueueMixin, BaseQueueTestCase): class MarshalFifoDiskQueueRequestTest(FifoQueueMixin, BaseQueueTestCase): def queue(self): - return MarshalFifoDiskQueue.from_crawler(crawler=self.crawler, key="marshal/fifo") + return MarshalFifoDiskQueue.from_crawler( + crawler=self.crawler, key="marshal/fifo" + ) class MarshalLifoDiskQueueRequestTest(LifoQueueMixin, BaseQueueTestCase): def queue(self): - return MarshalLifoDiskQueue.from_crawler(crawler=self.crawler, key="marshal/lifo") + return MarshalLifoDiskQueue.from_crawler( + crawler=self.crawler, key="marshal/lifo" + ) class FifoMemoryQueueRequestTest(FifoQueueMixin, BaseQueueTestCase): diff --git a/tests/test_stats.py b/tests/test_stats.py index 2bbbb9e2c..3d4c7e88e 100644 --- a/tests/test_stats.py +++ b/tests/test_stats.py @@ -1,42 +1,41 @@ -from datetime import datetime import unittest +from datetime import datetime from unittest import mock from scrapy.extensions.corestats import CoreStats from scrapy.spiders import Spider -from scrapy.statscollectors import StatsCollector, DummyStatsCollector +from scrapy.statscollectors import DummyStatsCollector, StatsCollector from scrapy.utils.test import get_crawler class CoreStatsExtensionTest(unittest.TestCase): - def setUp(self): self.crawler = get_crawler(Spider) - self.spider = self.crawler._create_spider('foo') + self.spider = self.crawler._create_spider("foo") - @mock.patch('scrapy.extensions.corestats.datetime') + @mock.patch("scrapy.extensions.corestats.datetime") def test_core_stats_default_stats_collector(self, mock_datetime): fixed_datetime = datetime(2019, 12, 1, 11, 38) - mock_datetime.utcnow = mock.Mock(return_value=fixed_datetime) + mock_datetime.now = mock.Mock(return_value=fixed_datetime) self.crawler.stats = StatsCollector(self.crawler) ext = CoreStats.from_crawler(self.crawler) ext.spider_opened(self.spider) ext.item_scraped({}, self.spider) ext.response_received(self.spider) ext.item_dropped({}, self.spider, ZeroDivisionError()) - ext.spider_closed(self.spider, 'finished') + ext.spider_closed(self.spider, "finished") self.assertEqual( ext.stats._stats, { - 'start_time': fixed_datetime, - 'finish_time': fixed_datetime, - 'item_scraped_count': 1, - 'response_received_count': 1, - 'item_dropped_count': 1, - 'item_dropped_reasons_count/ZeroDivisionError': 1, - 'finish_reason': 'finished', - 'elapsed_time_seconds': 0.0, - } + "start_time": fixed_datetime, + "finish_time": fixed_datetime, + "item_scraped_count": 1, + "response_received_count": 1, + "item_dropped_count": 1, + "item_dropped_reasons_count/ZeroDivisionError": 1, + "finish_reason": "finished", + "elapsed_time_seconds": 0.0, + }, ) def test_core_stats_dummy_stats_collector(self): @@ -46,53 +45,52 @@ class CoreStatsExtensionTest(unittest.TestCase): ext.item_scraped({}, self.spider) ext.response_received(self.spider) ext.item_dropped({}, self.spider, ZeroDivisionError()) - ext.spider_closed(self.spider, 'finished') + ext.spider_closed(self.spider, "finished") self.assertEqual(ext.stats._stats, {}) class StatsCollectorTest(unittest.TestCase): - def setUp(self): self.crawler = get_crawler(Spider) - self.spider = self.crawler._create_spider('foo') + self.spider = self.crawler._create_spider("foo") def test_collector(self): stats = StatsCollector(self.crawler) self.assertEqual(stats.get_stats(), {}) - self.assertEqual(stats.get_value('anything'), None) - self.assertEqual(stats.get_value('anything', 'default'), 'default') - stats.set_value('test', 'value') - self.assertEqual(stats.get_stats(), {'test': 'value'}) - stats.set_value('test2', 23) - self.assertEqual(stats.get_stats(), {'test': 'value', 'test2': 23}) - self.assertEqual(stats.get_value('test2'), 23) - stats.inc_value('test2') - self.assertEqual(stats.get_value('test2'), 24) - stats.inc_value('test2', 6) - self.assertEqual(stats.get_value('test2'), 30) - stats.max_value('test2', 6) - self.assertEqual(stats.get_value('test2'), 30) - stats.max_value('test2', 40) - self.assertEqual(stats.get_value('test2'), 40) - stats.max_value('test3', 1) - self.assertEqual(stats.get_value('test3'), 1) - stats.min_value('test2', 60) - self.assertEqual(stats.get_value('test2'), 40) - stats.min_value('test2', 35) - self.assertEqual(stats.get_value('test2'), 35) - stats.min_value('test4', 7) - self.assertEqual(stats.get_value('test4'), 7) + self.assertEqual(stats.get_value("anything"), None) + self.assertEqual(stats.get_value("anything", "default"), "default") + stats.set_value("test", "value") + self.assertEqual(stats.get_stats(), {"test": "value"}) + stats.set_value("test2", 23) + self.assertEqual(stats.get_stats(), {"test": "value", "test2": 23}) + self.assertEqual(stats.get_value("test2"), 23) + stats.inc_value("test2") + self.assertEqual(stats.get_value("test2"), 24) + stats.inc_value("test2", 6) + self.assertEqual(stats.get_value("test2"), 30) + stats.max_value("test2", 6) + self.assertEqual(stats.get_value("test2"), 30) + stats.max_value("test2", 40) + self.assertEqual(stats.get_value("test2"), 40) + stats.max_value("test3", 1) + self.assertEqual(stats.get_value("test3"), 1) + stats.min_value("test2", 60) + self.assertEqual(stats.get_value("test2"), 40) + stats.min_value("test2", 35) + self.assertEqual(stats.get_value("test2"), 35) + stats.min_value("test4", 7) + self.assertEqual(stats.get_value("test4"), 7) def test_dummy_collector(self): stats = DummyStatsCollector(self.crawler) self.assertEqual(stats.get_stats(), {}) - self.assertEqual(stats.get_value('anything'), None) - self.assertEqual(stats.get_value('anything', 'default'), 'default') - stats.set_value('test', 'value') - stats.inc_value('v1') - stats.max_value('v2', 100) - stats.min_value('v3', 100) - stats.open_spider('a') - stats.set_value('test', 'value', spider=self.spider) + self.assertEqual(stats.get_value("anything"), None) + self.assertEqual(stats.get_value("anything", "default"), "default") + stats.set_value("test", "value") + stats.inc_value("v1") + stats.max_value("v2", 100) + stats.min_value("v3", 100) + stats.open_spider("a") + stats.set_value("test", "value", spider=self.spider) self.assertEqual(stats.get_stats(), {}) - self.assertEqual(stats.get_stats('a'), {}) + self.assertEqual(stats.get_stats("a"), {}) diff --git a/tests/test_toplevel.py b/tests/test_toplevel.py index fdc5df166..d272101b8 100644 --- a/tests/test_toplevel.py +++ b/tests/test_toplevel.py @@ -4,7 +4,6 @@ import scrapy class ToplevelTestCase(TestCase): - def test_version(self): self.assertIs(type(scrapy.__version__), str) @@ -12,19 +11,23 @@ class ToplevelTestCase(TestCase): self.assertIs(type(scrapy.version_info), tuple) def test_request_shortcut(self): - from scrapy.http import Request, FormRequest + from scrapy.http import FormRequest, Request + self.assertIs(scrapy.Request, Request) self.assertIs(scrapy.FormRequest, FormRequest) def test_spider_shortcut(self): from scrapy.spiders import Spider + self.assertIs(scrapy.Spider, Spider) def test_selector_shortcut(self): from scrapy.selector import Selector + self.assertIs(scrapy.Selector, Selector) def test_item_shortcut(self): - from scrapy.item import Item, Field + from scrapy.item import Field, Item + self.assertIs(scrapy.Item, Item) self.assertIs(scrapy.Field, Field) diff --git a/tests/test_urlparse_monkeypatches.py b/tests/test_urlparse_monkeypatches.py index bea0cf3e5..c695968d7 100644 --- a/tests/test_urlparse_monkeypatches.py +++ b/tests/test_urlparse_monkeypatches.py @@ -1,12 +1,11 @@ -from urllib.parse import urlparse import unittest +from urllib.parse import urlparse class UrlparseTestCase(unittest.TestCase): - def test_s3_url(self): - p = urlparse('s3://bucket/key/name?param=value') - self.assertEqual(p.scheme, 's3') - self.assertEqual(p.hostname, 'bucket') - self.assertEqual(p.path, '/key/name') - self.assertEqual(p.query, 'param=value') + p = urlparse("s3://bucket/key/name?param=value") + self.assertEqual(p.scheme, "s3") + self.assertEqual(p.hostname, "bucket") + self.assertEqual(p.path, "/key/name") + self.assertEqual(p.query, "param=value") diff --git a/tests/test_utils_asyncio.py b/tests/test_utils_asyncio.py index 42780ace7..65e352053 100644 --- a/tests/test_utils_asyncio.py +++ b/tests/test_utils_asyncio.py @@ -1,22 +1,37 @@ +import asyncio import warnings from unittest import TestCase from pytest import mark -from scrapy.utils.reactor import is_asyncio_reactor_installed, install_reactor +from scrapy.utils.defer import deferred_f_from_coro_f +from scrapy.utils.reactor import ( + install_reactor, + is_asyncio_reactor_installed, + set_asyncio_event_loop, +) -@mark.usefixtures('reactor_pytest') +@mark.usefixtures("reactor_pytest") class AsyncioTest(TestCase): - def test_is_asyncio_reactor_installed(self): # the result should depend only on the pytest --reactor argument - self.assertEqual(is_asyncio_reactor_installed(), self.reactor_pytest == 'asyncio') + self.assertEqual( + is_asyncio_reactor_installed(), self.reactor_pytest == "asyncio" + ) def test_install_asyncio_reactor(self): from twisted.internet import reactor as original_reactor + with warnings.catch_warnings(record=True) as w: install_reactor("twisted.internet.asyncioreactor.AsyncioSelectorReactor") self.assertEqual(len(w), 0) from twisted.internet import reactor + assert original_reactor == reactor + + @mark.only_asyncio() + @deferred_f_from_coro_f + async def test_set_asyncio_event_loop(self): + install_reactor("twisted.internet.asyncioreactor.AsyncioSelectorReactor") + assert set_asyncio_event_loop(None) is asyncio.get_running_loop() diff --git a/tests/test_utils_conf.py b/tests/test_utils_conf.py index a92880626..dc3f01d57 100644 --- a/tests/test_utils_conf.py +++ b/tests/test_utils_conf.py @@ -1,206 +1,244 @@ import unittest import warnings -from scrapy.exceptions import UsageError, ScrapyDeprecationWarning +import pytest + +from scrapy.exceptions import ScrapyDeprecationWarning, UsageError from scrapy.settings import BaseSettings, Settings from scrapy.utils.conf import ( arglist_to_dict, build_component_list, feed_complete_default_values_from_settings, - feed_process_params_from_cli + feed_process_params_from_cli, ) class BuildComponentListTest(unittest.TestCase): - def test_build_dict(self): - d = {'one': 1, 'two': None, 'three': 8, 'four': 4} - self.assertEqual(build_component_list(d, convert=lambda x: x), - ['one', 'four', 'three']) + d = {"one": 1, "two": None, "three": 8, "four": 4} + self.assertEqual( + build_component_list(d, convert=lambda x: x), ["one", "four", "three"] + ) def test_backward_compatible_build_dict(self): - base = {'one': 1, 'two': 2, 'three': 3, 'five': 5, 'six': None} - custom = {'two': None, 'three': 8, 'four': 4} - self.assertEqual(build_component_list(base, custom, - convert=lambda x: x), - ['one', 'four', 'five', 'three']) + base = {"one": 1, "two": 2, "three": 3, "five": 5, "six": None} + custom = {"two": None, "three": 8, "four": 4} + with pytest.warns(ScrapyDeprecationWarning, match="The 'custom' attribute"): + self.assertEqual( + build_component_list(base, custom, convert=lambda x: x), + ["one", "four", "five", "three"], + ) def test_return_list(self): - custom = ['a', 'b', 'c'] - self.assertEqual(build_component_list(None, custom, - convert=lambda x: x), - custom) + custom = ["a", "b", "c"] + with pytest.warns(ScrapyDeprecationWarning, match="The 'custom' attribute"): + self.assertEqual( + build_component_list(None, custom, convert=lambda x: x), custom + ) def test_map_dict(self): - custom = {'one': 1, 'two': 2, 'three': 3} - self.assertEqual(build_component_list({}, custom, - convert=lambda x: x.upper()), - ['ONE', 'TWO', 'THREE']) + custom = {"one": 1, "two": 2, "three": 3} + with pytest.warns(ScrapyDeprecationWarning, match="The 'custom' attribute"): + self.assertEqual( + build_component_list({}, custom, convert=lambda x: x.upper()), + ["ONE", "TWO", "THREE"], + ) def test_map_list(self): - custom = ['a', 'b', 'c'] - self.assertEqual(build_component_list(None, custom, - lambda x: x.upper()), - ['A', 'B', 'C']) + custom = ["a", "b", "c"] + with pytest.warns(ScrapyDeprecationWarning, match="The 'custom' attribute"): + self.assertEqual( + build_component_list(None, custom, lambda x: x.upper()), ["A", "B", "C"] + ) def test_duplicate_components_in_dict(self): - duplicate_dict = {'one': 1, 'two': 2, 'ONE': 4} - self.assertRaises(ValueError, build_component_list, {}, duplicate_dict, - convert=lambda x: x.lower()) + duplicate_dict = {"one": 1, "two": 2, "ONE": 4} + with self.assertRaises(ValueError): + with pytest.warns(ScrapyDeprecationWarning, match="The 'custom' attribute"): + build_component_list({}, duplicate_dict, convert=lambda x: x.lower()) def test_duplicate_components_in_list(self): - duplicate_list = ['a', 'b', 'a'] + duplicate_list = ["a", "b", "a"] with self.assertRaises(ValueError) as cm: - build_component_list(None, duplicate_list, convert=lambda x: x) + with pytest.warns(ScrapyDeprecationWarning, match="The 'custom' attribute"): + build_component_list(None, duplicate_list, convert=lambda x: x) self.assertIn(str(duplicate_list), str(cm.exception)) def test_duplicate_components_in_basesettings(self): # Higher priority takes precedence - duplicate_bs = BaseSettings({'one': 1, 'two': 2}, priority=0) - duplicate_bs.set('ONE', 4, priority=10) - self.assertEqual(build_component_list(duplicate_bs, - convert=lambda x: x.lower()), - ['two', 'one']) - duplicate_bs.set('one', duplicate_bs['one'], priority=20) - self.assertEqual(build_component_list(duplicate_bs, - convert=lambda x: x.lower()), - ['one', 'two']) + duplicate_bs = BaseSettings({"one": 1, "two": 2}, priority=0) + duplicate_bs.set("ONE", 4, priority=10) + self.assertEqual( + build_component_list(duplicate_bs, convert=lambda x: x.lower()), + ["two", "one"], + ) + duplicate_bs.set("one", duplicate_bs["one"], priority=20) + self.assertEqual( + build_component_list(duplicate_bs, convert=lambda x: x.lower()), + ["one", "two"], + ) # Same priority raises ValueError - duplicate_bs.set('ONE', duplicate_bs['ONE'], priority=20) - self.assertRaises(ValueError, build_component_list, duplicate_bs, - convert=lambda x: x.lower()) + duplicate_bs.set("ONE", duplicate_bs["ONE"], priority=20) + with self.assertRaises(ValueError): + build_component_list(duplicate_bs, convert=lambda x: x.lower()) def test_valid_numbers(self): # work well with None and numeric values - d = {'a': 10, 'b': None, 'c': 15, 'd': 5.0} - self.assertEqual(build_component_list(d, convert=lambda x: x), - ['d', 'a', 'c']) - d = {'a': 33333333333333333333, 'b': 11111111111111111111, 'c': 22222222222222222222} - self.assertEqual(build_component_list(d, convert=lambda x: x), - ['b', 'c', 'a']) + d = {"a": 10, "b": None, "c": 15, "d": 5.0} + self.assertEqual(build_component_list(d, convert=lambda x: x), ["d", "a", "c"]) + d = { + "a": 33333333333333333333, + "b": 11111111111111111111, + "c": 22222222222222222222, + } + self.assertEqual(build_component_list(d, convert=lambda x: x), ["b", "c", "a"]) # raise exception for invalid values - d = {'one': '5'} - self.assertRaises(ValueError, build_component_list, {}, d, convert=lambda x: x) - d = {'one': '1.0'} - self.assertRaises(ValueError, build_component_list, {}, d, convert=lambda x: x) - d = {'one': [1, 2, 3]} - self.assertRaises(ValueError, build_component_list, {}, d, convert=lambda x: x) - d = {'one': {'a': 'a', 'b': 2}} - self.assertRaises(ValueError, build_component_list, {}, d, convert=lambda x: x) - d = {'one': 'lorem ipsum'} - self.assertRaises(ValueError, build_component_list, {}, d, convert=lambda x: x) + d = {"one": "5"} + with self.assertRaises(ValueError): + with pytest.warns(ScrapyDeprecationWarning, match="The 'custom' attribute"): + build_component_list({}, d, convert=lambda x: x) class UtilsConfTestCase(unittest.TestCase): - def test_arglist_to_dict(self): self.assertEqual( - arglist_to_dict(['arg1=val1', 'arg2=val2']), - {'arg1': 'val1', 'arg2': 'val2'}) + arglist_to_dict(["arg1=val1", "arg2=val2"]), + {"arg1": "val1", "arg2": "val2"}, + ) class FeedExportConfigTestCase(unittest.TestCase): - def test_feed_export_config_invalid_format(self): settings = Settings() - self.assertRaises(UsageError, feed_process_params_from_cli, settings, ['items.dat'], 'noformat') + self.assertRaises( + UsageError, + feed_process_params_from_cli, + settings, + ["items.dat"], + "noformat", + ) def test_feed_export_config_mismatch(self): settings = Settings() self.assertRaises( UsageError, - feed_process_params_from_cli, settings, ['items1.dat', 'items2.dat'], 'noformat' + feed_process_params_from_cli, + settings, + ["items1.dat", "items2.dat"], + "noformat", ) def test_feed_export_config_backward_compatible(self): with warnings.catch_warnings(record=True) as cw: settings = Settings() self.assertEqual( - {'items.dat': {'format': 'csv'}}, - feed_process_params_from_cli(settings, ['items.dat'], 'csv') + {"items.dat": {"format": "csv"}}, + feed_process_params_from_cli(settings, ["items.dat"], "csv"), ) self.assertEqual(cw[0].category, ScrapyDeprecationWarning) def test_feed_export_config_explicit_formats(self): settings = Settings() self.assertEqual( - {'items_1.dat': {'format': 'json'}, 'items_2.dat': {'format': 'xml'}, 'items_3.dat': {'format': 'csv'}}, - feed_process_params_from_cli(settings, ['items_1.dat:json', 'items_2.dat:xml', 'items_3.dat:csv']) + { + "items_1.dat": {"format": "json"}, + "items_2.dat": {"format": "xml"}, + "items_3.dat": {"format": "csv"}, + }, + feed_process_params_from_cli( + settings, ["items_1.dat:json", "items_2.dat:xml", "items_3.dat:csv"] + ), ) def test_feed_export_config_implicit_formats(self): settings = Settings() self.assertEqual( - {'items_1.json': {'format': 'json'}, 'items_2.xml': {'format': 'xml'}, 'items_3.csv': {'format': 'csv'}}, - feed_process_params_from_cli(settings, ['items_1.json', 'items_2.xml', 'items_3.csv']) + { + "items_1.json": {"format": "json"}, + "items_2.xml": {"format": "xml"}, + "items_3.csv": {"format": "csv"}, + }, + feed_process_params_from_cli( + settings, ["items_1.json", "items_2.xml", "items_3.csv"] + ), ) def test_feed_export_config_stdout(self): settings = Settings() self.assertEqual( - {'stdout:': {'format': 'pickle'}}, - feed_process_params_from_cli(settings, ['-:pickle']) + {"stdout:": {"format": "pickle"}}, + feed_process_params_from_cli(settings, ["-:pickle"]), ) def test_feed_export_config_overwrite(self): settings = Settings() self.assertEqual( - {'output.json': {'format': 'json', 'overwrite': True}}, - feed_process_params_from_cli(settings, [], None, ['output.json']) + {"output.json": {"format": "json", "overwrite": True}}, + feed_process_params_from_cli(settings, [], None, ["output.json"]), ) def test_output_and_overwrite_output(self): with self.assertRaises(UsageError): feed_process_params_from_cli( Settings(), - ['output1.json'], + ["output1.json"], None, - ['output2.json'], + ["output2.json"], ) def test_feed_complete_default_values_from_settings_empty(self): feed = {} - settings = Settings({ - "FEED_EXPORT_ENCODING": "custom encoding", - "FEED_EXPORT_FIELDS": ["f1", "f2", "f3"], - "FEED_EXPORT_INDENT": 42, - "FEED_STORE_EMPTY": True, - "FEED_URI_PARAMS": (1, 2, 3, 4), - "FEED_EXPORT_BATCH_ITEM_COUNT": 2, - }) + settings = Settings( + { + "FEED_EXPORT_ENCODING": "custom encoding", + "FEED_EXPORT_FIELDS": ["f1", "f2", "f3"], + "FEED_EXPORT_INDENT": 42, + "FEED_STORE_EMPTY": True, + "FEED_URI_PARAMS": (1, 2, 3, 4), + "FEED_EXPORT_BATCH_ITEM_COUNT": 2, + } + ) new_feed = feed_complete_default_values_from_settings(feed, settings) - self.assertEqual(new_feed, { - "encoding": "custom encoding", - "fields": ["f1", "f2", "f3"], - "indent": 42, - "store_empty": True, - "uri_params": (1, 2, 3, 4), - "batch_item_count": 2, - "item_export_kwargs": {}, - }) + self.assertEqual( + new_feed, + { + "encoding": "custom encoding", + "fields": ["f1", "f2", "f3"], + "indent": 42, + "store_empty": True, + "uri_params": (1, 2, 3, 4), + "batch_item_count": 2, + "item_export_kwargs": {}, + }, + ) def test_feed_complete_default_values_from_settings_non_empty(self): feed = { "encoding": "other encoding", "fields": None, } - settings = Settings({ - "FEED_EXPORT_ENCODING": "custom encoding", - "FEED_EXPORT_FIELDS": ["f1", "f2", "f3"], - "FEED_EXPORT_INDENT": 42, - "FEED_STORE_EMPTY": True, - "FEED_EXPORT_BATCH_ITEM_COUNT": 2, - }) + settings = Settings( + { + "FEED_EXPORT_ENCODING": "custom encoding", + "FEED_EXPORT_FIELDS": ["f1", "f2", "f3"], + "FEED_EXPORT_INDENT": 42, + "FEED_STORE_EMPTY": True, + "FEED_EXPORT_BATCH_ITEM_COUNT": 2, + } + ) new_feed = feed_complete_default_values_from_settings(feed, settings) - self.assertEqual(new_feed, { - "encoding": "other encoding", - "fields": None, - "indent": 42, - "store_empty": True, - "uri_params": None, - "batch_item_count": 2, - "item_export_kwargs": {}, - }) + self.assertEqual( + new_feed, + { + "encoding": "other encoding", + "fields": None, + "indent": 42, + "store_empty": True, + "uri_params": None, + "batch_item_count": 2, + "item_export_kwargs": {}, + }, + ) if __name__ == "__main__": diff --git a/tests/test_utils_console.py b/tests/test_utils_console.py index 380c41367..dabd6054d 100644 --- a/tests/test_utils_console.py +++ b/tests/test_utils_console.py @@ -1,14 +1,17 @@ import unittest from scrapy.utils.console import get_shell_embed_func + try: import bpython + bpy = True del bpython except ImportError: bpy = False try: import IPython + ipy = True del IPython except ImportError: @@ -16,29 +19,25 @@ except ImportError: class UtilsConsoleTestCase(unittest.TestCase): - def test_get_shell_embed_func(self): - - shell = get_shell_embed_func(['invalid']) + shell = get_shell_embed_func(["invalid"]) self.assertEqual(shell, None) - shell = get_shell_embed_func(['invalid', 'python']) + shell = get_shell_embed_func(["invalid", "python"]) self.assertTrue(callable(shell)) - self.assertEqual(shell.__name__, '_embed_standard_shell') + self.assertEqual(shell.__name__, "_embed_standard_shell") - @unittest.skipIf(not bpy, 'bpython not available in testenv') + @unittest.skipIf(not bpy, "bpython not available in testenv") def test_get_shell_embed_func2(self): - - shell = get_shell_embed_func(['bpython']) + shell = get_shell_embed_func(["bpython"]) self.assertTrue(callable(shell)) - self.assertEqual(shell.__name__, '_embed_bpython_shell') + self.assertEqual(shell.__name__, "_embed_bpython_shell") - @unittest.skipIf(not ipy, 'IPython not available in testenv') + @unittest.skipIf(not ipy, "IPython not available in testenv") def test_get_shell_embed_func3(self): - # default shell should be 'ipython' shell = get_shell_embed_func() - self.assertEqual(shell.__name__, '_embed_ipython_shell') + self.assertEqual(shell.__name__, "_embed_ipython_shell") if __name__ == "__main__": diff --git a/tests/test_utils_curl.py b/tests/test_utils_curl.py index f5d684d3f..1816db29b 100644 --- a/tests/test_utils_curl.py +++ b/tests/test_utils_curl.py @@ -34,10 +34,7 @@ class CurlToRequestKwargsTest(unittest.TestCase): "method": "GET", "url": "https://api.test.com/", "headers": [ - ( - "Authorization", - basic_auth_header("some_username", "some_password") - ) + ("Authorization", basic_auth_header("some_username", "some_password")) ], } self._test_command(curl_command, expected_result) @@ -77,11 +74,11 @@ class CurlToRequestKwargsTest(unittest.TestCase): ("Connection", "keep-alive"), ], "cookies": { - '_gauges_unique_year': '1', - '_gauges_unique_hour': '1', - '_gauges_unique_day': '1', - '_gauges_unique': '1', - '_gauges_unique_month': '1' + "_gauges_unique_year": "1", + "_gauges_unique_hour": "1", + "_gauges_unique_day": "1", + "_gauges_unique": "1", + "_gauges_unique_month": "1", }, } self._test_command(curl_command, expected_result) @@ -107,14 +104,14 @@ class CurlToRequestKwargsTest(unittest.TestCase): "method": "POST", "url": "http://httpbin.org/post", "body": "custname=John+Smith&custtel=500&custemail=jsmith%40exampl" - "e.org&size=small&topping=cheese&topping=onion&delivery=12" - "%3A15&comments=", + "e.org&size=small&topping=cheese&topping=onion&delivery=12" + "%3A15&comments=", "cookies": { - '_gauges_unique_year': '1', - '_gauges_unique_hour': '1', - '_gauges_unique_day': '1', - '_gauges_unique': '1', - '_gauges_unique_month': '1' + "_gauges_unique_year": "1", + "_gauges_unique_hour": "1", + "_gauges_unique_day": "1", + "_gauges_unique": "1", + "_gauges_unique_month": "1", }, "headers": [ ("Origin", "http://httpbin.org"), @@ -152,16 +149,26 @@ class CurlToRequestKwargsTest(unittest.TestCase): "body": ( "excerptLength=200&enableDidYouMean=true&sortCriteria=ffirstz3" "2xnamez32x201740686%20ascending&queryFunctions=%5B%5D&ranking" - "Functions=%5B%5D") + "Functions=%5B%5D" + ), + } + self._test_command(curl_command, expected_result) + + def test_post_data_raw_with_string_prefix(self): + curl_command = "curl 'https://www.example.org/' --data-raw $'{\"$filters\":\"Filter\u0021\"}'" + expected_result = { + "method": "POST", + "url": "https://www.example.org/", + "body": '{"$filters":"Filter!"}', } self._test_command(curl_command, expected_result) def test_explicit_get_with_data(self): - curl_command = 'curl httpbin.org/anything -X GET --data asdf' + curl_command = "curl httpbin.org/anything -X GET --data asdf" expected_result = { "method": "GET", "url": "http://httpbin.org/anything", - "body": "asdf" + "body": "asdf", } self._test_command(curl_command, expected_result) @@ -182,16 +189,14 @@ class CurlToRequestKwargsTest(unittest.TestCase): ("Authorization", basic_auth_header("username", "password")), ], "body": '{"hostname": "agent02.example.com", "agent_config_state"' - ': "Enabled", "resources": ["Java","Linux"], "environments' - '": ["Dev"]}', + ': "Enabled", "resources": ["Java","Linux"], "environments' + '": ["Dev"]}', } self._test_command(curl_command, expected_result) def test_delete(self): curl_command = 'curl -X "DELETE" https://www.url.com/page' - expected_result = { - "method": "DELETE", "url": "https://www.url.com/page" - } + expected_result = {"method": "DELETE", "url": "https://www.url.com/page"} self._test_command(curl_command, expected_result) def test_get_silent(self): @@ -209,8 +214,8 @@ class CurlToRequestKwargsTest(unittest.TestCase): def test_ignore_unknown_options(self): # case 1: ignore_unknown_options=True: with warnings.catch_warnings(): # avoid warning when executing tests - warnings.simplefilter('ignore') - curl_command = 'curl --bar --baz http://www.example.com' + warnings.simplefilter("ignore") + curl_command = "curl --bar --baz http://www.example.com" expected_result = {"method": "GET", "url": "http://www.example.com"} self.assertEqual(curl_to_request_kwargs(curl_command), expected_result) @@ -219,13 +224,12 @@ class CurlToRequestKwargsTest(unittest.TestCase): ValueError, "Unrecognized options:.*--bar.*--baz", lambda: curl_to_request_kwargs( - "curl --bar --baz http://www.example.com", - ignore_unknown_options=False + "curl --bar --baz http://www.example.com", ignore_unknown_options=False ), ) def test_must_start_with_curl_error(self): self.assertRaises( ValueError, - lambda: curl_to_request_kwargs("carl -X POST http://example.org") + lambda: curl_to_request_kwargs("carl -X POST http://example.org"), ) diff --git a/tests/test_utils_datatypes.py b/tests/test_utils_datatypes.py index e4bccf30e..9e5f88f48 100644 --- a/tests/test_utils_datatypes.py +++ b/tests/test_utils_datatypes.py @@ -1,28 +1,35 @@ import copy import unittest +import warnings from collections.abc import Mapping, MutableMapping +from typing import Iterator +from scrapy.exceptions import ScrapyDeprecationWarning from scrapy.http import Request -from scrapy.utils.datatypes import CaselessDict, LocalCache, LocalWeakReferencedCache, SequenceExclude +from scrapy.utils.datatypes import ( + CaseInsensitiveDict, + CaselessDict, + LocalCache, + LocalWeakReferencedCache, + SequenceExclude, +) from scrapy.utils.python import garbage_collect - -__doctests__ = ['scrapy.utils.datatypes'] +__doctests__ = ["scrapy.utils.datatypes"] -class CaselessDictTest(unittest.TestCase): - +class CaseInsensitiveDictMixin: def test_init_dict(self): - seq = {'red': 1, 'black': 3} - d = CaselessDict(seq) - self.assertEqual(d['red'], 1) - self.assertEqual(d['black'], 3) + seq = {"red": 1, "black": 3} + d = self.dict_class(seq) + self.assertEqual(d["red"], 1) + self.assertEqual(d["black"], 3) def test_init_pair_sequence(self): - seq = (('red', 1), ('black', 3)) - d = CaselessDict(seq) - self.assertEqual(d['red'], 1) - self.assertEqual(d['black'], 3) + seq = (("red", 1), ("black", 3)) + d = self.dict_class(seq) + self.assertEqual(d["red"], 1) + self.assertEqual(d["black"], 3) def test_init_mapping(self): class MyMapping(Mapping): @@ -39,9 +46,9 @@ class CaselessDictTest(unittest.TestCase): return len(self._d) seq = MyMapping(red=1, black=3) - d = CaselessDict(seq) - self.assertEqual(d['red'], 1) - self.assertEqual(d['black'], 3) + d = self.dict_class(seq) + self.assertEqual(d["red"], 1) + self.assertEqual(d["black"], 3) def test_init_mutable_mapping(self): class MyMutableMapping(MutableMapping): @@ -64,122 +71,165 @@ class CaselessDictTest(unittest.TestCase): return len(self._d) seq = MyMutableMapping(red=1, black=3) - d = CaselessDict(seq) - self.assertEqual(d['red'], 1) - self.assertEqual(d['black'], 3) + d = self.dict_class(seq) + self.assertEqual(d["red"], 1) + self.assertEqual(d["black"], 3) def test_caseless(self): - d = CaselessDict() - d['key_Lower'] = 1 - self.assertEqual(d['KEy_loWer'], 1) - self.assertEqual(d.get('KEy_loWer'), 1) + d = self.dict_class() + d["key_Lower"] = 1 + self.assertEqual(d["KEy_loWer"], 1) + self.assertEqual(d.get("KEy_loWer"), 1) - d['KEY_LOWER'] = 3 - self.assertEqual(d['key_Lower'], 3) - self.assertEqual(d.get('key_Lower'), 3) + d["KEY_LOWER"] = 3 + self.assertEqual(d["key_Lower"], 3) + self.assertEqual(d.get("key_Lower"), 3) def test_delete(self): - d = CaselessDict({'key_lower': 1}) - del d['key_LOWER'] - self.assertRaises(KeyError, d.__getitem__, 'key_LOWER') - self.assertRaises(KeyError, d.__getitem__, 'key_lower') + d = self.dict_class({"key_lower": 1}) + del d["key_LOWER"] + self.assertRaises(KeyError, d.__getitem__, "key_LOWER") + self.assertRaises(KeyError, d.__getitem__, "key_lower") def test_getdefault(self): d = CaselessDict() - self.assertEqual(d.get('c', 5), 5) - d['c'] = 10 - self.assertEqual(d.get('c', 5), 10) + self.assertEqual(d.get("c", 5), 5) + d["c"] = 10 + self.assertEqual(d.get("c", 5), 10) def test_setdefault(self): - d = CaselessDict({'a': 1, 'b': 2}) + d = CaselessDict({"a": 1, "b": 2}) - r = d.setdefault('A', 5) + r = d.setdefault("A", 5) self.assertEqual(r, 1) - self.assertEqual(d['A'], 1) + self.assertEqual(d["A"], 1) - r = d.setdefault('c', 5) + r = d.setdefault("c", 5) self.assertEqual(r, 5) - self.assertEqual(d['C'], 5) + self.assertEqual(d["C"], 5) def test_fromkeys(self): - keys = ('a', 'b') + keys = ("a", "b") - d = CaselessDict.fromkeys(keys) - self.assertEqual(d['A'], None) - self.assertEqual(d['B'], None) + d = self.dict_class.fromkeys(keys) + self.assertEqual(d["A"], None) + self.assertEqual(d["B"], None) - d = CaselessDict.fromkeys(keys, 1) - self.assertEqual(d['A'], 1) - self.assertEqual(d['B'], 1) + d = self.dict_class.fromkeys(keys, 1) + self.assertEqual(d["A"], 1) + self.assertEqual(d["B"], 1) - instance = CaselessDict() + instance = self.dict_class() d = instance.fromkeys(keys) - self.assertEqual(d['A'], None) - self.assertEqual(d['B'], None) + self.assertEqual(d["A"], None) + self.assertEqual(d["B"], None) d = instance.fromkeys(keys, 1) - self.assertEqual(d['A'], 1) - self.assertEqual(d['B'], 1) + self.assertEqual(d["A"], 1) + self.assertEqual(d["B"], 1) def test_contains(self): - d = CaselessDict() - d['a'] = 1 - assert 'a' in d + d = self.dict_class() + d["a"] = 1 + assert "A" in d def test_pop(self): - d = CaselessDict() - d['a'] = 1 - self.assertEqual(d.pop('A'), 1) - self.assertRaises(KeyError, d.pop, 'A') + d = self.dict_class() + d["a"] = 1 + self.assertEqual(d.pop("A"), 1) + self.assertRaises(KeyError, d.pop, "A") def test_normkey(self): - class MyDict(CaselessDict): - def normkey(self, key): + class MyDict(self.dict_class): + def _normkey(self, key): return key.title() + normkey = _normkey # deprecated CaselessDict class + d = MyDict() - d['key-one'] = 2 - self.assertEqual(list(d.keys()), ['Key-One']) + d["key-one"] = 2 + self.assertEqual(list(d.keys()), ["Key-One"]) def test_normvalue(self): - class MyDict(CaselessDict): - def normvalue(self, value): + class MyDict(self.dict_class): + def _normvalue(self, value): if value is not None: return value + 1 - d = MyDict({'key': 1}) - self.assertEqual(d['key'], 2) - self.assertEqual(d.get('key'), 2) + normvalue = _normvalue # deprecated CaselessDict class + + d = MyDict({"key": 1}) + self.assertEqual(d["key"], 2) + self.assertEqual(d.get("key"), 2) d = MyDict() - d['key'] = 1 - self.assertEqual(d['key'], 2) - self.assertEqual(d.get('key'), 2) + d["key"] = 1 + self.assertEqual(d["key"], 2) + self.assertEqual(d.get("key"), 2) d = MyDict() - d.setdefault('key', 1) - self.assertEqual(d['key'], 2) - self.assertEqual(d.get('key'), 2) + d.setdefault("key", 1) + self.assertEqual(d["key"], 2) + self.assertEqual(d.get("key"), 2) d = MyDict() - d.update({'key': 1}) - self.assertEqual(d['key'], 2) - self.assertEqual(d.get('key'), 2) + d.update({"key": 1}) + self.assertEqual(d["key"], 2) + self.assertEqual(d.get("key"), 2) - d = MyDict.fromkeys(('key',), 1) - self.assertEqual(d['key'], 2) - self.assertEqual(d.get('key'), 2) + d = MyDict.fromkeys(("key",), 1) + self.assertEqual(d["key"], 2) + self.assertEqual(d.get("key"), 2) def test_copy(self): - h1 = CaselessDict({'header1': 'value'}) + h1 = self.dict_class({"header1": "value"}) h2 = copy.copy(h1) + assert isinstance(h2, self.dict_class) self.assertEqual(h1, h2) - self.assertEqual(h1.get('header1'), h2.get('header1')) - assert isinstance(h2, CaselessDict) + self.assertEqual(h1.get("header1"), h2.get("header1")) + self.assertEqual(h1.get("header1"), h2.get("HEADER1")) + h3 = h1.copy() + assert isinstance(h3, self.dict_class) + self.assertEqual(h1, h3) + self.assertEqual(h1.get("header1"), h3.get("header1")) + self.assertEqual(h1.get("header1"), h3.get("HEADER1")) + + +class CaseInsensitiveDictTest(CaseInsensitiveDictMixin, unittest.TestCase): + dict_class = CaseInsensitiveDict + + def test_repr(self): + d1 = self.dict_class({"foo": "bar"}) + self.assertEqual(repr(d1), "