pax_global_header 0000666 0000000 0000000 00000000064 15250510124 0014505 g ustar 00root root 0000000 0000000 52 comment=8026deeaac371a5d9a3edbe4886d58f61139d464 scrapy-scrapy-5569de3/ 0000775 0000000 0000000 00000000000 15250510124 0014653 5 ustar 00root root 0000000 0000000 scrapy-scrapy-5569de3/.git-blame-ignore-revs 0000664 0000000 0000000 00000000502 15250510124 0020750 0 ustar 00root root 0000000 0000000 # .git-blame-ignore-revs # adding black formatter to all the code e211ec0aa26ecae0da8ae55d064ea60e1efe4d0d # reapplying black to the code with default line length 303f0a70fcf8067adf0a909c2096a5009162383a # reapplying black again and removing line length on pre-commit black config c5cdd0d30ceb68ccba04af0e71d1b8e6678e2962 scrapy-scrapy-5569de3/.gitattributes 0000664 0000000 0000000 00000000034 15250510124 0017543 0 ustar 00root root 0000000 0000000 tests/sample_data/** binary scrapy-scrapy-5569de3/.github/ 0000775 0000000 0000000 00000000000 15250510124 0016213 5 ustar 00root root 0000000 0000000 scrapy-scrapy-5569de3/.github/ISSUE_TEMPLATE/ 0000775 0000000 0000000 00000000000 15250510124 0020376 5 ustar 00root root 0000000 0000000 scrapy-scrapy-5569de3/.github/ISSUE_TEMPLATE/bug_report.md 0000664 0000000 0000000 00000002763 15250510124 0023100 0 ustar 00root root 0000000 0000000 --- name: Bug report about: Report a problem to help us improve --- ### Description [Description of the issue] ### Steps to Reproduce 1. [First Step] 2. [Second Step] 3. [and so on...] **Expected behavior:** [What you expect to happen] **Actual behavior:** [What actually happens] **Reproduces how often:** [What percentage of the time does it reproduce?] ### Versions Please paste here the output of executing `scrapy version --verbose` in the command line. ### Additional context Any additional information, configuration, data or output from commands that might be necessary to reproduce or understand the issue. Please try not to include screenshots of code or the command line, paste the contents as text instead. You can use [GitHub Flavored Markdown](https://help.github.com/en/articles/creating-and-highlighting-code-blocks) to make the text look better. scrapy-scrapy-5569de3/.github/ISSUE_TEMPLATE/feature_request.md 0000664 0000000 0000000 00000002370 15250510124 0024125 0 ustar 00root root 0000000 0000000 --- name: Feature request about: Suggest an idea for an enhancement or new feature --- ## Summary One paragraph explanation of the feature. ## Motivation Why are we doing this? What use cases does it support? What is the expected outcome? ## Describe alternatives you've considered A clear and concise description of the alternative solutions you've considered. Be sure to explain why Scrapy's existing customizability isn't suitable for this feature. ## Additional context Any additional information about the feature request here. scrapy-scrapy-5569de3/.github/ISSUE_TEMPLATE/question.md 0000664 0000000 0000000 00000000644 15250510124 0022573 0 ustar 00root root 0000000 0000000 --- 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. scrapy-scrapy-5569de3/.github/dependabot.yml 0000664 0000000 0000000 00000000333 15250510124 0021042 0 ustar 00root root 0000000 0000000 version: 2 updates: - package-ecosystem: github-actions directory: "/" schedule: interval: monthly groups: github-actions: patterns: - "*" cooldown: default-days: 7 scrapy-scrapy-5569de3/.github/workflows/ 0000775 0000000 0000000 00000000000 15250510124 0020250 5 ustar 00root root 0000000 0000000 scrapy-scrapy-5569de3/.github/workflows/checks.yml 0000664 0000000 0000000 00000004130 15250510124 0022231 0 ustar 00root root 0000000 0000000 name: Checks permissions: contents: read on: push: branches: - master - '[0-9]+.[0-9]+' pull_request: types: [opened, synchronize, reopened, closed] concurrency: group: ${{github.workflow}}-${{ github.head_ref || github.ref }} cancel-in-progress: true jobs: checks: if: github.event.action != 'closed' runs-on: ubuntu-latest timeout-minutes: 10 env: # Make uv use the interpreter that actions/setup-python installed instead # of downloading one of its own. UV_PYTHON_PREFERENCE: only-system strategy: fail-fast: false matrix: include: - python-version: "3.14" env: TOXENV: pylint - python-version: "3.10" env: TOXENV: mypy - python-version: "3.10" env: TOXENV: mypy-tests # Keep in sync with pyproject.toml tool.sphinx-scrapy.python-version. - python-version: "3.14" env: TOXENV: docs - python-version: "3.13" env: TOXENV: docs-tests - python-version: "3.14" env: TOXENV: twinecheck steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: ${{ matrix.python-version }} - name: Set up uv uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 with: cache-dependency-glob: | docs/requirements.txt pyproject.toml tox.ini - name: Run check env: ${{ matrix.env }} run: uvx --with tox-uv tox pre-commit: if: github.event.action != 'closed' runs-on: ubuntu-latest timeout-minutes: 10 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # v3.0.1 scrapy-scrapy-5569de3/.github/workflows/codspeed.yml 0000664 0000000 0000000 00000007117 15250510124 0022567 0 ustar 00root root 0000000 0000000 --- name: codspeed on: push: branches: - master pull_request: types: [opened, synchronize, reopened, closed] paths: - scrapy/** - tests/benchmarks/** - .github/workflows/codspeed.yml - tox.ini workflow_dispatch: concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.ref }} cancel-in-progress: true permissions: {} jobs: benchmark: if: github.event.action != 'closed' runs-on: ubuntu-latest timeout-minutes: 25 env: # Make uv use the interpreter that actions/setup-python installed # instead of downloading one of its own. UV_PYTHON_PREFERENCE: only-system permissions: contents: read id-token: write # OIDC authentication with CodSpeed steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: '3.14' - name: Set up uv uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 with: cache-dependency-glob: | pyproject.toml tox.ini - name: Install dependencies # tox must stay on PATH for the CodSpeed action to invoke it. run: | uv tool install --with tox-uv tox tox -n -e benchmark - name: Run benchmarks uses: CodSpeedHQ/action@373d6868929f444bc08d901fd0eb0ad52a8875ea # v5.2.1 with: mode: simulation run: tox -e benchmark benchmark-walltime: # Only benchmarks marked "walltime" (ones where a real concurrency # win can't be shown by instruction counting) run here, on # CodSpeed's dedicated, minutes-limited macro runner; everything # else runs above, on a regular GitHub-hosted runner. if: github.event.action != 'closed' runs-on: codspeed-macro env: UV_PYTHON_PREFERENCE: only-system permissions: contents: read id-token: write # OIDC authentication with CodSpeed steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: '3.14' - name: Set up uv uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 with: cache-dependency-glob: | pyproject.toml tox.ini - name: Install dependencies run: | uv tool install --with tox-uv tox tox -n -e benchmark - name: Run benchmarks uses: CodSpeedHQ/action@373d6868929f444bc08d901fd0eb0ad52a8875ea # v5.2.1 with: mode: walltime # No benchmark is walltime-marked on some branches (e.g. # this one), which would otherwise fail the step. allow-empty: true run: tox -e benchmark -- tests/benchmarks -m walltime scrapy-scrapy-5569de3/.github/workflows/flag-prs-for-triage.yml 0000664 0000000 0000000 00000033100 15250510124 0024540 0 ustar 00root root 0000000 0000000 name: Flag PRs for triage # Labels pull requests whose author's public activity suggests that an LLM is # writing them without supervision, and records the evidence in the workflow # run summary so that triaging one does not require reading a user profile. # # Four independent signals, any of which is enough to label. Each one abstains # when the data it needs is unavailable, so a missing signal never counts # against an author: # # - Rejection burst: pull requests of theirs closed unmerged elsewhere within # the last month. Volume of rejections in absolute terms separates spraying # from ordinary contribution far better than a merge ratio does, since # ratios reward authors who accumulate merges in trivial repositories. # - Spray breadth: unrelated repositories they open pull requests against # within one week. Breadth catches an agent on its first day, before any of # its pull requests have been closed, and it comes from the event feed, so it # also covers authors that the search API refuses to return. # - Assistant voice: their recent comments across GitHub read as assistant # output rather than as a developer talking, by section headings, bullet # lists, em dash density or stock acknowledgement phrases. # - Agent branch: the branch name carries an agent prefix. # # Authors that the organisations behind this repository already trust are left # alone before any of that runs: public members of those organisations, and # authors with a track record of pull requests merged into their repositories. # Trust from a merge record rather than from a list of names keeps the exemption # in step with who is actually contributing. # # Deliberately not used: account age, fork age, follower count, total pull # request count and cross-repository merge ratio. All of them were measured # against hand-labelled pull requests and either failed to separate or, in the # case of the merge ratio, inverted on held-out data. # # The label is advisory, and it says the author's history is worth a look # before reviewing in depth; it does not say the pull request is bad. # # The workflow only reads pull request and public activity metadata through the # API, it never checks out or runs pull request code, so pull_request_target is # safe here. on: # zizmor: ignore[dangerous-triggers] pull_request_target: types: [opened] permissions: contents: read pull-requests: write jobs: flag-pr-for-triage: name: Label PR if the author's activity suggests unsupervised LLM use runs-on: ubuntu-latest steps: - name: Score the author and label the PR uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | const LABEL = 'needs triage'; const RETRIES = 5; const RETRY_WAIT_MS = 60000; const REJECTION_WINDOW_DAYS = 30; const MIN_REJECTIONS = 1; const MIN_COMMENTS = 2; const MAX_REPOS_PER_WEEK = 2; const VOICE = { structure: 0.10, emDashPerKChar: 0.30, acknowledgement: 0.40 }; const EVENT_PAGES = 3; const TRUSTED_ORGS = ['scrapy', 'scrapy-plugins', 'scrapinghub', 'zytedata']; const MIN_TRUSTED_MERGES = 10; const AGENT_BRANCH = /^(agent|codex|claude|cursor|devin|copilot|jules|bot)[\/_-]/i; const { owner, repo } = context.repo; const pr = context.payload.pull_request; const author = pr.user.login; if (pr.user.type === 'Bot' || ['MEMBER', 'OWNER', 'COLLABORATOR'].includes(pr.author_association)) { core.info(`Skipping PR #${pr.number} by ${author} (${pr.user.type}, ${pr.author_association}).`); return; } // Rate and abuse limits reset on the order of a minute, so waiting // is enough; other errors are not worth retrying. const retriable = new Set([403, 429, 500, 502, 503, 504]); const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)); async function withRetries(description, call) { for (let attempt = 1; ; attempt++) { try { return await call(); } catch (error) { if (!retriable.has(error.status) || attempt > RETRIES) throw error; const reset = Number(error.response?.headers?.['x-ratelimit-reset']) * 1000 - Date.now(); const after = Number(error.response?.headers?.['retry-after']) * 1000; const wait = Math.min(Math.max(after || reset || RETRY_WAIT_MS, RETRY_WAIT_MS), 15 * RETRY_WAIT_MS); core.info(`${description} failed with ${error.status}, retrying in ${Math.round(wait / 1000)}s (attempt ${attempt}/${RETRIES}).`); await sleep(wait); } } } // Accounts excluded from search, deleted users and the like leave a // signal unmeasurable rather than negative. const orNull = promise => promise.catch(error => { if ([404, 410, 422].includes(error.status)) return null; throw error; }); // author_association only reports membership of the organisation // that owns this repository, and only when it is public, so trust // in the author is established here instead. const trustedOrg = (await Promise.all(TRUSTED_ORGS.map(org => orNull(withRetries(`Checking public membership of ${org}`, () => github.rest.orgs.checkPublicMembershipForUser({ org, username: author }), )).then(response => response && org), ))).find(Boolean); if (trustedOrg) { core.info(`Skipping PR #${pr.number} by ${author} (public member of ${trustedOrg}).`); return; } // Repeating a qualifier narrows the search instead of widening it, // hence the explicit disjunction. const trustedMerges = await orNull(withRetries('Counting merged PRs in trusted organisations', () => github.rest.search.issuesAndPullRequests({ q: `author:${author} type:pr is:merged` + ` (${TRUSTED_ORGS.map(org => `org:${org}`).join(' OR ')})`, advanced_search: 'true', per_page: 1, }).then(response => response.data.total_count), )); if (trustedMerges >= MIN_TRUSTED_MERGES) { core.info(`Skipping PR #${pr.number} by ${author}` + ` (${trustedMerges} PR(s) merged into ${TRUSTED_ORGS.join(', ')}).`); return; } const opened = new Date(pr.created_at); const daysBefore = date => (opened - new Date(date)) / 86400000; // Signal 1: pull requests closed unmerged elsewhere, recently. const search = await orNull(withRetries('Searching for PRs by the author', () => github.rest.search.issuesAndPullRequests({ q: `author:${author} type:pr`, advanced_search: 'true', sort: 'created', order: 'desc', per_page: 100, }).then(response => response.data), )); let rejections = null; if (search) { rejections = search.items.filter(item => { const itemOwner = item.repository_url.split('/repos/')[1].split('/')[0].toLowerCase(); return itemOwner !== author.toLowerCase() && item.state === 'closed' && !item.pull_request?.merged_at && daysBefore(item.created_at) >= 0 && daysBefore(item.created_at) <= REJECTION_WINDOW_DAYS; }).map(item => item.html_url); } // Signal 2: how their recent comments across GitHub read. const events = []; for (let page = 1; page <= EVENT_PAGES; page++) { const batch = await orNull(withRetries(`Reading public events page ${page}`, () => github.rest.activity.listPublicEventsForUser({ username: author, per_page: 100, page, }).then(response => response.data), )); if (!batch?.length) break; events.push(...batch); if (batch.length < 100) break; } const comments = events .filter(event => ['IssueCommentEvent', 'PullRequestReviewCommentEvent'].includes(event.type)) .map(event => event.payload?.comment?.body) .filter(Boolean); // Signal 3: how many unrelated projects they open pull requests // against in a single week. Breadth rather than volume: a focused // contributor sends many pull requests to few repositories, while // an unattended agent sprays a few across many. Taken from the // event feed, which unlike search covers authors that search // refuses to return. const weeks = {}; for (const event of events) { if (event.type !== 'PullRequestEvent' || event.payload?.action !== 'opened') continue; const name = event.repo?.name; if (!name || name.toLowerCase().startsWith(`${author.toLowerCase()}/`)) continue; const week = Math.floor(new Date(event.created_at) / (7 * 86400000)); (weeks[week] ??= new Set()).add(name); } const breadth = events.length ? Math.max(0, ...Object.values(weeks).map(repos => repos.size)) : null; const STRUCTURE = [/^\s*#{2,3}\s/m, /^\s*[-*]\s.+\n\s*[-*]\s/m, /\*\*[^*]+\*\*/, /```/]; const ACKNOWLEDGEMENT = [ /thanks for (the )?(review|feedback|pointing|catching|flagging|clarif)/i, /you'?re (absolutely )?right/i, /great catch/i, /that makes sense/i, /i'?ll (continue|investigate|update|submit|look into|make sure)/i, /let me know (if|whether)/i, /happy to (update|adjust|revise|change)/i, /i understand that/i, /thanks for your time/i, /just following up/i, /hope (this|that) helps/i, /please let me know/i, /i'?ve (updated|addressed|fixed)/i, ]; let voice = null; if (comments.length >= MIN_COMMENTS) { const chars = comments.reduce((total, body) => total + body.length, 0); const rate = patterns => comments.filter(body => patterns.some(re => re.test(body))).length / comments.length; voice = { comments: comments.length, structure: rate(STRUCTURE), acknowledgement: rate(ACKNOWLEDGEMENT), emDashPerKChar: 1000 * comments.reduce((total, body) => total + (body.match(/—/g) || []).length, 0) / chars, }; } const reasons = []; if (rejections && rejections.length >= MIN_REJECTIONS) { reasons.push(`${rejections.length} PR(s) of theirs closed unmerged elsewhere in the last` + ` ${REJECTION_WINDOW_DAYS} days: ${rejections.slice(0, 10).join(' ')}`); } if (voice && (voice.structure > VOICE.structure || voice.emDashPerKChar > VOICE.emDashPerKChar || voice.acknowledgement > VOICE.acknowledgement)) { reasons.push(`comment style over ${voice.comments} recent comments:` + ` ${(100 * voice.structure).toFixed(0)}% structured,` + ` ${(100 * voice.acknowledgement).toFixed(0)}% stock acknowledgements,` + ` ${voice.emDashPerKChar.toFixed(2)} em dashes per 1000 characters`); } if (breadth !== null && breadth > MAX_REPOS_PER_WEEK) { reasons.push(`opened pull requests against ${breadth} unrelated repositories within a week`); } if (AGENT_BRANCH.test(pr.head?.ref || '')) { reasons.push(`branch name carries an agent prefix: ${pr.head.ref}`); } await core.summary .addHeading(`PR #${pr.number} by ${author}`, 3) .addList([ rejections === null ? 'recent rejections elsewhere: unmeasurable, the author cannot be searched' : `recent rejections elsewhere: ${rejections.length}`, voice === null ? `comment style: unmeasurable, fewer than ${MIN_COMMENTS} recent comments found` : `comment style: ${(100 * voice.structure).toFixed(0)}% structured,` + ` ${(100 * voice.acknowledgement).toFixed(0)}% stock acknowledgements,` + ` ${voice.emDashPerKChar.toFixed(2)} em dashes per 1000 characters` + ` over ${voice.comments} comments`, breadth === null ? 'repositories per week: unmeasurable, no public events found' : `repositories per week, at most: ${breadth}`, `branch: ${pr.head?.ref ?? 'unknown'}`, `verdict: ${reasons.length ? `labelled "${LABEL}"` : 'not labelled'}`, ]) .addRaw(reasons.length ? `\n${reasons.map(reason => `- ${reason}`).join('\n')}\n` : '') .write(); if (!reasons.length) { core.info(`Not labelling PR #${pr.number}.`); return; } await withRetries('Adding the label', () => github.rest.issues.addLabels({ owner, repo, issue_number: pr.number, labels: [LABEL] }), ); core.info(`Labelled PR #${pr.number}: ${reasons.join(' | ')}`); scrapy-scrapy-5569de3/.github/workflows/publish.yml 0000664 0000000 0000000 00000002431 15250510124 0022441 0 ustar 00root root 0000000 0000000 name: Publish permissions: contents: read on: push: tags: - '[0-9]+.[0-9]+.[0-9]+' concurrency: group: ${{github.workflow}}-${{ github.ref }} cancel-in-progress: true jobs: build: name: Build distribution runs-on: ubuntu-latest steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" - run: | python -m pip install --upgrade build python -m build - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: python-package-distributions path: dist/ publish: name: Upload release to PyPI needs: - build runs-on: ubuntu-latest environment: name: pypi url: https://pypi.org/p/Scrapy permissions: id-token: write steps: - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: python-package-distributions path: dist/ - name: Publish to PyPI uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2 scrapy-scrapy-5569de3/.github/workflows/tests-macos.yml 0000664 0000000 0000000 00000004626 15250510124 0023245 0 ustar 00root root 0000000 0000000 name: macOS permissions: contents: read on: push: branches: - master - '[0-9]+.[0-9]+' pull_request: types: [opened, synchronize, reopened, closed] concurrency: group: ${{github.workflow}}-${{ github.head_ref || github.ref }} cancel-in-progress: true jobs: tests: if: github.event.action != 'closed' name: tests (${{ matrix.python-version }}, ${{ matrix.env.TOXENV }}) runs-on: macos-latest timeout-minutes: 15 env: PYTEST_ADDOPTS: ${{ matrix.coverage && '-n logical' || '-n logical --no-cov' }} # Make uv use the interpreter that actions/setup-python installed instead # of downloading one of its own. UV_PYTHON_PREFERENCE: only-system strategy: fail-fast: false matrix: include: - python-version: "3.10" env: TOXENV: py - python-version: "3.14" env: TOXENV: py coverage: true - python-version: "3.14" env: TOXENV: no-reactor # 3.15 pre-release - python-version: "3.15.0-rc.1" env: TOXENV: py - python-version: "3.15.0-rc.1" env: TOXENV: no-reactor steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: ${{ matrix.python-version }} - name: Set up uv uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 with: cache-dependency-glob: | pyproject.toml tox.ini - name: Install mitmproxy env: # mitmproxy needs a newer Python than the oldest matrix entries, so let # uv download one where no system interpreter is new enough. UV_PYTHON_PREFERENCE: system run: uv tool install mitmproxy - name: Run tests env: ${{ matrix.env }} run: uvx --with tox-uv tox - name: Upload coverage report if: ${{ matrix.coverage }} uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 - name: Upload test results if: ${{ !cancelled() }} uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 with: report_type: test_results scrapy-scrapy-5569de3/.github/workflows/tests-ubuntu.yml 0000664 0000000 0000000 00000010400 15250510124 0023450 0 ustar 00root root 0000000 0000000 name: Ubuntu permissions: contents: read on: push: branches: - master - '[0-9]+.[0-9]+' pull_request: types: [opened, synchronize, reopened, closed] concurrency: group: ${{github.workflow}}-${{ github.head_ref || github.ref }} cancel-in-progress: true jobs: tests: if: github.event.action != 'closed' name: tests (${{ matrix.python-version }}, ${{ matrix.env.TOXENV }}) runs-on: ubuntu-latest timeout-minutes: 20 env: PYTEST_ADDOPTS: ${{ matrix.coverage && '-n logical' || '-n logical --no-cov' }} # Make uv use the interpreter that actions/setup-python installed instead # of downloading one of its own. UV_PYTHON_PREFERENCE: only-system strategy: fail-fast: false matrix: include: - python-version: "3.10" env: TOXENV: py - python-version: "3.11" env: TOXENV: py - python-version: "3.12" env: TOXENV: py - python-version: "3.13" env: TOXENV: py - python-version: "3.14" env: TOXENV: py coverage: true - python-version: "3.14" env: TOXENV: default-reactor coverage: true - python-version: "3.14" env: TOXENV: no-reactor coverage: true # min deps - python-version: "3.10.19" env: TOXENV: min coverage: true - python-version: "3.10.19" env: TOXENV: min-default-reactor coverage: true # pinned due to https://github.com/pypy/pypy/issues/5388 - python-version: pypy3.11-7.3.20 env: TOXENV: min-pypy3 - python-version: "3.10.19" env: TOXENV: min-extra-deps coverage: true - python-version: "3.10.19" env: TOXENV: min-botocore coverage: true - python-version: "3.14" env: TOXENV: extra-deps coverage: true - python-version: "3.14" env: TOXENV: no-reactor-extra-deps coverage: true # pinned due to https://github.com/pypy/pypy/issues/5388 - python-version: pypy3.11-7.3.20 env: TOXENV: pypy3-extra-deps coverage: true - python-version: "3.14" env: TOXENV: botocore coverage: true # 3.15 pre-release - python-version: "3.15.0-rc.1" env: TOXENV: py - python-version: "3.15.0-rc.1" env: TOXENV: default-reactor - python-version: "3.15.0-rc.1" env: TOXENV: no-reactor steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: ${{ matrix.python-version }} - name: Install system libraries if: contains(matrix.python-version, 'pypy') || contains(matrix.env.TOXENV, 'min') run: | sudo apt-get update sudo apt-get install libxml2-dev libxslt-dev - name: Set up uv uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 with: cache-dependency-glob: | pyproject.toml tox.ini - name: Install mitmproxy env: # mitmproxy needs a newer Python than the oldest matrix entries, so let # uv download one where no system interpreter is new enough. UV_PYTHON_PREFERENCE: system # mitmproxy has no PyPy wheels, so run it on CPython regardless of the # interpreter under test. run: uv tool install --python cpython mitmproxy - name: Run tests env: ${{ matrix.env }} run: uvx --with tox-uv tox - name: Upload coverage report if: ${{ matrix.coverage }} uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 - name: Upload test results if: ${{ !cancelled() }} uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 with: report_type: test_results scrapy-scrapy-5569de3/.github/workflows/tests-vcs-deps.yml 0000664 0000000 0000000 00000003131 15250510124 0023655 0 ustar 00root root 0000000 0000000 name: VCS dependencies permissions: contents: read on: schedule: - cron: '0 4 * * *' workflow_dispatch: concurrency: group: ${{github.workflow}}-${{ github.ref }} cancel-in-progress: true jobs: tests: name: tests runs-on: ubuntu-latest timeout-minutes: 30 env: # A development branch of a dependency can make a test hang forever, so # tests get a time limit here that they do not need elsewhere. PYTEST_ADDOPTS: -n logical --no-cov --timeout=120 TOXENV: vcs-deps UV_PYTHON_PREFERENCE: only-system steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.15" allow-prereleases: true # Dependencies that ship wheels on PyPI are built from source here, so # their build dependencies are needed: libxml2 and libxslt for lxml, # libjpeg and zlib for Pillow, and autotools for the libuv bundled in # uvloop. - name: Install system libraries run: | sudo apt-get update sudo apt-get install automake libjpeg-dev libtool libxml2-dev libxslt-dev zlib1g-dev - name: Set up uv uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 with: cache-dependency-glob: | pyproject.toml tox.ini - name: Install mitmproxy run: uv tool install --python cpython mitmproxy - name: Run tests run: uvx --with tox-uv tox scrapy-scrapy-5569de3/.github/workflows/tests-windows.yml 0000664 0000000 0000000 00000005370 15250510124 0023632 0 ustar 00root root 0000000 0000000 name: Windows permissions: contents: read on: push: branches: - master - '[0-9]+.[0-9]+' pull_request: types: [opened, synchronize, reopened, closed] concurrency: group: ${{github.workflow}}-${{ github.head_ref || github.ref }} cancel-in-progress: true jobs: tests: if: github.event.action != 'closed' name: tests (${{ matrix.python-version }}, ${{ matrix.env.TOXENV }}) runs-on: windows-latest timeout-minutes: 20 env: PYTEST_ADDOPTS: ${{ matrix.coverage && '-n logical' || '-n logical --no-cov' }} # Make uv use the interpreter that actions/setup-python installed instead # of downloading one of its own. UV_PYTHON_PREFERENCE: only-system strategy: fail-fast: false matrix: include: - python-version: "3.10" env: TOXENV: py - python-version: "3.14" env: TOXENV: py coverage: true - python-version: "3.14" env: TOXENV: no-reactor # min deps - python-version: "3.10.11" env: TOXENV: min - python-version: "3.10.11" env: TOXENV: min-extra-deps - python-version: "3.14" env: TOXENV: extra-deps # 3.15 pre-release - python-version: "3.15.0-rc.1" env: TOXENV: py - python-version: "3.15.0-rc.1" env: TOXENV: default-reactor - python-version: "3.15.0-rc.1" env: TOXENV: no-reactor steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: ${{ matrix.python-version }} - name: Set up uv uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 with: cache-dependency-glob: | pyproject.toml tox.ini - name: Install mitmproxy env: # mitmproxy needs a newer Python than the oldest matrix entries, so let # uv download one where no system interpreter is new enough. UV_PYTHON_PREFERENCE: system run: uv tool install mitmproxy - name: Run tests env: ${{ matrix.env }} run: uvx --with tox-uv tox - name: Upload coverage report if: ${{ matrix.coverage }} uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 - name: Upload test results if: ${{ !cancelled() }} uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 with: report_type: test_results scrapy-scrapy-5569de3/.gitignore 0000664 0000000 0000000 00000000514 15250510124 0016643 0 ustar 00root root 0000000 0000000 /.vagrant /scrapy.iml *.pyc _trial_temp* dropin.cache docs/_build *egg-info .tox/ venv/ .venv/ build/ dist/ .idea/ .vscode/ htmlcov/ .pytest_cache/ .coverage .coverage.* coverage.* *.junit.xml test-output.* .cache/ .mypy_cache/ /tests/keys/localhost.crt /tests/keys/localhost.key # Windows Thumbs.db # OSX miscellaneous .DS_Store scrapy-scrapy-5569de3/.pre-commit-config.yaml 0000664 0000000 0000000 00000001510 15250510124 0021131 0 ustar 00root root 0000000 0000000 exclude: | (?x)( ^docs/_static| ^docs/_tests| ^tests/sample_data ) repos: - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.15.20 hooks: - id: ruff-check args: [ --fix ] - id: ruff-format - repo: https://github.com/adamchainz/blacken-docs rev: 1.20.0 hooks: - id: blacken-docs additional_dependencies: - black==26.5.1 - repo: https://github.com/pre-commit/pre-commit-hooks rev: v6.0.0 hooks: - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/sphinx-contrib/sphinx-lint rev: v1.0.2 hooks: - id: sphinx-lint - repo: https://github.com/scrapy/sphinx-scrapy rev: 0.9.0 hooks: - id: sphinx-scrapy - repo: https://github.com/zizmorcore/zizmor-pre-commit rev: v1.28.0 hooks: - id: zizmor args: [--no-progress, --fix] scrapy-scrapy-5569de3/.readthedocs.yml 0000664 0000000 0000000 00000000325 15250510124 0017741 0 ustar 00root root 0000000 0000000 version: 2 build: os: ubuntu-24.04 tools: python: "3.14" commands: - pip install tox - tox -e docs - mkdir -p $READTHEDOCS_OUTPUT/html - cp -a docs/_build/all/. $READTHEDOCS_OUTPUT/html/ scrapy-scrapy-5569de3/AUTHORS 0000664 0000000 0000000 00000002404 15250510124 0015723 0 ustar 00root root 0000000 0000000 Scrapy was brought to life by Shane Evans while hacking a scraping framework prototype for Mydeco (mydeco.com). It soon became maintained, extended and improved by Insophia (insophia.com), with the initial sponsorship of Mydeco to bootstrap the project. In mid-2011, Scrapinghub (now Zyte) became the new official maintainer. Here is the list of the primary authors & contributors: * Pablo Hoffman * Daniel Graña * Martin Olveyra * Gabriel GarcÃa * Michael Cetrulo * Artem Bogomyagkov * Damian Canabal * Andres Moreira * Ismael Carnales * MatÃas Aguirre * German Hoffmann * Anibal Pacheco * Bruno Deferrari * Shane Evans * Ezequiel Rivero * Patrick Mezard * Rolando Espinoza * Ping Yin * Lucian Ursu * Shuaib Khan * Didier Deshommes * Vikas Dhiman * Jochen Maes * Darian Moody * Jordi Lonch * Zuhao Wan * Steven Almeroth * Tom Mortimer-Jones * Chris Tilden * Alexandr N Zamaraev * Emanuel Schorsch * Michal Danilak * Natan Lao * Hasnain Lakhani * Pedro Faustino * Alex Cepoi * Ilya Baryshev * Libor Nenadál * Jae-Myoung Yu * Vladislav Poluhin * Marc Abramowitz * Valentin-Costel Hăloiu * Jason Yeo * Сергей Прохоров * Simon Ratne * Julien Duponchelle * Jochen Maes * Vikas Dhiman * Juan Picca * Nicolás RamÃrez scrapy-scrapy-5569de3/CITATION.cff 0000664 0000000 0000000 00000000253 15250510124 0016545 0 ustar 00root root 0000000 0000000 cff-version: 1.2.0 message: If you use Scrapy in published research, please cite it as below. title: Scrapy authors: - name: Scrapy contributors url: https://scrapy.org scrapy-scrapy-5569de3/CODE_OF_CONDUCT.md 0000664 0000000 0000000 00000012555 15250510124 0017462 0 ustar 00root root 0000000 0000000 # Contributor Covenant Code of Conduct ## Our Pledge We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation. We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. ## Our Standards Examples of behavior that contributes to a positive environment for our community include: * Demonstrating empathy and kindness toward other people * Being respectful of differing opinions, viewpoints, and experiences * Giving and gracefully accepting constructive feedback * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience * Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: * The use of sexualized language or imagery, and sexual attention or advances of any kind * Trolling, insulting or derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or email address, without their explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. ## Scope This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at opensource@zyte.com. All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the reporter of any incident. ## Enforcement Guidelines Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: ### 1. Correction **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. ### 2. Warning **Community Impact**: A violation through a single incident or series of actions. **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. ### 3. Temporary Ban **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. ### 4. Permanent Ban **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. **Consequence**: A permanent ban from any sort of public interaction within the community. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.1, available at [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. For answers to common questions about this code of conduct, see the FAQ at [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at [https://www.contributor-covenant.org/translations][translations]. [homepage]: https://www.contributor-covenant.org [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html [Mozilla CoC]: https://github.com/mozilla/diversity [FAQ]: https://www.contributor-covenant.org/faq [translations]: https://www.contributor-covenant.org/translations scrapy-scrapy-5569de3/CONTRIBUTING.md 0000664 0000000 0000000 00000000457 15250510124 0017112 0 ustar 00root root 0000000 0000000 The guidelines for contributing are available here: https://docs.scrapy.org/en/master/contributing.html Please do not abuse the issue tracker for support questions. If your issue topic can be rephrased to "How to ...?", please use the support channels to get it answered: https://scrapy.org/community/ scrapy-scrapy-5569de3/INSTALL.md 0000664 0000000 0000000 00000000233 15250510124 0016301 0 ustar 00root root 0000000 0000000 For information about installing Scrapy see: * [Local docs](docs/intro/install.rst) * [Online docs](https://docs.scrapy.org/en/latest/intro/install.html) scrapy-scrapy-5569de3/LICENSE 0000664 0000000 0000000 00000002755 15250510124 0015671 0 ustar 00root root 0000000 0000000 Copyright (c) Scrapy developers. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions, and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions, and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of Scrapy nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. scrapy-scrapy-5569de3/NEWS 0000664 0000000 0000000 00000000022 15250510124 0015344 0 ustar 00root root 0000000 0000000 See docs/news.rst scrapy-scrapy-5569de3/README.rst 0000664 0000000 0000000 00000003401 15250510124 0016340 0 ustar 00root root 0000000 0000000 |logo| .. |logo| image:: https://raw.githubusercontent.com/scrapy/scrapy/master/docs/_static/logo.svg :target: https://scrapy.org :alt: Scrapy :width: 480px |version| |python_version| |tests| |coverage| |conda| |deepwiki| .. |version| image:: https://img.shields.io/pypi/v/Scrapy.svg :target: https://pypi.org/pypi/Scrapy :alt: PyPI Version .. |python_version| image:: https://img.shields.io/pypi/pyversions/Scrapy.svg :target: https://pypi.org/pypi/Scrapy :alt: Supported Python Versions .. |tests| image:: https://img.shields.io/github/check-runs/scrapy/scrapy/master?label=tests :target: https://github.com/scrapy/scrapy/actions?query=branch%3Amaster :alt: Tests .. |coverage| image:: https://img.shields.io/codecov/c/github/scrapy/scrapy/master.svg :target: https://codecov.io/github/scrapy/scrapy?branch=master :alt: Coverage report .. |conda| image:: https://anaconda.org/conda-forge/scrapy/badges/version.svg :target: https://anaconda.org/conda-forge/scrapy :alt: Conda Version .. |deepwiki| image:: https://deepwiki.com/badge.svg :target: https://deepwiki.com/scrapy/scrapy :alt: Ask DeepWiki Scrapy_ is a web scraping framework to extract structured data from websites. It is cross-platform, and requires Python 3.10+. It is maintained by Zyte_ (formerly Scrapinghub) and `many other contributors`_. .. _many other contributors: https://github.com/scrapy/scrapy/graphs/contributors .. _Scrapy: https://scrapy.org/ .. _Zyte: https://www.zyte.com/ Install with: .. code:: bash pip install scrapy And follow the documentation_ to learn how to use it. .. _documentation: https://docs.scrapy.org/en/latest/ If you wish to contribute, see Contributing_. .. _Contributing: https://docs.scrapy.org/en/master/contributing.html scrapy-scrapy-5569de3/SECURITY.md 0000664 0000000 0000000 00000000463 15250510124 0016447 0 ustar 00root root 0000000 0000000 # Security Policy ## Supported Versions | Version | Supported | | ------- | ------------------ | | 2.19.x | :white_check_mark: | | < 2.19.x | :x: | ## Reporting a Vulnerability Please report the vulnerability using https://github.com/scrapy/scrapy/security/advisories/new. scrapy-scrapy-5569de3/codecov.yml 0000664 0000000 0000000 00000000120 15250510124 0017011 0 ustar 00root root 0000000 0000000 comment: layout: "header, diff, tree" coverage: status: project: false scrapy-scrapy-5569de3/conftest.py 0000664 0000000 0000000 00000017542 15250510124 0017063 0 ustar 00root root 0000000 0000000 from __future__ import annotations import os from importlib.util import find_spec from pathlib import Path from typing import TYPE_CHECKING import pytest from twisted.web.http import H2_ENABLED from scrapy.core.engine import ExecutionEngine from scrapy.utils.reactor import set_asyncio_event_loop_policy from scrapy.utils.reactorless import install_reactor_import_hook from tests.keys import generate_keys from tests.mockserver.http import MockServer from tests.mockserver.mitm_proxy import MitmProxy, mitmdump_cmd if TYPE_CHECKING: from collections.abc import Generator def _py_files(folder): return (str(p) for p in Path(folder).rglob("*.py")) collect_ignore = [ # may need extra deps "docs/_ext", # contains scripts to be run by tests/test_crawler_subprocess.py::AsyncCrawlerProcessSubprocess *_py_files("tests/AsyncCrawlerProcess"), # contains scripts to be run by tests/test_crawler_subprocess.py::AsyncCrawlerRunnerSubprocess *_py_files("tests/AsyncCrawlerRunner"), # contains scripts to be run by tests/test_crawler_subprocess.py::CrawlerProcessSubprocess *_py_files("tests/CrawlerProcess"), # contains scripts to be run by tests/test_crawler_subprocess.py::CrawlerRunnerSubprocess *_py_files("tests/CrawlerRunner"), ] base_dir = Path(__file__).parent ignore_file_path = base_dir / "tests" / "ignores.txt" with ignore_file_path.open(encoding="utf-8") as reader: for line in reader: file_path = line.strip() 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", *_py_files("scrapy/core/_http2"), ) ) if find_spec("httpx2") is None and find_spec("httpx") is None: collect_ignore.append("scrapy/core/downloader/handlers/_httpx.py") if find_spec("pytest_codspeed") is None: collect_ignore.append("tests/benchmarks") def pytest_addoption(parser, pluginmanager): if pluginmanager.hasplugin("twisted"): return # add the full choice set so that pytest doesn't complain about invalid choices in some cases parser.addoption( "--reactor", default="none", choices=["asyncio", "default", "none"], ) @pytest.fixture(autouse=True) def fast_engine_heartbeat(monkeypatch: pytest.MonkeyPatch) -> None: """Shorten the interval at which the engine checks for work while idle. Whenever nothing else wakes the engine up, e.g. while a component sleeps, it stays idle until its next heartbeat, so tests that go through that wait pay the whole interval. """ monkeypatch.setattr(ExecutionEngine, "_SLOT_HEARTBEAT_INTERVAL", 0.1) @pytest.fixture(scope="session") def mockserver() -> Generator[MockServer]: with MockServer() as mockserver: yield mockserver @pytest.fixture(scope="session") def _mitm_proxies() -> Generator[dict[str, tuple[MitmProxy, str]]]: proxies: dict[str, tuple[MitmProxy, str]] = {} try: yield proxies finally: for proxy, _url in proxies.values(): proxy.stop() @pytest.fixture # function scope because it modifies os.environ def proxy_server( request: pytest.FixtureRequest, monkeypatch: pytest.MonkeyPatch, _mitm_proxies: dict[str, tuple[MitmProxy, str]], ) -> str: kind: str = request.param if kind not in _mitm_proxies: proxy = MitmProxy(mode="socks5" if kind == "socks5" else None) _mitm_proxies[kind] = (proxy, proxy.start()) _, url = _mitm_proxies[kind] if kind == "https": url = url.replace("http://", "https://") monkeypatch.setenv("http_proxy", url) monkeypatch.setenv("https_proxy", url) return kind @pytest.fixture(scope="session") def reactor_pytest(request) -> str: return request.config.getoption("--reactor") def pytest_configure(config): if config.getoption("--reactor") in {"asyncio", "none"}: # Needed on Windows to switch from proactor to selector, which supports # add_reader/add_writer (required by the Twisted asyncio reactor, and by # tests that register their own readers) and is what Twisted expects. set_asyncio_event_loop_policy() if config.getoption("--reactor") == "none": install_reactor_import_hook() # Test modules made up mostly of tests that spawn a subprocess per test (see # tests/utils/cmdline.py). Collected in file order, they form one contiguous # block. pytest-xdist's initial scheduling (LoadScheduling.schedule() in # xdist/scheduler/load.py) hands each worker one contiguous slice of the # collection, so whichever worker's slice overlaps the block ends up running # most of it while workers with lighter slices drain them and sit idle for # the rest of the run. Spreading these tests evenly through the whole # collection instead means every slice gets a proportional share. _SUBPROCESS_HEAVY_SUFFIXES = ( "tests/test_cmdline/__init__.py", "tests/test_cmdline_crawl_with_pipeline/__init__.py", "tests/test_command_check.py", "tests/test_command_crawl.py", "tests/test_command_fetch.py", "tests/test_command_genspider.py", "tests/test_command_parse.py", "tests/test_command_runspider.py", "tests/test_command_shell.py", "tests/test_command_startproject.py", "tests/test_command_version.py", "tests/test_commands.py", "tests/test_crawler_subprocess.py", ) def _is_subprocess_heavy(item): return item.path.as_posix().endswith(_SUBPROCESS_HEAVY_SUFFIXES) def _interleave_evenly(items, is_heavy): """Merge *light* and *heavy* items so heavy ones land at roughly even intervals throughout the result, instead of clustered together.""" light = [item for item in items if not is_heavy(item)] heavy = [item for item in items if is_heavy(item)] result = [] light_i = heavy_i = 0 while light_i < len(light) or heavy_i < len(heavy): heavy_progress = heavy_i / len(heavy) if heavy else 1.0 light_progress = light_i / len(light) if light else 1.0 if heavy_i < len(heavy) and heavy_progress <= light_progress: result.append(heavy[heavy_i]) heavy_i += 1 else: result.append(light[light_i]) light_i += 1 return result def pytest_collection_modifyitems(items): items[:] = _interleave_evenly(items, _is_subprocess_heavy) for item in items: if item.get_closest_marker("requires_internet"): # Requests to real websites fail every now and then in CI for # reasons unrelated to the code under test. item.add_marker(pytest.mark.flaky(reruns=2, reruns_delay=5)) def pytest_runtest_setup(item): # Skip tests based on reactor markers reactor = item.config.getoption("--reactor") if item.get_closest_marker("requires_reactor") and reactor == "none": pytest.skip('This test is only run when the --reactor value is not "none"') if item.get_closest_marker("only_asyncio") and reactor not in {"asyncio", "none"}: pytest.skip( 'This test is only run when the --reactor value is "asyncio" (default) or "none"' ) if item.get_closest_marker("only_not_asyncio") and reactor in {"asyncio", "none"}: pytest.skip( 'This test is only run when the --reactor value is not "asyncio" (default) or "none"' ) # Skip tests requiring optional dependencies optional_deps = [ "uvloop", "botocore", "boto3", ] for module in optional_deps: if item.get_closest_marker(f"requires_{module}") and find_spec(module) is None: pytest.skip(f"{module} is not installed") if item.get_closest_marker("requires_mitmproxy") and mitmdump_cmd() is None: pytest.skip("mitmdump is not available") # Generate localhost certificate files, needed by some tests (but only once if xdist is used) if "PYTEST_XDIST_WORKER" not in os.environ: generate_keys() scrapy-scrapy-5569de3/docs/ 0000775 0000000 0000000 00000000000 15250510124 0015603 5 ustar 00root root 0000000 0000000 scrapy-scrapy-5569de3/docs/Makefile 0000664 0000000 0000000 00000001171 15250510124 0017243 0 ustar 00root root 0000000 0000000 # Minimal makefile for Sphinx documentation # # You can set these variables from the command line, and also # from the environment for the first two. SPHINXOPTS ?= SPHINXBUILD ?= sphinx-build SOURCEDIR = . BUILDDIR = build # Put it first so that "make" without argument is like "make help". help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) .PHONY: help Makefile # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) scrapy-scrapy-5569de3/docs/_ext/ 0000775 0000000 0000000 00000000000 15250510124 0016542 5 ustar 00root root 0000000 0000000 scrapy-scrapy-5569de3/docs/_ext/scrapydocs.py 0000664 0000000 0000000 00000012450 15250510124 0021270 0 ustar 00root root 0000000 0000000 # pylint: disable=import-error from collections.abc import Sequence from operator import itemgetter from typing import Any, TypedDict from docutils import nodes from docutils.nodes import Element, General, Node, document from docutils.parsers.rst import Directive from sphinx.application import Sphinx from sphinx.util.nodes import make_refnode class SettingData(TypedDict): docname: str setting_name: str refid: str class SettingslistNode(General, Element): pass class SettingsListDirective(Directive): def run(self) -> Sequence[Node]: return [SettingslistNode()] def is_setting_index(node: Node) -> bool: if node.tagname == "index" and node["entries"]: # type: ignore[index,attr-defined] # index entries for setting directives look like: # [('pair', 'SETTING_NAME; setting', 'std:setting-SETTING_NAME', '')] entry_type, info, _ = node["entries"][0][:3] # type: ignore[index] return entry_type == "pair" and info.endswith("; setting") return False def get_setting_name_and_refid(node: Node) -> tuple[str, str]: """Extract setting name from directive index node""" _, info, refid = node["entries"][0][:3] # type: ignore[index] return info.replace("; setting", ""), refid def collect_scrapy_settings_refs(app: Sphinx, doctree: document) -> None: env = app.builder.env if not hasattr(env, "scrapy_all_settings"): emptyList: list[SettingData] = [] env.scrapy_all_settings = emptyList # type: ignore[attr-defined] for node in doctree.findall(is_setting_index): setting_name, refid = get_setting_name_and_refid(node) env.scrapy_all_settings.append( # type: ignore[attr-defined] SettingData( docname=env.docname, setting_name=setting_name, refid=refid, ) ) def make_setting_element( setting_data: SettingData, app: Sphinx, fromdocname: str ) -> Any: 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 item = nodes.list_item() item += p return item def make_setting_markdown_item( setting_data: SettingData, app: Sphinx, fromdocname: str ) -> str: uri = app.builder.get_relative_uri(fromdocname, setting_data["docname"]) if uri.startswith("#"): target = f"#{setting_data['refid']}" else: target = f"{uri}#{setting_data['refid']}" return f"* [{setting_data['setting_name']}]({target})" def _iter_sorted_settings(env: Any, fromdocname: str) -> list[SettingData]: return [ d for d in sorted(env.scrapy_all_settings, key=itemgetter("setting_name")) # type: ignore[attr-defined] if fromdocname != d["docname"] ] def replace_settingslist_nodes( app: Sphinx, doctree: document, fromdocname: str ) -> None: env = app.builder.env for node in doctree.findall(SettingslistNode): settings_list = nodes.bullet_list() settings_list.extend( [ make_setting_element(d, app, fromdocname) for d in _iter_sorted_settings(env, fromdocname) ] ) node.replace_self(settings_list) def visit_settingslist_node_markdown(translator: Any, _node: Node) -> None: builder = translator.builder env = builder.env fromdocname = getattr(builder, "current_doc_name", env.docname) lines = [ make_setting_markdown_item(setting_data, builder.app, fromdocname) for setting_data in _iter_sorted_settings(env, fromdocname) ] if lines: translator.add("\n".join(lines), prefix_eol=2, suffix_eol=2) raise nodes.SkipNode def depart_settingslist_node_markdown(_translator: Any, _node: Node) -> None: return None def source_role( name, rawtext, text: str, lineno, inliner, options=None, content=None ) -> tuple[list[Any], list[Any]]: ref = "https://github.com/scrapy/scrapy/blob/master/" + text node = nodes.reference(rawtext, text, refuri=ref, **options) return [node], [] def commit_role( name, rawtext, text: str, lineno, inliner, options=None, content=None ) -> tuple[list[Any], list[Any]]: ref = "https://github.com/scrapy/scrapy/commit/" + text node = nodes.reference(rawtext, "commit " + text, refuri=ref) return [node], [] def rev_role( name, rawtext, text: str, lineno, inliner, options=None, content=None ) -> tuple[list[Any], list[Any]]: ref = "http://hg.scrapy.org/scrapy/changeset/" + text node = nodes.reference(rawtext, "r" + text, refuri=ref) return [node], [] def setup(app: Sphinx) -> dict[str, Any]: app.add_role("source", source_role) app.add_role("commit", commit_role) app.add_role("rev", rev_role) app.add_node( SettingslistNode, markdown=(visit_settingslist_node_markdown, depart_settingslist_node_markdown), singlemarkdown=( visit_settingslist_node_markdown, depart_settingslist_node_markdown, ), ) app.add_directive("settingslist", SettingsListDirective) app.connect("doctree-read", collect_scrapy_settings_refs) app.connect("doctree-resolved", replace_settingslist_nodes) return {"parallel_read_safe": True} scrapy-scrapy-5569de3/docs/_ext/scrapyfixautodoc.py 0000664 0000000 0000000 00000001207 15250510124 0022503 0 ustar 00root root 0000000 0000000 """ Must be included after 'sphinx.ext.autodoc'. Fixes unwanted 'alias of' behavior. https://github.com/sphinx-doc/sphinx/issues/4422 """ from typing import Any # pylint: disable=import-error from sphinx.application import Sphinx def maybe_skip_member(app: Sphinx, what, name: str, obj, skip: bool, options) -> bool: if not skip: # autodoc was generating the text "alias of" for the following members return name in {"default_item_class", "default_selector_class"} return skip def setup(app: Sphinx) -> dict[str, Any]: app.connect("autodoc-skip-member", maybe_skip_member) return {"parallel_read_safe": True} scrapy-scrapy-5569de3/docs/_static/ 0000775 0000000 0000000 00000000000 15250510124 0017231 5 ustar 00root root 0000000 0000000 scrapy-scrapy-5569de3/docs/_static/custom.css 0000664 0000000 0000000 00000003227 15250510124 0021261 0 ustar 00root root 0000000 0000000 /* Move lists closer to their introducing paragraph */ .rst-content .section ol p, .rst-content .section ul p { margin-bottom: 0px; } .rst-content p + ol, .rst-content p + ul { margin-top: -18px; /* Compensates margin-top: 24px of p */ } .rst-content dl p + ol, .rst-content dl p + ul { margin-top: -6px; /* Compensates margin-top: 12px of p */ } /*override some styles in sphinx-rtd-dark-mode/static/dark_mode_css/general.css*/ .theme-switcher { right: 0.4em !important; top: 0.6em !important; -webkit-box-shadow: 0px 3px 14px 4px rgba(0, 0, 0, 0.30) !important; box-shadow: 0px 3px 14px 4px rgba(0, 0, 0, 0.30) !important; height: 2em !important; width: 2em !important; } /*place the toggle button for dark mode at the bottom right corner on small screens*/ @media (max-width: 768px) { .theme-switcher { right: 0.4em !important; bottom: 2.6em !important; top: auto !important; } } /*persist blue color at the top left used in default rtd theme*/ html[data-theme="dark"] .wy-side-nav-search, html[data-theme="dark"] .wy-nav-top { background-color: #1d577d !important; } /*all the styles below used to present API objects nicely in dark mode*/ html[data-theme="dark"] .sig.sig-object { border-left-color: #3e4446 !important; background-color: #202325 !important } html[data-theme="dark"] .sig-name, html[data-theme="dark"] .sig-prename, html[data-theme="dark"] .property, html[data-theme="dark"] .sig-param, html[data-theme="dark"] .sig-paren, html[data-theme="dark"] .sig-return-icon, html[data-theme="dark"] .sig-return-typehint, html[data-theme="dark"] .optional { color: #e8e6e3 !important } scrapy-scrapy-5569de3/docs/_static/logo.svg 0000664 0000000 0000000 00000016737 15250510124 0020730 0 ustar 00root root 0000000 0000000 scrapy-scrapy-5569de3/docs/_static/selectors-sample1.html 0000664 0000000 0000000 00000001233 15250510124 0023461 0 ustar 00root root 0000000 0000000