Merge remote-tracking branch 'upstream/main' into patch-1
This commit is contained in:
commit
024252ccfc
|
@ -7,3 +7,11 @@
|
|||
|
||||
Error-de_DE=Wenn ist das Nunstück git und Slotermeyer?
|
||||
Ja! Beiherhund das Oder die Virtualenvironment gersput!
|
||||
|
||||
<https://devguide.python.org/pullrequest/#licensing>`__
|
||||
|
||||
class ClassE[T: [str, int]]: ... # Type checker error: illegal expression form
|
||||
class ClassE[T: t1]: ... # Type checker error: literal tuple expression required
|
||||
|
||||
explicitly declared using ``in``, ``out`` and ``inout`` keywords.
|
||||
| | | | | | | inout |
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
adaptee
|
||||
ancilliary
|
||||
ans
|
||||
arithmetics
|
||||
asend
|
||||
|
@ -6,12 +7,15 @@ ba
|
|||
clos
|
||||
complies
|
||||
crate
|
||||
dedented
|
||||
extraversion
|
||||
falsy
|
||||
fo
|
||||
iif
|
||||
nd
|
||||
ned
|
||||
recuse
|
||||
reenable
|
||||
referencable
|
||||
therefor
|
||||
warmup
|
||||
|
|
|
@ -3,3 +3,7 @@
|
|||
*.png binary
|
||||
*.pptx binary
|
||||
*.odp binary
|
||||
|
||||
# Instruct linguist not to ignore the PEPs
|
||||
# https://github.com/github-linguist/linguist/blob/master/docs/overrides.md
|
||||
peps/*.rst text linguist-detectable
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -10,7 +10,7 @@ If your PEP is not Standards Track, remove the corresponding section.
|
|||
## Basic requirements (all PEP Types)
|
||||
|
||||
* [ ] Read and followed [PEP 1](https://peps.python.org/1) & [PEP 12](https://peps.python.org/12)
|
||||
* [ ] File created from the [latest PEP template](https://github.com/python/peps/blob/main/pep-0012/pep-NNNN.rst?plain=1)
|
||||
* [ ] File created from the [latest PEP template](https://github.com/python/peps/blob/main/peps/pep-0012/pep-NNNN.rst?plain=1)
|
||||
* [ ] PEP has next available number, & set in filename (``pep-NNNN.rst``), PR title (``PEP 123: <Title of PEP>``) and ``PEP`` header
|
||||
* [ ] Title clearly, accurately and concisely describes the content in 79 characters or less
|
||||
* [ ] Core dev/PEP editor listed as ``Author`` or ``Sponsor``, and formally confirmed their approval
|
||||
|
|
|
@ -9,4 +9,4 @@ If you're unsure about something, just leave it blank and we'll take a look.
|
|||
* [ ] Any substantial changes since the accepted version approved by the SC/PEP delegate
|
||||
* [ ] Pull request title in appropriate format (``PEP 123: Mark Final``)
|
||||
* [ ] ``Status`` changed to ``Final`` (and ``Python-Version`` is correct)
|
||||
* [ ] Canonical docs/spec linked with a ``canonical-doc`` directive (or ``pypa-spec``, for packaging PEPs)
|
||||
* [ ] Canonical docs/spec linked with a ``canonical-doc`` directive (or ``canonical-pypa-spec``, for packaging PEPs)
|
||||
|
|
|
@ -1,12 +1,18 @@
|
|||
name: Read the Docs PR preview
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types:
|
||||
- opened
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
documentation-links:
|
||||
runs-on: ubuntu-latest
|
||||
|
|
|
@ -1,6 +1,20 @@
|
|||
name: Lint
|
||||
name: Lint PEPs
|
||||
|
||||
on: [push, pull_request, workflow_dispatch]
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
FORCE_COLOR: 1
|
||||
RUFF_FORMAT: github
|
||||
|
||||
jobs:
|
||||
pre-commit:
|
||||
|
@ -8,13 +22,31 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Python 3
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.x'
|
||||
python-version: "3.x"
|
||||
cache: pip
|
||||
|
||||
- name: Run pre-commit hooks
|
||||
uses: pre-commit/action@v3.0.0
|
||||
|
||||
- name: Check spelling
|
||||
uses: pre-commit/action@v3.0.0
|
||||
with:
|
||||
extra_args: --all-files --hook-stage manual codespell || true
|
||||
|
||||
check-peps:
|
||||
name: Run check-peps
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Python 3
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3"
|
||||
|
||||
- name: Run check-peps
|
||||
run: python check-peps.py --detailed
|
||||
|
|
|
@ -1,48 +1,65 @@
|
|||
name: Render PEPs
|
||||
|
||||
on: [push, pull_request, workflow_dispatch]
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
FORCE_COLOR: 1
|
||||
|
||||
jobs:
|
||||
render-peps:
|
||||
name: Render PEPs
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: ["3.x", "3.12-dev"]
|
||||
python-version:
|
||||
- "3.x"
|
||||
- "3.12-dev"
|
||||
|
||||
steps:
|
||||
- name: 🛎️ Checkout
|
||||
uses: actions/checkout@v3
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # fetch all history so that last modified date-times are accurate
|
||||
|
||||
- name: 🐍 Set up Python ${{ matrix.python-version }}
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
cache: pip
|
||||
|
||||
- name: 👷 Install dependencies
|
||||
- name: Update pip
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
|
||||
- name: 🔧 Render PEPs
|
||||
- name: Render PEPs
|
||||
run: make dirhtml JOBS=$(nproc)
|
||||
|
||||
# remove the .doctrees folder when building for deployment as it takes two thirds of disk space
|
||||
- name: 🔥 Clean up files
|
||||
- name: Clean up files
|
||||
run: rm -r build/.doctrees/
|
||||
|
||||
- name: 🚀 Deploy to GitHub pages
|
||||
- name: Deploy to GitHub pages
|
||||
# This allows CI to build branches for testing
|
||||
if: (github.ref == 'refs/heads/main') && (matrix.python-version == '3.x')
|
||||
uses: JamesIves/github-pages-deploy-action@v4
|
||||
with:
|
||||
folder: build # Synchronise with build.py -> build_directory
|
||||
folder: build # Synchronise with Makefile -> OUTPUT_DIR
|
||||
single-commit: true # Delete existing files
|
||||
|
||||
- name: ♻️ Purge CDN cache
|
||||
- name: Purge CDN cache
|
||||
if: github.ref == 'refs/heads/main'
|
||||
run: |
|
||||
curl -H "Accept: application/json" -H "Fastly-Key: $FASTLY_TOKEN" -X POST "https://api.fastly.com/service/$FASTLY_SERVICE_ID/purge_all"
|
||||
|
|
|
@ -13,6 +13,13 @@ on:
|
|||
- "tox.ini"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
FORCE_COLOR: 1
|
||||
|
||||
|
@ -22,12 +29,18 @@ jobs:
|
|||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: ["3.9", "3.10", "3.11", "3.12-dev"]
|
||||
os: [windows-latest, macos-latest, ubuntu-latest]
|
||||
python-version:
|
||||
- "3.9"
|
||||
- "3.10"
|
||||
- "3.11"
|
||||
- "3.12-dev"
|
||||
os:
|
||||
- "windows-latest"
|
||||
- "macos-latest"
|
||||
- "ubuntu-latest"
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
|
@ -40,7 +53,7 @@ jobs:
|
|||
python -m pip install -U wheel
|
||||
python -m pip install -U tox
|
||||
|
||||
- name: Run tests with tox
|
||||
- name: Run tests
|
||||
run: |
|
||||
tox -e py -- -v --cov-report term
|
||||
|
||||
|
|
|
@ -1,18 +1,24 @@
|
|||
coverage.xml
|
||||
pep-0000.txt
|
||||
# PEPs
|
||||
pep-0000.rst
|
||||
pep-????.html
|
||||
peps.rss
|
||||
topic
|
||||
/build
|
||||
|
||||
# Bytecode
|
||||
__pycache__
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.py[co]
|
||||
|
||||
# Editors
|
||||
*~
|
||||
*env
|
||||
.coverage
|
||||
.tox
|
||||
.idea
|
||||
.vscode
|
||||
*.swp
|
||||
/build
|
||||
/package
|
||||
/topic
|
||||
|
||||
# Tests
|
||||
coverage.xml
|
||||
.coverage
|
||||
.tox
|
||||
|
||||
# Virtual environments
|
||||
*env
|
||||
/venv
|
||||
|
|
|
@ -43,7 +43,7 @@ repos:
|
|||
name: "Check YAML"
|
||||
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 22.12.0
|
||||
rev: 23.7.0
|
||||
hooks:
|
||||
- id: black
|
||||
name: "Format with Black"
|
||||
|
@ -52,22 +52,23 @@ repos:
|
|||
- '--target-version=py310'
|
||||
files: 'pep_sphinx_extensions/tests/.*'
|
||||
|
||||
- repo: https://github.com/PyCQA/isort
|
||||
rev: 5.12.0
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.0.287
|
||||
hooks:
|
||||
- id: isort
|
||||
name: "Sort imports with isort"
|
||||
args: ['--profile=black', '--atomic']
|
||||
files: 'pep_sphinx_extensions/tests/.*'
|
||||
- id: ruff
|
||||
name: "Lint with Ruff"
|
||||
args:
|
||||
- '--exit-non-zero-on-fix'
|
||||
files: '^pep_sphinx_extensions/tests/'
|
||||
|
||||
- repo: https://github.com/tox-dev/tox-ini-fmt
|
||||
rev: 0.6.1
|
||||
rev: 1.3.1
|
||||
hooks:
|
||||
- id: tox-ini-fmt
|
||||
name: "Format tox.ini"
|
||||
|
||||
- repo: https://github.com/sphinx-contrib/sphinx-lint
|
||||
rev: v0.6.7
|
||||
rev: v0.6.8
|
||||
hooks:
|
||||
- id: sphinx-lint
|
||||
name: "Sphinx lint"
|
||||
|
@ -79,20 +80,16 @@ repos:
|
|||
hooks:
|
||||
- id: rst-backticks
|
||||
name: "Check RST: No single backticks"
|
||||
files: '^pep-\d\.txt|\.rst$'
|
||||
types: [text]
|
||||
|
||||
- id: rst-inline-touching-normal
|
||||
name: "Check RST: No backticks touching text"
|
||||
files: '^pep-\d+\.txt|\.rst$'
|
||||
types: [text]
|
||||
|
||||
- id: rst-directive-colons
|
||||
name: "Check RST: 2 colons after directives"
|
||||
files: '^pep-\d+\.txt|\.rst$'
|
||||
types: [text]
|
||||
|
||||
# Manual codespell check
|
||||
- repo: https://github.com/codespell-project/codespell
|
||||
rev: v2.2.2
|
||||
rev: v2.2.5
|
||||
hooks:
|
||||
- id: codespell
|
||||
name: "Check for common misspellings in text files"
|
||||
|
@ -101,152 +98,134 @@ repos:
|
|||
# Local checks for PEP headers and more
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: check-no-tabs
|
||||
name: "Check tabs not used in PEPs"
|
||||
language: pygrep
|
||||
entry: '\t'
|
||||
files: '^pep-\d+\.(rst|txt)$'
|
||||
types: [text]
|
||||
# # Hook to run "check-peps.py"
|
||||
# - id: "check-peps"
|
||||
# name: "Check PEPs for metadata and content enforcement"
|
||||
# entry: "python check-peps.py"
|
||||
# language: "system"
|
||||
# files: "^pep-\d{4}\.(rst|txt)$"
|
||||
# require_serial: true
|
||||
|
||||
- id: check-required-headers
|
||||
name: "PEPs must have all required headers"
|
||||
language: pygrep
|
||||
entry: '(?-m:^PEP:(?=[\s\S]*\nTitle:)(?=[\s\S]*\nAuthor:)(?=[\s\S]*\nStatus:)(?=[\s\S]*\nType:)(?=[\s\S]*\nContent-Type:)(?=[\s\S]*\nCreated:))'
|
||||
args: ['--negate', '--multiline']
|
||||
files: '^pep-\d+\.(rst|txt)$'
|
||||
types: [text]
|
||||
files: '^peps/pep-\d+\.rst$'
|
||||
|
||||
- id: check-header-order
|
||||
name: "PEP header order must follow PEP 12"
|
||||
language: pygrep
|
||||
entry: '^PEP:[^\n]+\nTitle:[^\n]+\n(Version:[^\n]+\n)?(Last-Modified:[^\n]+\n)?Author:[^\n]+\n( +\S[^\n]+\n)*(Sponsor:[^\n]+\n)?((PEP|BDFL)-Delegate:[^\n]*\n)?(Discussions-To:[^\n]*\n)?Status:[^\n]+\nType:[^\n]+\n(Topic:[^\n]+\n)?Content-Type:[^\n]+\n(Requires:[^\n]+\n)?Created:[^\n]+\n(Python-Version:[^\n]*\n)?(Post-History:[^\n]*\n( +\S[^\n]*\n)*)?(Replaces:[^\n]+\n)?(Superseded-By:[^\n]+\n)?(Resolution:[^\n]*\n)?\n'
|
||||
args: ['--negate', '--multiline']
|
||||
files: '^pep-\d+\.(rst|txt)$'
|
||||
types: [text]
|
||||
files: '^peps/pep-\d+\.rst$'
|
||||
|
||||
- id: validate-pep-number
|
||||
name: "'PEP' header must be a number 1-9999"
|
||||
language: pygrep
|
||||
entry: '(?-m:^PEP:(?:(?! +(0|[1-9][0-9]{0,3})\n)))'
|
||||
args: ['--multiline']
|
||||
files: '^pep-\d+\.(rst|txt)$'
|
||||
types: [text]
|
||||
files: '^peps/pep-\d+\.rst$'
|
||||
|
||||
- id: validate-title
|
||||
name: "'Title' must be 1-79 characters"
|
||||
language: pygrep
|
||||
entry: '(?<=\n)Title:(?:(?! +\S.{1,78}\n(?=[A-Z])))'
|
||||
args: ['--multiline']
|
||||
files: '^pep-\d+\.(rst|txt)$'
|
||||
exclude: '^pep-(0499)\.(rst|txt)$'
|
||||
types: [text]
|
||||
files: '^peps/pep-\d+\.rst$'
|
||||
exclude: '^peps/pep-(0499)\.rst$'
|
||||
|
||||
- id: validate-author
|
||||
name: "'Author' must be list of 'Name <email@example.com>, ...'"
|
||||
language: pygrep
|
||||
entry: '(?<=\n)Author:(?:(?!((( +|\n {1,8})[^!#$%&()*+,/:;<=>?@\[\\\]\^_`{|}~]+( <[\w!#$%&''*+\-/=?^_{|}~.]+(@| at )[\w\-.]+\.[A-Za-z0-9]+>)?)(,|(?=\n[^ ])))+\n(?=[A-Z])))'
|
||||
args: [--multiline]
|
||||
files: '^pep-\d+\.(rst|txt)$'
|
||||
types: [text]
|
||||
args: ["--multiline"]
|
||||
files: '^peps/pep-\d+\.rst$'
|
||||
|
||||
- id: validate-sponsor
|
||||
name: "'Sponsor' must have format 'Name <email@example.com>'"
|
||||
language: pygrep
|
||||
entry: '^Sponsor:(?: (?! *[^!#$%&()*+,/:;<=>?@\[\\\]\^_`{|}~]+( <[\w!#$%&''*+\-/=?^_{|}~.]+(@| at )[\w\-.]+\.[A-Za-z0-9]+>)?$))'
|
||||
files: '^pep-\d+\.(rst|txt)$'
|
||||
types: [text]
|
||||
files: '^peps/pep-\d+\.rst$'
|
||||
|
||||
- id: validate-delegate
|
||||
name: "'Delegate' must have format 'Name <email@example.com>'"
|
||||
language: pygrep
|
||||
entry: '^(PEP|BDFL)-Delegate: (?:(?! *[^!#$%&()*+,/:;<=>?@\[\\\]\^_`{|}~]+( <[\w!#$%&''*+\-/=?^_{|}~.]+(@| at )[\w\-.]+\.[A-Za-z0-9]+>)?$))'
|
||||
files: '^pep-\d+\.(rst|txt)$'
|
||||
exclude: '^pep-(0451)\.(rst|txt)$'
|
||||
types: [text]
|
||||
files: '^peps/pep-\d+\.rst$'
|
||||
exclude: '^peps/pep-(0451)\.rst$'
|
||||
|
||||
- id: validate-discussions-to
|
||||
name: "'Discussions-To' must be a thread URL"
|
||||
language: pygrep
|
||||
entry: '^Discussions-To: (?:(?!([\w\-]+@(python\.org|googlegroups\.com))|https://((discuss\.python\.org/t/([\w\-]+/)?\d+/?)|(mail\.python\.org/pipermail/[\w\-]+/\d{4}-[A-Za-z]+/[A-Za-z0-9]+\.html)|(mail\.python\.org/archives/list/[\w\-]+@python\.org/thread/[A-Za-z0-9]+/?))$))'
|
||||
files: '^pep-\d+\.(rst|txt)$'
|
||||
types: [text]
|
||||
files: '^peps/pep-\d+\.rst$'
|
||||
|
||||
- id: validate-status
|
||||
name: "'Status' must be a valid PEP status"
|
||||
language: pygrep
|
||||
entry: '^Status:(?:(?! +(Draft|Withdrawn|Rejected|Accepted|Final|Active|Provisional|Deferred|Superseded|April Fool!)$))'
|
||||
files: '^pep-\d+\.(rst|txt)$'
|
||||
types: [text]
|
||||
files: '^peps/pep-\d+\.rst$'
|
||||
|
||||
- id: validate-type
|
||||
name: "'Type' must be a valid PEP type"
|
||||
language: pygrep
|
||||
entry: '^Type:(?:(?! +(Standards Track|Informational|Process)$))'
|
||||
files: '^pep-\d+\.(rst|txt)$'
|
||||
types: [text]
|
||||
files: '^peps/pep-\d+\.rst$'
|
||||
|
||||
- id: validate-topic
|
||||
name: "'Topic' must be for a valid sub-index"
|
||||
language: pygrep
|
||||
entry: '^Topic:(?:(?! +(Governance|Packaging|Typing|Release)(, (Governance|Packaging|Typing|Release))*$))'
|
||||
files: '^pep-\d+\.(rst|txt)$'
|
||||
types: [text]
|
||||
files: '^peps/pep-\d+\.rst$'
|
||||
|
||||
- id: validate-content-type
|
||||
name: "'Content-Type' must be 'text/x-rst'"
|
||||
language: pygrep
|
||||
entry: '^Content-Type:(?:(?! +text/x-rst$))'
|
||||
files: '^pep-\d+\.(rst|txt)$'
|
||||
types: [text]
|
||||
files: '^peps/pep-\d+\.rst$'
|
||||
|
||||
- id: validate-pep-references
|
||||
name: "`Requires`/`Replaces`/`Superseded-By` must be 'NNN' PEP IDs"
|
||||
language: pygrep
|
||||
entry: '^(Requires|Replaces|Superseded-By):(?:(?! *( (0|[1-9][0-9]{0,3})(,|$))+$))'
|
||||
files: '^pep-\d+\.(rst|txt)$'
|
||||
types: [text]
|
||||
files: '^peps/pep-\d+\.rst$'
|
||||
|
||||
- id: validate-created
|
||||
name: "'Created' must be a 'DD-mmm-YYYY' date"
|
||||
language: pygrep
|
||||
entry: '^Created:(?:(?! +([0-2][0-9]|(3[01]))-(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)-(199[0-9]|20[0-9][0-9])$))'
|
||||
files: '^pep-\d+\.(rst|txt)$'
|
||||
types: [text]
|
||||
files: '^peps/pep-\d+\.rst$'
|
||||
|
||||
- id: validate-python-version
|
||||
name: "'Python-Version' must be a 'X.Y[.Z]` version"
|
||||
language: pygrep
|
||||
entry: '^Python-Version:(?:(?! *( [1-9]\.([0-9][0-9]?|x)(\.[1-9][0-9]?)?(,|$))+$))'
|
||||
files: '^pep-\d+\.(rst|txt)$'
|
||||
types: [text]
|
||||
files: '^peps/pep-\d+\.rst$'
|
||||
|
||||
- id: validate-post-history
|
||||
name: "'Post-History' must be '`DD-mmm-YYYY <Thread URL>`__, ...'"
|
||||
language: pygrep
|
||||
entry: '(?<=\n)Post-History:(?:(?! ?\n|((( +|\n {1,14})(([0-2][0-9]|(3[01]))-(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)-(199[0-9]|20[0-9][0-9])|`([0-2][0-9]|(3[01]))-(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)-(199[0-9]|20[0-9][0-9]) <https://((discuss\.python\.org/t/([\w\-]+/)?\d+(?:/\d+/|/?))|(mail\.python\.org/pipermail/[\w\-]+/\d{4}-[A-Za-z]+/[A-Za-z0-9]+\.html)|(mail\.python\.org/archives/list/[\w\-]+@python\.org/thread/[A-Za-z0-9]+/?(#[A-Za-z0-9]+)?))>`__)(,|(?=\n[^ ])))+\n(?=[A-Z\n]))))'
|
||||
args: [--multiline]
|
||||
files: '^pep-\d+\.(rst|txt)$'
|
||||
types: [text]
|
||||
files: '^peps/pep-\d+\.rst$'
|
||||
|
||||
- id: validate-resolution
|
||||
name: "'Resolution' must be a direct thread/message URL"
|
||||
language: pygrep
|
||||
entry: '(?<!\n\n)(?<=\n)Resolution: (?:(?!https://((discuss\.python\.org/t/([\w\-]+/)?\d+(/\d+)?/?)|(mail\.python\.org/pipermail/[\w\-]+/\d{4}-[A-Za-z]+/[A-Za-z0-9]+\.html)|(mail\.python\.org/archives/list/[\w\-]+@python\.org/(message|thread)/[A-Za-z0-9]+/?(#[A-Za-z0-9]+)?))\n))'
|
||||
args: ['--multiline']
|
||||
files: '^pep-\d+\.(rst|txt)$'
|
||||
types: [text]
|
||||
files: '^peps/pep-\d+\.rst$'
|
||||
|
||||
- id: check-direct-pep-links
|
||||
name: "Check that PEPs aren't linked directly"
|
||||
language: pygrep
|
||||
entry: '(dev/peps|peps\.python\.org)/pep-\d+'
|
||||
files: '^pep-\d+\.(rst|txt)$'
|
||||
exclude: '^pep-(0009|0287|0676|0684|8001)\.(rst|txt)$'
|
||||
types: [text]
|
||||
files: '^peps/pep-\d+\.rst$'
|
||||
exclude: '^peps/pep-(0009|0287|0676|0684|8001)\.rst$'
|
||||
|
||||
- id: check-direct-rfc-links
|
||||
name: "Check that RFCs aren't linked directly"
|
||||
language: pygrep
|
||||
entry: '(rfc-editor\.org|ietf\.org)/[\.\-_\?\&\#\w/]*[Rr][Ff][Cc][\-_]?\d+'
|
||||
files: '\.(rst|txt)$'
|
||||
types: [text]
|
||||
types: ['rst']
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
ignore = [
|
||||
"E501", # Line too long
|
||||
]
|
||||
|
||||
select = [
|
||||
"E", # pycodestyle errors
|
||||
"F", # pyflakes
|
||||
"I", # isort
|
||||
"PT", # flake8-pytest-style
|
||||
"W", # pycodestyle warnings
|
||||
]
|
||||
|
||||
show-source = true
|
||||
|
||||
target-version = "py39"
|
|
@ -1,12 +0,0 @@
|
|||
Overridden Name,Surname First,Name Reference
|
||||
The Python core team and community,"The Python core team and community",python-dev
|
||||
Erik De Bonte,"De Bonte, Erik",De Bonte
|
||||
Greg Ewing,"Ewing, Gregory",Ewing
|
||||
Guido van Rossum,"van Rossum, Guido (GvR)",GvR
|
||||
Inada Naoki,"Inada, Naoki",Inada
|
||||
Jim Jewett,"Jewett, Jim J.",Jewett
|
||||
Just van Rossum,"van Rossum, Just (JvR)",JvR
|
||||
Martin v. Löwis,"von Löwis, Martin",von Löwis
|
||||
Nathaniel Smith,"Smith, Nathaniel J.",Smith
|
||||
P.J. Eby,"Eby, Phillip J.",Eby
|
||||
Germán Méndez Bravo,"Méndez Bravo, Germán",Méndez Bravo
|
|
48
Makefile
48
Makefile
|
@ -1,15 +1,23 @@
|
|||
# Builds PEP files to HTML using sphinx
|
||||
|
||||
PYTHON=python3
|
||||
VENVDIR=.venv
|
||||
JOBS=8
|
||||
OUTPUT_DIR=build
|
||||
RENDER_COMMAND=$(VENVDIR)/bin/python3 build.py -j $(JOBS) -o $(OUTPUT_DIR)
|
||||
# You can set these variables from the command line.
|
||||
PYTHON = python3
|
||||
VENVDIR = .venv
|
||||
SPHINXBUILD = PATH=$(VENVDIR)/bin:$$PATH sphinx-build
|
||||
BUILDER = html
|
||||
JOBS = 8
|
||||
SOURCES =
|
||||
# synchronise with render.yml -> deploy step
|
||||
OUTPUT_DIR = build
|
||||
SPHINXERRORHANDLING = -W --keep-going -w sphinx-warnings.txt
|
||||
|
||||
ALLSPHINXOPTS = -b $(BUILDER) -j $(JOBS) \
|
||||
$(SPHINXOPTS) $(SPHINXERRORHANDLING) peps $(OUTPUT_DIR) $(SOURCES)
|
||||
|
||||
## html to render PEPs to "pep-NNNN.html" files
|
||||
.PHONY: html
|
||||
html: venv
|
||||
$(RENDER_COMMAND)
|
||||
$(SPHINXBUILD) $(ALLSPHINXOPTS)
|
||||
|
||||
## htmlview to open the index page built by the html target in your browser
|
||||
.PHONY: htmlview
|
||||
|
@ -18,23 +26,15 @@ htmlview: html
|
|||
|
||||
## dirhtml to render PEPs to "index.html" files within "pep-NNNN" directories
|
||||
.PHONY: dirhtml
|
||||
dirhtml: venv rss
|
||||
$(RENDER_COMMAND) --build-dirs
|
||||
|
||||
## fail-warning to render PEPs to "pep-NNNN.html" files and fail the Sphinx build on any warning
|
||||
.PHONY: fail-warning
|
||||
fail-warning: venv
|
||||
$(RENDER_COMMAND) --fail-on-warning
|
||||
dirhtml: BUILDER = dirhtml
|
||||
dirhtml: venv
|
||||
$(SPHINXBUILD) $(ALLSPHINXOPTS)
|
||||
|
||||
## check-links to check validity of links within PEP sources
|
||||
.PHONY: check-links
|
||||
check-links: BUILDER = linkcheck
|
||||
check-links: venv
|
||||
$(RENDER_COMMAND) --check-links
|
||||
|
||||
## rss to generate the peps.rss file
|
||||
.PHONY: rss
|
||||
rss: venv
|
||||
$(VENVDIR)/bin/python3 generate_rss.py -o $(OUTPUT_DIR)
|
||||
$(SPHINXBUILD) $(ALLSPHINXOPTS)
|
||||
|
||||
## clean to remove the venv and build files
|
||||
.PHONY: clean
|
||||
|
@ -76,16 +76,6 @@ spellcheck: venv
|
|||
$(VENVDIR)/bin/python3 -m pre_commit --version > /dev/null || $(VENVDIR)/bin/python3 -m pip install pre-commit
|
||||
$(VENVDIR)/bin/python3 -m pre_commit run --all-files --hook-stage manual codespell
|
||||
|
||||
## render (deprecated: use 'make html' alias instead)
|
||||
.PHONY: render
|
||||
render: html
|
||||
@echo "\033[0;33mWarning:\033[0;31m 'make render' \033[0;33mis deprecated, use\033[0;32m 'make html' \033[0;33malias instead\033[0m"
|
||||
|
||||
## pages (deprecated: use 'make dirhtml' alias instead)
|
||||
.PHONY: pages
|
||||
pages: dirhtml
|
||||
@echo "\033[0;33mWarning:\033[0;31m 'make pages' \033[0;33mis deprecated, use\033[0;32m 'make dirhtml' \033[0;33malias instead\033[0m"
|
||||
|
||||
.PHONY: help
|
||||
help : Makefile
|
||||
@echo "Please use \`make <target>' where <target> is one of"
|
||||
|
|
|
@ -58,8 +58,8 @@ In summary, run the following in a fresh, activated virtual environment:
|
|||
# Install requirements
|
||||
python -m pip install -U -r requirements.txt
|
||||
|
||||
# Render the PEPs
|
||||
make render
|
||||
# Build the PEPs
|
||||
make html
|
||||
|
||||
# Or, if you don't have 'make':
|
||||
python build.py
|
||||
|
|
38
build.py
38
build.py
|
@ -5,6 +5,7 @@
|
|||
"""Build script for Sphinx documentation"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from sphinx.application import Sphinx
|
||||
|
@ -27,19 +28,10 @@ def create_parser():
|
|||
help='Render PEPs to "index.html" files within "pep-NNNN" directories. '
|
||||
'Cannot be used with "-f" or "-l".')
|
||||
|
||||
# flags / options
|
||||
parser.add_argument("-w", "--fail-on-warning", action="store_true",
|
||||
help="Fail the Sphinx build on any warning.")
|
||||
parser.add_argument("-n", "--nitpicky", action="store_true",
|
||||
help="Run Sphinx in 'nitpicky' mode, "
|
||||
"warning on every missing reference target.")
|
||||
parser.add_argument("-j", "--jobs", type=int, default=1,
|
||||
help="How many parallel jobs to run (if supported). "
|
||||
"Integer, default 1.")
|
||||
parser.add_argument(
|
||||
"-o",
|
||||
"--output-dir",
|
||||
default="build", # synchronise with render.yaml -> deploy step
|
||||
default="build",
|
||||
help="Output directory, relative to root. Default 'build'.",
|
||||
)
|
||||
|
||||
|
@ -61,33 +53,23 @@ def create_index_file(html_root: Path, builder: str) -> None:
|
|||
if __name__ == "__main__":
|
||||
args = create_parser()
|
||||
|
||||
root_directory = Path(".").absolute()
|
||||
source_directory = root_directory
|
||||
root_directory = Path(__file__).resolve().parent
|
||||
source_directory = root_directory / "peps"
|
||||
build_directory = root_directory / args.output_dir
|
||||
doctree_directory = build_directory / ".doctrees"
|
||||
|
||||
# builder configuration
|
||||
if args.builder is not None:
|
||||
sphinx_builder = args.builder
|
||||
else:
|
||||
# default builder
|
||||
sphinx_builder = "html"
|
||||
|
||||
# other configuration
|
||||
config_overrides = {}
|
||||
if args.nitpicky:
|
||||
config_overrides["nitpicky"] = True
|
||||
sphinx_builder = args.builder or "html"
|
||||
|
||||
app = Sphinx(
|
||||
source_directory,
|
||||
confdir=source_directory,
|
||||
outdir=build_directory,
|
||||
doctreedir=doctree_directory,
|
||||
outdir=build_directory / sphinx_builder,
|
||||
doctreedir=build_directory / "doctrees",
|
||||
buildername=sphinx_builder,
|
||||
confoverrides=config_overrides,
|
||||
warningiserror=args.fail_on_warning,
|
||||
parallel=args.jobs,
|
||||
warningiserror=True,
|
||||
parallel=os.cpu_count() or 1,
|
||||
tags=["internal_builder"],
|
||||
keep_going=True,
|
||||
)
|
||||
app.build()
|
||||
|
||||
|
|
|
@ -0,0 +1,605 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
# This file is placed in the public domain or under the
|
||||
# CC0-1.0-Universal license, whichever is more permissive.
|
||||
|
||||
"""check-peps: Check PEPs for common mistakes.
|
||||
|
||||
Usage: check-peps [-d | --detailed] <PEP files...>
|
||||
|
||||
Only the PEPs specified are checked.
|
||||
If none are specified, all PEPs are checked.
|
||||
|
||||
Use "--detailed" to show the contents of lines where errors were found.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime as dt
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
TYPE_CHECKING = False
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Iterable, Iterator, KeysView, Sequence
|
||||
from typing import TypeAlias
|
||||
|
||||
# (line number, warning message)
|
||||
Message: TypeAlias = tuple[int, str]
|
||||
MessageIterator: TypeAlias = Iterator[Message]
|
||||
|
||||
|
||||
# get the directory with the PEP sources
|
||||
ROOT_DIR = Path(__file__).resolve().parent
|
||||
PEP_ROOT = ROOT_DIR / "peps"
|
||||
|
||||
# See PEP 12 for the order
|
||||
# Note we retain "BDFL-Delegate"
|
||||
ALL_HEADERS = (
|
||||
"PEP",
|
||||
"Title",
|
||||
"Version",
|
||||
"Last-Modified",
|
||||
"Author",
|
||||
"Sponsor",
|
||||
"BDFL-Delegate", "PEP-Delegate",
|
||||
"Discussions-To",
|
||||
"Status",
|
||||
"Type",
|
||||
"Topic",
|
||||
"Content-Type",
|
||||
"Requires",
|
||||
"Created",
|
||||
"Python-Version",
|
||||
"Post-History",
|
||||
"Replaces",
|
||||
"Superseded-By",
|
||||
"Resolution",
|
||||
)
|
||||
REQUIRED_HEADERS = frozenset({"PEP", "Title", "Author", "Status", "Type", "Created"})
|
||||
|
||||
# See PEP 1 for the full list
|
||||
ALL_STATUSES = frozenset({
|
||||
"Accepted",
|
||||
"Active",
|
||||
"April Fool!",
|
||||
"Deferred",
|
||||
"Draft",
|
||||
"Final",
|
||||
"Provisional",
|
||||
"Rejected",
|
||||
"Superseded",
|
||||
"Withdrawn",
|
||||
})
|
||||
|
||||
# PEPs that are allowed to link directly to PEPs
|
||||
SKIP_DIRECT_PEP_LINK_CHECK = frozenset({"0009", "0287", "0676", "0684", "8001"})
|
||||
|
||||
DEFAULT_FLAGS = re.ASCII | re.IGNORECASE # Insensitive latin
|
||||
|
||||
# any sequence of letters or '-', followed by a single ':' and a space or end of line
|
||||
HEADER_PATTERN = re.compile(r"^([a-z\-]+):(?: |$)", DEFAULT_FLAGS)
|
||||
# any sequence of unicode letters or legal special characters
|
||||
NAME_PATTERN = re.compile(r"(?:[^\W\d_]|[ ',\-.])+(?: |$)")
|
||||
# any sequence of ASCII letters, digits, or legal special characters
|
||||
EMAIL_LOCAL_PART_PATTERN = re.compile(r"[\w!#$%&'*+\-/=?^{|}~.]+", DEFAULT_FLAGS)
|
||||
|
||||
DISCOURSE_THREAD_PATTERN = re.compile(r"([\w\-]+/)?\d+", DEFAULT_FLAGS)
|
||||
DISCOURSE_POST_PATTERN = re.compile(r"([\w\-]+/)?\d+(/\d+)?", DEFAULT_FLAGS)
|
||||
|
||||
MAILMAN_2_PATTERN = re.compile(r"[\w\-]+/\d{4}-[a-z]+/\d+\.html", DEFAULT_FLAGS)
|
||||
MAILMAN_3_THREAD_PATTERN = re.compile(r"[\w\-]+@python\.org/thread/[a-z0-9]+/?", DEFAULT_FLAGS)
|
||||
MAILMAN_3_MESSAGE_PATTERN = re.compile(r"[\w\-]+@python\.org/message/[a-z0-9]+/?(#[a-z0-9]+)?", DEFAULT_FLAGS)
|
||||
|
||||
# Controlled by the "--detailed" flag
|
||||
DETAILED_ERRORS = False
|
||||
|
||||
|
||||
def check(filenames: Sequence[str] = (), /) -> int:
|
||||
"""The main entry-point."""
|
||||
if filenames:
|
||||
filenames = map(Path, filenames)
|
||||
else:
|
||||
filenames = PEP_ROOT.glob("pep-????.rst")
|
||||
if (count := sum(map(check_file, filenames))) > 0:
|
||||
s = "s" * (count != 1)
|
||||
print(f"check-peps failed: {count} error{s}", file=sys.stderr)
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
def check_file(filename: Path, /) -> int:
|
||||
filename = filename.resolve()
|
||||
try:
|
||||
content = filename.read_text(encoding="utf-8")
|
||||
except FileNotFoundError:
|
||||
return _output_error(filename, [""], [(0, "Could not read PEP!")])
|
||||
else:
|
||||
lines = content.splitlines()
|
||||
return _output_error(filename, lines, check_peps(filename, lines))
|
||||
|
||||
|
||||
def check_peps(filename: Path, lines: Sequence[str], /) -> MessageIterator:
|
||||
yield from check_headers(lines)
|
||||
for line_num, line in enumerate(lines, start=1):
|
||||
if filename.stem.removeprefix("pep-") in SKIP_DIRECT_PEP_LINK_CHECK:
|
||||
continue
|
||||
yield from check_direct_links(line_num, line.lstrip())
|
||||
|
||||
|
||||
def check_headers(lines: Sequence[str], /) -> MessageIterator:
|
||||
yield from _validate_pep_number(next(iter(lines), ""))
|
||||
|
||||
found_headers = {}
|
||||
line_num = 0
|
||||
for line_num, line in enumerate(lines, start=1):
|
||||
if line.strip() == "":
|
||||
headers_end_line_num = line_num
|
||||
break
|
||||
if match := HEADER_PATTERN.match(line):
|
||||
header = match[1]
|
||||
if header in ALL_HEADERS:
|
||||
if header not in found_headers:
|
||||
found_headers[match[1]] = line_num
|
||||
else:
|
||||
yield line_num, f"Must not have duplicate header: {header} "
|
||||
else:
|
||||
yield line_num, f"Must not have invalid header: {header}"
|
||||
else:
|
||||
headers_end_line_num = line_num
|
||||
|
||||
yield from _validate_required_headers(found_headers.keys())
|
||||
|
||||
shifted_line_nums = list(found_headers.values())[1:]
|
||||
for i, (header, line_num) in enumerate(found_headers.items()):
|
||||
start = line_num - 1
|
||||
end = headers_end_line_num - 1
|
||||
if i < len(found_headers) - 1:
|
||||
end = shifted_line_nums[i] - 1
|
||||
remainder = "\n".join(lines[start:end]).removeprefix(f"{header}:")
|
||||
if remainder != "":
|
||||
if remainder[0] not in {" ", "\n"}:
|
||||
yield line_num, f"Headers must have a space after the colon: {header}"
|
||||
remainder = remainder.lstrip()
|
||||
yield from _validate_header(header, line_num, remainder)
|
||||
|
||||
|
||||
def _validate_header(header: str, line_num: int, content: str) -> MessageIterator:
|
||||
if header == "Title":
|
||||
yield from _validate_title(line_num, content)
|
||||
elif header == "Author":
|
||||
yield from _validate_author(line_num, content)
|
||||
elif header == "Sponsor":
|
||||
yield from _validate_sponsor(line_num, content)
|
||||
elif header in {"BDFL-Delegate", "PEP-Delegate"}:
|
||||
yield from _validate_delegate(line_num, content)
|
||||
elif header == "Discussions-To":
|
||||
yield from _validate_discussions_to(line_num, content)
|
||||
elif header == "Status":
|
||||
yield from _validate_status(line_num, content)
|
||||
elif header == "Type":
|
||||
yield from _validate_type(line_num, content)
|
||||
elif header == "Topic":
|
||||
yield from _validate_topic(line_num, content)
|
||||
elif header == "Content-Type":
|
||||
yield from _validate_content_type(line_num, content)
|
||||
elif header in {"Requires", "Replaces", "Superseded-By"}:
|
||||
yield from _validate_pep_references(line_num, content)
|
||||
elif header == "Created":
|
||||
yield from _validate_created(line_num, content)
|
||||
elif header == "Python-Version":
|
||||
yield from _validate_python_version(line_num, content)
|
||||
elif header == "Post-History":
|
||||
yield from _validate_post_history(line_num, content)
|
||||
elif header == "Resolution":
|
||||
yield from _validate_resolution(line_num, content)
|
||||
|
||||
|
||||
def check_direct_links(line_num: int, line: str) -> MessageIterator:
|
||||
"""Check that PEPs and RFCs aren't linked directly"""
|
||||
|
||||
line = line.lower()
|
||||
if "dev/peps/pep-" in line or "peps.python.org/pep-" in line:
|
||||
yield line_num, "Use the :pep:`NNN` role to refer to PEPs"
|
||||
if "rfc-editor.org/rfc/" in line or "ietf.org/doc/html/rfc" in line:
|
||||
yield line_num, "Use the :rfc:`NNN` role to refer to RFCs"
|
||||
|
||||
|
||||
def _output_error(filename: Path, lines: Sequence[str], errors: Iterable[Message]) -> int:
|
||||
relative_filename = filename.relative_to(ROOT_DIR)
|
||||
err_count = 0
|
||||
for line_num, msg in errors:
|
||||
err_count += 1
|
||||
|
||||
print(f"{relative_filename}:{line_num}: {msg}")
|
||||
if not DETAILED_ERRORS:
|
||||
continue
|
||||
|
||||
line = lines[line_num - 1]
|
||||
print(" |")
|
||||
print(f"{line_num: >4} | '{line}'")
|
||||
print(" |")
|
||||
|
||||
return err_count
|
||||
|
||||
|
||||
###########################
|
||||
# PEP Header Validators #
|
||||
###########################
|
||||
|
||||
|
||||
def _validate_required_headers(found_headers: KeysView[str]) -> MessageIterator:
|
||||
"""PEPs must have all required headers, in the PEP 12 order"""
|
||||
|
||||
if missing := REQUIRED_HEADERS.difference(found_headers):
|
||||
for missing_header in sorted(missing, key=ALL_HEADERS.index):
|
||||
yield 1, f"Must have required header: {missing_header}"
|
||||
|
||||
ordered_headers = sorted(found_headers, key=ALL_HEADERS.index)
|
||||
if list(found_headers) != ordered_headers:
|
||||
order_str = ", ".join(ordered_headers)
|
||||
yield 1, "Headers must be in PEP 12 order. Correct order: " + order_str
|
||||
|
||||
|
||||
def _validate_pep_number(line: str) -> MessageIterator:
|
||||
"""'PEP' header must be a number 1-9999"""
|
||||
|
||||
if not line.startswith("PEP: "):
|
||||
yield 1, "PEP must begin with the 'PEP:' header"
|
||||
return
|
||||
|
||||
pep_number = line.removeprefix("PEP: ").lstrip()
|
||||
yield from _pep_num(1, pep_number, "'PEP:' header")
|
||||
|
||||
|
||||
def _validate_title(line_num: int, line: str) -> MessageIterator:
|
||||
"""'Title' must be 1-79 characters"""
|
||||
|
||||
if len(line) == 0:
|
||||
yield line_num, "PEP must have a title"
|
||||
elif len(line) > 79:
|
||||
yield line_num, "PEP title must be less than 80 characters"
|
||||
|
||||
|
||||
def _validate_author(line_num: int, body: str) -> MessageIterator:
|
||||
"""'Author' must be list of 'Name <email@example.com>, …'"""
|
||||
|
||||
lines = body.split("\n")
|
||||
for offset, line in enumerate(lines):
|
||||
if offset >= 1 and line[:9].isspace():
|
||||
# Checks for:
|
||||
# Author: Alice
|
||||
# Bob
|
||||
# ^^^^
|
||||
# Note that len("Author: ") == 8
|
||||
yield line_num + offset, "Author line must not be over-indented"
|
||||
if offset < len(lines) - 1:
|
||||
if not line.endswith(","):
|
||||
yield line_num + offset, "Author continuation lines must end with a comma"
|
||||
for part in line.removesuffix(",").split(", "):
|
||||
yield from _email(line_num + offset, part, "Author")
|
||||
|
||||
|
||||
def _validate_sponsor(line_num: int, line: str) -> MessageIterator:
|
||||
"""'Sponsor' must have format 'Name <email@example.com>'"""
|
||||
|
||||
yield from _email(line_num, line, "Sponsor")
|
||||
|
||||
|
||||
def _validate_delegate(line_num: int, line: str) -> MessageIterator:
|
||||
"""'Delegate' must have format 'Name <email@example.com>'"""
|
||||
|
||||
if line == "":
|
||||
return
|
||||
|
||||
# PEP 451
|
||||
if ", " in line:
|
||||
for part in line.removesuffix(",").split(", "):
|
||||
yield from _email(line_num, part, "Delegate")
|
||||
return
|
||||
|
||||
yield from _email(line_num, line, "Delegate")
|
||||
|
||||
|
||||
def _validate_discussions_to(line_num: int, line: str) -> MessageIterator:
|
||||
"""'Discussions-To' must be a thread URL"""
|
||||
|
||||
yield from _thread(line_num, line, "Discussions-To", discussions_to=True)
|
||||
if line.startswith("https://"):
|
||||
return
|
||||
for suffix in "@python.org", "@googlegroups.com":
|
||||
if line.endswith(suffix):
|
||||
remainder = line.removesuffix(suffix)
|
||||
if re.fullmatch(r"[\w\-]+", remainder) is None:
|
||||
yield line_num, "Discussions-To must be a valid mailing list"
|
||||
return
|
||||
yield line_num, "Discussions-To must be a valid thread URL or mailing list"
|
||||
|
||||
|
||||
def _validate_status(line_num: int, line: str) -> MessageIterator:
|
||||
"""'Status' must be a valid PEP status"""
|
||||
|
||||
if line not in ALL_STATUSES:
|
||||
yield line_num, "Status must be a valid PEP status"
|
||||
|
||||
|
||||
def _validate_type(line_num: int, line: str) -> MessageIterator:
|
||||
"""'Type' must be a valid PEP type"""
|
||||
|
||||
if line not in {"Standards Track", "Informational", "Process"}:
|
||||
yield line_num, "Type must be a valid PEP type"
|
||||
|
||||
|
||||
def _validate_topic(line_num: int, line: str) -> MessageIterator:
|
||||
"""'Topic' must be for a valid sub-index"""
|
||||
|
||||
topics = line.split(", ")
|
||||
unique_topics = set(topics)
|
||||
if len(topics) > len(unique_topics):
|
||||
yield line_num, "Topic must not contain duplicates"
|
||||
|
||||
if unique_topics - {"Governance", "Packaging", "Typing", "Release"}:
|
||||
if not all(map(str.istitle, unique_topics)):
|
||||
yield line_num, "Topic must be properly capitalised (Title Case)"
|
||||
if unique_topics - {"governance", "packaging", "typing", "release"}:
|
||||
yield line_num, "Topic must be for a valid sub-index"
|
||||
if sorted(topics) != topics:
|
||||
yield line_num, "Topic must be sorted lexicographically"
|
||||
|
||||
|
||||
def _validate_content_type(line_num: int, line: str) -> MessageIterator:
|
||||
"""'Content-Type' must be 'text/x-rst'"""
|
||||
|
||||
if line != "text/x-rst":
|
||||
yield line_num, "Content-Type must be 'text/x-rst'"
|
||||
|
||||
|
||||
def _validate_pep_references(line_num: int, line: str) -> MessageIterator:
|
||||
"""`Requires`/`Replaces`/`Superseded-By` must be 'NNN' PEP IDs"""
|
||||
|
||||
line = line.removesuffix(",").rstrip()
|
||||
if line.count(", ") != line.count(","):
|
||||
yield line_num, "PEP references must be separated by comma-spaces (', ')"
|
||||
return
|
||||
|
||||
references = line.split(", ")
|
||||
for reference in references:
|
||||
yield from _pep_num(line_num, reference, "PEP reference")
|
||||
|
||||
|
||||
def _validate_created(line_num: int, line: str) -> MessageIterator:
|
||||
"""'Created' must be a 'DD-mmm-YYYY' date"""
|
||||
|
||||
yield from _date(line_num, line, "Created")
|
||||
|
||||
|
||||
def _validate_python_version(line_num: int, line: str) -> MessageIterator:
|
||||
"""'Python-Version' must be an ``X.Y[.Z]`` version"""
|
||||
|
||||
versions = line.split(", ")
|
||||
for version in versions:
|
||||
if version.count(".") not in {1, 2}:
|
||||
yield line_num, f"Python-Version must have two or three segments: {version}"
|
||||
continue
|
||||
|
||||
try:
|
||||
major, minor, micro = version.split(".", 2)
|
||||
except ValueError:
|
||||
major, minor = version.split(".", 1)
|
||||
micro = ""
|
||||
|
||||
if major not in "123":
|
||||
yield line_num, f"Python-Version major part must be 1, 2, or 3: {version}"
|
||||
if not _is_digits(minor) and minor != "x":
|
||||
yield line_num, f"Python-Version minor part must be numeric: {version}"
|
||||
elif minor != "0" and minor[0] == "0":
|
||||
yield line_num, f"Python-Version minor part must not have leading zeros: {version}"
|
||||
|
||||
if micro == "":
|
||||
return
|
||||
if minor == "x":
|
||||
yield line_num, f"Python-Version micro part must be empty if minor part is 'x': {version}"
|
||||
elif micro[0] == "0":
|
||||
yield line_num, f"Python-Version micro part must not have leading zeros: {version}"
|
||||
elif not _is_digits(micro):
|
||||
yield line_num, f"Python-Version micro part must be numeric: {version}"
|
||||
|
||||
|
||||
def _validate_post_history(line_num: int, body: str) -> MessageIterator:
|
||||
"""'Post-History' must be '`DD-mmm-YYYY <Thread URL>`__, …'"""
|
||||
|
||||
if body == "":
|
||||
return
|
||||
|
||||
for offset, line in enumerate(body.removesuffix(",").split("\n"), start=line_num):
|
||||
for post in line.removesuffix(",").strip().split(", "):
|
||||
if not post.startswith("`") and not post.endswith(">`__"):
|
||||
yield from _date(offset, post, "Post-History")
|
||||
else:
|
||||
post_date, post_url = post[1:-4].split(" <")
|
||||
yield from _date(offset, post_date, "Post-History")
|
||||
yield from _thread(offset, post_url, "Post-History")
|
||||
|
||||
|
||||
def _validate_resolution(line_num: int, line: str) -> MessageIterator:
|
||||
"""'Resolution' must be a direct thread/message URL"""
|
||||
|
||||
yield from _thread(line_num, line, "Resolution", allow_message=True)
|
||||
|
||||
|
||||
########################
|
||||
# Validation Helpers #
|
||||
########################
|
||||
|
||||
def _pep_num(line_num: int, pep_number: str, prefix: str) -> MessageIterator:
|
||||
if pep_number == "":
|
||||
yield line_num, f"{prefix} must not be blank: {pep_number!r}"
|
||||
return
|
||||
if pep_number.startswith("0") and pep_number != "0":
|
||||
yield line_num, f"{prefix} must not contain leading zeros: {pep_number!r}"
|
||||
if not _is_digits(pep_number):
|
||||
yield line_num, f"{prefix} must be numeric: {pep_number!r}"
|
||||
elif not 0 <= int(pep_number) <= 9999:
|
||||
yield line_num, f"{prefix} must be between 0 and 9999: {pep_number!r}"
|
||||
|
||||
|
||||
def _is_digits(string: str) -> bool:
|
||||
"""Match a string of ASCII digits ([0-9]+)."""
|
||||
return string.isascii() and string.isdigit()
|
||||
|
||||
|
||||
def _email(line_num: int, author_email: str, prefix: str) -> MessageIterator:
|
||||
author_email = author_email.strip()
|
||||
|
||||
if author_email.count("<") > 1:
|
||||
msg = f"{prefix} entries must not contain multiple '<': {author_email!r}"
|
||||
yield line_num, msg
|
||||
if author_email.count(">") > 1:
|
||||
msg = f"{prefix} entries must not contain multiple '>': {author_email!r}"
|
||||
yield line_num, msg
|
||||
if author_email.count("@") > 1:
|
||||
msg = f"{prefix} entries must not contain multiple '@': {author_email!r}"
|
||||
yield line_num, msg
|
||||
|
||||
author = author_email.split("<", 1)[0].rstrip()
|
||||
if NAME_PATTERN.fullmatch(author) is None:
|
||||
msg = f"{prefix} entries must begin with a valid 'Name': {author_email!r}"
|
||||
yield line_num, msg
|
||||
return
|
||||
|
||||
email_text = author_email.removeprefix(author)
|
||||
if not email_text:
|
||||
# Does not have the optional email part
|
||||
return
|
||||
|
||||
if not email_text.startswith(" <") or not email_text.endswith(">"):
|
||||
msg = f"{prefix} entries must be formatted as 'Name <email@example.com>': {author_email!r}"
|
||||
yield line_num, msg
|
||||
email_text = email_text.removeprefix(" <").removesuffix(">")
|
||||
|
||||
if "@" in email_text:
|
||||
local, domain = email_text.rsplit("@", 1)
|
||||
elif " at " in email_text:
|
||||
local, domain = email_text.rsplit(" at ", 1)
|
||||
else:
|
||||
yield line_num, f"{prefix} entries must contain a valid email address: {author_email!r}"
|
||||
return
|
||||
if EMAIL_LOCAL_PART_PATTERN.fullmatch(local) is None or _invalid_domain(domain):
|
||||
yield line_num, f"{prefix} entries must contain a valid email address: {author_email!r}"
|
||||
|
||||
|
||||
def _invalid_domain(domain_part: str) -> bool:
|
||||
*labels, root = domain_part.split(".")
|
||||
for label in labels:
|
||||
if not label.replace("-", "").isalnum():
|
||||
return True
|
||||
return not root.isalnum() or not root.isascii()
|
||||
|
||||
|
||||
def _thread(line_num: int, url: str, prefix: str, *, allow_message: bool = False, discussions_to: bool = False) -> MessageIterator:
|
||||
if allow_message and discussions_to:
|
||||
msg = "allow_message and discussions_to cannot both be True"
|
||||
raise ValueError(msg)
|
||||
|
||||
msg = f"{prefix} must be a valid thread URL"
|
||||
|
||||
if not url.startswith("https://"):
|
||||
if not discussions_to:
|
||||
yield line_num, msg
|
||||
return
|
||||
|
||||
if url.startswith("https://discuss.python.org/t/"):
|
||||
remainder = url.removeprefix("https://discuss.python.org/t/").removesuffix("/")
|
||||
|
||||
# Discussions-To links must be the thread itself, not a post
|
||||
if discussions_to:
|
||||
# The equivalent pattern is similar to '([\w\-]+/)?\d+',
|
||||
# but the topic name must contain a non-numeric character
|
||||
|
||||
# We use ``str.rpartition`` as the topic name is optional
|
||||
topic_name, _, topic_id = remainder.rpartition("/")
|
||||
if topic_name == '' and _is_digits(topic_id):
|
||||
return
|
||||
topic_name = topic_name.replace("-", "0").replace("_", "0")
|
||||
# the topic name must not be entirely numeric
|
||||
valid_topic_name = not _is_digits(topic_name) and topic_name.isalnum()
|
||||
if valid_topic_name and _is_digits(topic_id):
|
||||
return
|
||||
else:
|
||||
# The equivalent pattern is similar to '([\w\-]+/)?\d+(/\d+)?',
|
||||
# but the topic name must contain a non-numeric character
|
||||
if remainder.count("/") == 2:
|
||||
# When there are three parts, the URL must be "topic-name/topic-id/post-id".
|
||||
topic_name, topic_id, post_id = remainder.rsplit("/", 2)
|
||||
topic_name = topic_name.replace("-", "0").replace("_", "0")
|
||||
valid_topic_name = not _is_digits(topic_name) and topic_name.isalnum()
|
||||
if valid_topic_name and _is_digits(topic_id) and _is_digits(post_id):
|
||||
# the topic name must not be entirely numeric
|
||||
return
|
||||
elif remainder.count("/") == 1:
|
||||
# When there are only two parts, there's an ambiguity between
|
||||
# "topic-name/topic-id" and "topic-id/post-id".
|
||||
# We disambiguate by checking if the LHS is a valid name and
|
||||
# the RHS is a valid topic ID (for the former),
|
||||
# and then if both the LHS and RHS are valid IDs (for the latter).
|
||||
left, right = remainder.rsplit("/")
|
||||
left = left.replace("-", "0").replace("_", "0")
|
||||
# the topic name must not be entirely numeric
|
||||
left_is_name = not _is_digits(left) and left.isalnum()
|
||||
if left_is_name and _is_digits(right):
|
||||
return
|
||||
elif _is_digits(left) and _is_digits(right):
|
||||
return
|
||||
else:
|
||||
# When there's only one part, it must be a valid topic ID.
|
||||
if _is_digits(remainder):
|
||||
return
|
||||
|
||||
if url.startswith("https://mail.python.org/pipermail/"):
|
||||
remainder = url.removeprefix("https://mail.python.org/pipermail/")
|
||||
if MAILMAN_2_PATTERN.fullmatch(remainder) is not None:
|
||||
return
|
||||
|
||||
if url.startswith("https://mail.python.org/archives/list/"):
|
||||
remainder = url.removeprefix("https://mail.python.org/archives/list/")
|
||||
if allow_message and MAILMAN_3_MESSAGE_PATTERN.fullmatch(remainder) is not None:
|
||||
return
|
||||
if MAILMAN_3_THREAD_PATTERN.fullmatch(remainder) is not None:
|
||||
return
|
||||
|
||||
yield line_num, msg
|
||||
|
||||
|
||||
def _date(line_num: int, date_str: str, prefix: str) -> MessageIterator:
|
||||
try:
|
||||
parsed_date = dt.datetime.strptime(date_str, "%d-%b-%Y")
|
||||
except ValueError:
|
||||
yield line_num, f"{prefix} must be a 'DD-mmm-YYYY' date: {date_str!r}"
|
||||
return
|
||||
else:
|
||||
if date_str[1] == "-": # Date must be zero-padded
|
||||
yield line_num, f"{prefix} must be a 'DD-mmm-YYYY' date: {date_str!r}"
|
||||
return
|
||||
|
||||
if parsed_date.year < 1990:
|
||||
yield line_num, f"{prefix} must not be before Python was invented: {date_str!r}"
|
||||
if parsed_date > (dt.datetime.now() + dt.timedelta(days=14)):
|
||||
yield line_num, f"{prefix} must not be in the future: {date_str!r}"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if {"-h", "--help", "-?"}.intersection(sys.argv[1:]):
|
||||
print(__doc__, file=sys.stderr)
|
||||
raise SystemExit(0)
|
||||
|
||||
files = {}
|
||||
for arg in sys.argv[1:]:
|
||||
if not arg.startswith("-"):
|
||||
files[arg] = None
|
||||
elif arg in {"-d", "--detailed"}:
|
||||
DETAILED_ERRORS = True
|
||||
else:
|
||||
print(f"Unknown option: {arg!r}", file=sys.stderr)
|
||||
raise SystemExit(1)
|
||||
raise SystemExit(check(files))
|
|
@ -1,5 +1,4 @@
|
|||
..
|
||||
Author: Adam Turner
|
||||
:author: Adam Turner
|
||||
|
||||
|
||||
Building PEPs Locally
|
||||
|
@ -10,8 +9,8 @@ This can also be used to check that the PEP is valid reStructuredText before
|
|||
submission to the PEP editors.
|
||||
|
||||
The rest of this document assumes you are working from a local clone of the
|
||||
`PEPs repository <https://github.com/python/peps>`__, with
|
||||
**Python 3.9 or later** installed.
|
||||
`PEPs repository <https://github.com/python/peps>`__,
|
||||
with **Python 3.9 or later** installed.
|
||||
|
||||
|
||||
Render PEPs locally
|
||||
|
@ -43,7 +42,7 @@ Render PEPs locally
|
|||
|
||||
.. code-block:: shell
|
||||
|
||||
make render
|
||||
make html
|
||||
|
||||
If you don't have access to ``make``, run:
|
||||
|
||||
|
@ -51,11 +50,6 @@ Render PEPs locally
|
|||
|
||||
(venv) PS> python build.py
|
||||
|
||||
.. note::
|
||||
|
||||
There may be a series of warnings about unreferenced citations or labels.
|
||||
Whilst these are valid warnings, they do not impact the build process.
|
||||
|
||||
4. Navigate to the ``build`` directory of your PEPs repo to find the HTML pages.
|
||||
PEP 0 provides a formatted index, and may be a useful reference.
|
||||
|
||||
|
@ -87,28 +81,8 @@ Check the validity of links within PEP sources (runs the `Sphinx linkchecker
|
|||
|
||||
.. code-block:: shell
|
||||
|
||||
python build.py --check-links
|
||||
make check-links
|
||||
|
||||
|
||||
Stricter rendering
|
||||
''''''''''''''''''
|
||||
|
||||
Run in `nit-picky <https://www.sphinx-doc.org/en/master/usage/configuration.html#confval-nitpicky>`__
|
||||
mode.
|
||||
This generates warnings for all missing references.
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
python build.py --nitpicky
|
||||
|
||||
Fail the build on any warning.
|
||||
As of January 2022, there are around 250 warnings when building the PEPs.
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
python build.py --fail-on-warning
|
||||
make fail-warning
|
||||
python build.py --check-links
|
||||
make check-links
|
||||
|
||||
|
||||
``build.py`` usage
|
||||
|
@ -118,4 +92,4 @@ For details on the command-line options to the ``build.py`` script, run:
|
|||
|
||||
.. code-block:: shell
|
||||
|
||||
python build.py --help
|
||||
python build.py --help
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
..
|
||||
Author: Adam Turner
|
||||
:author: Adam Turner
|
||||
|
||||
..
|
||||
We can't use :pep:`N` references in this document, as they use links relative
|
||||
to the current file, which doesn't work in a subdirectory like this one.
|
||||
|
||||
|
@ -9,7 +9,7 @@ An Overview of the PEP Rendering System
|
|||
=======================================
|
||||
|
||||
This document provides an overview of the PEP rendering system, as a companion
|
||||
to :doc:`PEP 676 <../pep-0676>`.
|
||||
to `PEP 676 <https://peps.python.org/pep-0676/>`__.
|
||||
|
||||
|
||||
1. Configuration
|
||||
|
@ -17,14 +17,14 @@ to :doc:`PEP 676 <../pep-0676>`.
|
|||
|
||||
Configuration is stored in three files:
|
||||
|
||||
- ``conf.py`` contains the majority of the Sphinx configuration
|
||||
- ``contents.rst`` creates the Sphinx-mandated table of contents directive
|
||||
- ``peps/conf.py`` contains the majority of the Sphinx configuration
|
||||
- ``peps/contents.rst`` contains the compulsory table of contents directive
|
||||
- ``pep_sphinx_extensions/pep_theme/theme.conf`` sets the Pygments themes
|
||||
|
||||
The configuration:
|
||||
|
||||
- registers the custom Sphinx extension
|
||||
- sets both ``.txt`` and ``.rst`` suffixes to be parsed as PEPs
|
||||
- sets the ``.rst`` suffix to be parsed as PEPs
|
||||
- tells Sphinx which source files to use
|
||||
- registers the PEP theme, maths renderer, and template
|
||||
- disables some default settings that are covered in the extension
|
||||
|
@ -35,7 +35,7 @@ The configuration:
|
|||
----------------
|
||||
|
||||
``build.py`` manages the rendering process.
|
||||
Usage is covered in :doc:`build`.
|
||||
Usage is covered in `Building PEPs Locally <./build.rst>`_.
|
||||
|
||||
|
||||
3. Extension
|
||||
|
@ -110,7 +110,8 @@ This overrides the built-in ``:pep:`` role to return the correct URL.
|
|||
3.4.2 ``PEPHeaders`` transform
|
||||
******************************
|
||||
|
||||
PEPs start with a set of :rfc:`2822` headers, per :doc:`PEP 1 <../pep-0001>`.
|
||||
PEPs start with a set of :rfc:`2822` headers,
|
||||
per `PEP 1 <https://peps.python.org/pep-0001/>`__.
|
||||
This transform validates that the required headers are present and of the
|
||||
correct data type, and removes headers not for display.
|
||||
It must run before the ``PEPTitle`` transform.
|
||||
|
@ -122,7 +123,7 @@ It must run before the ``PEPTitle`` transform.
|
|||
We generate the title node from the parsed title in the PEP headers, and make
|
||||
all nodes in the document children of the new title node.
|
||||
This transform must also handle parsing reStructuredText markup within PEP
|
||||
titles, such as :doc:`PEP 604 <../pep-0604>`.
|
||||
titles, such as `PEP 604 <https://peps.python.org/pep-0604/>`__.
|
||||
|
||||
|
||||
3.4.4 ``PEPContents`` transform
|
||||
|
@ -153,7 +154,7 @@ the footer (source link and last modified timestamp).
|
|||
3.5 Prepare for writing
|
||||
''''''''''''''''''''''''
|
||||
|
||||
``pep_html_builder.FileBuilder.prepare_writing`` initialises the bare miniumum
|
||||
``pep_html_builder.FileBuilder.prepare_writing`` initialises the bare minimum
|
||||
of the Docutils writer and the settings for writing documents.
|
||||
This provides a significant speed-up over the base Sphinx implementation, as
|
||||
most of the data automatically initialised was unused.
|
||||
|
@ -216,12 +217,9 @@ parse and validate that metadata.
|
|||
After collecting and validating all the PEP data, the index itself is created in
|
||||
three steps:
|
||||
|
||||
1. Output the header text
|
||||
2. Output the category and numerical indices
|
||||
3. Output the author index
|
||||
|
||||
The ``AUTHOR_OVERRIDES.csv`` file can be used to override an author's name in
|
||||
the PEP 0 output.
|
||||
1. Output the header text
|
||||
2. Output the category and numerical indices
|
||||
3. Output the author index
|
||||
|
||||
We then add the newly created PEP 0 file to two Sphinx variables so that it will
|
||||
be processed as a normal source document.
|
||||
|
|
210
generate_rss.py
210
generate_rss.py
|
@ -1,210 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
# This file is placed in the public domain or under the
|
||||
# CC0-1.0-Universal license, whichever is more permissive.
|
||||
|
||||
import argparse
|
||||
import datetime
|
||||
import email.utils
|
||||
from html import escape
|
||||
from pathlib import Path
|
||||
import re
|
||||
|
||||
import docutils.frontend
|
||||
from docutils import nodes
|
||||
from docutils import utils
|
||||
from docutils.parsers import rst
|
||||
from docutils.parsers.rst import roles
|
||||
|
||||
# get the directory with the PEP sources
|
||||
PEP_ROOT = Path(__file__).parent
|
||||
|
||||
|
||||
def _format_rfc_2822(dt: datetime.datetime) -> str:
|
||||
dt = dt.replace(tzinfo=datetime.timezone.utc)
|
||||
return email.utils.format_datetime(dt, usegmt=True)
|
||||
|
||||
|
||||
line_cache: dict[Path, dict[str, str]] = {}
|
||||
|
||||
# Monkeypatch PEP and RFC reference roles to match Sphinx behaviour
|
||||
EXPLICIT_TITLE_RE = re.compile(r'^(.+?)\s*(?<!\x00)<(.*?)>$', re.DOTALL)
|
||||
|
||||
|
||||
def _pep_reference_role(role, rawtext, text, lineno, inliner,
|
||||
options={}, content=[]):
|
||||
matched = EXPLICIT_TITLE_RE.match(text)
|
||||
if matched:
|
||||
title = utils.unescape(matched.group(1))
|
||||
target = utils.unescape(matched.group(2))
|
||||
else:
|
||||
target = utils.unescape(text)
|
||||
title = "PEP " + utils.unescape(text)
|
||||
pep_str, _, fragment = target.partition("#")
|
||||
try:
|
||||
pepnum = int(pep_str)
|
||||
if pepnum < 0 or pepnum > 9999:
|
||||
raise ValueError
|
||||
except ValueError:
|
||||
msg = inliner.reporter.error(
|
||||
f'PEP number must be a number from 0 to 9999; "{pep_str}" is invalid.',
|
||||
line=lineno)
|
||||
prb = inliner.problematic(rawtext, rawtext, msg)
|
||||
return [prb], [msg]
|
||||
# Base URL mainly used by inliner.pep_reference; so this is correct:
|
||||
ref = (inliner.document.settings.pep_base_url
|
||||
+ inliner.document.settings.pep_file_url_template % pepnum)
|
||||
if fragment:
|
||||
ref += "#" + fragment
|
||||
roles.set_classes(options)
|
||||
return [nodes.reference(rawtext, title, refuri=ref, **options)], []
|
||||
|
||||
|
||||
def _rfc_reference_role(role, rawtext, text, lineno, inliner,
|
||||
options={}, content=[]):
|
||||
matched = EXPLICIT_TITLE_RE.match(text)
|
||||
if matched:
|
||||
title = utils.unescape(matched.group(1))
|
||||
target = utils.unescape(matched.group(2))
|
||||
else:
|
||||
target = utils.unescape(text)
|
||||
title = "RFC " + utils.unescape(text)
|
||||
pep_str, _, fragment = target.partition("#")
|
||||
try:
|
||||
rfcnum = int(pep_str)
|
||||
if rfcnum < 0 or rfcnum > 9999:
|
||||
raise ValueError
|
||||
except ValueError:
|
||||
msg = inliner.reporter.error(
|
||||
f'RFC number must be a number from 0 to 9999; "{pep_str}" is invalid.',
|
||||
line=lineno)
|
||||
prb = inliner.problematic(rawtext, rawtext, msg)
|
||||
return [prb], [msg]
|
||||
ref = (inliner.document.settings.rfc_base_url + inliner.rfc_url % rfcnum)
|
||||
if fragment:
|
||||
ref += "#" + fragment
|
||||
roles.set_classes(options)
|
||||
return [nodes.reference(rawtext, title, refuri=ref, **options)], []
|
||||
|
||||
|
||||
roles.register_canonical_role("pep-reference", _pep_reference_role)
|
||||
roles.register_canonical_role("rfc-reference", _rfc_reference_role)
|
||||
|
||||
|
||||
def first_line_starting_with(full_path: Path, text: str) -> str:
|
||||
# Try and retrieve from cache
|
||||
if full_path in line_cache:
|
||||
return line_cache[full_path].get(text, "")
|
||||
|
||||
# Else read source
|
||||
line_cache[full_path] = path_cache = {}
|
||||
for line in full_path.open(encoding="utf-8"):
|
||||
if line.startswith("Created:"):
|
||||
path_cache["Created:"] = line.removeprefix("Created:").strip()
|
||||
elif line.startswith("Title:"):
|
||||
path_cache["Title:"] = line.removeprefix("Title:").strip()
|
||||
elif line.startswith("Author:"):
|
||||
path_cache["Author:"] = line.removeprefix("Author:").strip()
|
||||
|
||||
# Once all have been found, exit loop
|
||||
if path_cache.keys == {"Created:", "Title:", "Author:"}:
|
||||
break
|
||||
return path_cache.get(text, "")
|
||||
|
||||
|
||||
def pep_creation(full_path: Path) -> datetime.datetime:
|
||||
created_str = first_line_starting_with(full_path, "Created:")
|
||||
if full_path.stem == "pep-0102":
|
||||
# remove additional content on the Created line
|
||||
created_str = created_str.split(" ", 1)[0]
|
||||
return datetime.datetime.strptime(created_str, "%d-%b-%Y")
|
||||
|
||||
|
||||
def parse_rst(full_path: Path) -> nodes.document:
|
||||
text = full_path.read_text(encoding="utf-8")
|
||||
settings = docutils.frontend.get_default_settings(rst.Parser)
|
||||
document = utils.new_document(f'<{full_path}>', settings=settings)
|
||||
rst.Parser(rfc2822=True).parse(text, document)
|
||||
return document
|
||||
|
||||
|
||||
def pep_abstract(full_path: Path) -> str:
|
||||
"""Return the first paragraph of the PEP abstract"""
|
||||
for node in parse_rst(full_path).findall(nodes.section):
|
||||
if node.next_node(nodes.title).astext() == "Abstract":
|
||||
return node.next_node(nodes.paragraph).astext().strip().replace("\n", " ")
|
||||
return ""
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Generate RSS feed")
|
||||
parser.add_argument(
|
||||
"-o",
|
||||
"--output-dir",
|
||||
default="build", # synchronise with render.yaml -> deploy step
|
||||
help="Output directory, relative to root. Default 'build'.",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
# get list of peps with creation time (from "Created:" string in pep source)
|
||||
peps_with_dt = sorted((pep_creation(path), path) for path in PEP_ROOT.glob("pep-????.???"))
|
||||
|
||||
# generate rss items for 10 most recent peps
|
||||
items = []
|
||||
for dt, full_path in peps_with_dt[-10:]:
|
||||
try:
|
||||
pep_num = int(full_path.stem.split("-")[-1])
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
title = first_line_starting_with(full_path, "Title:")
|
||||
author = first_line_starting_with(full_path, "Author:")
|
||||
if "@" in author or " at " in author:
|
||||
parsed_authors = email.utils.getaddresses([author])
|
||||
joined_authors = ", ".join(f"{name} ({email_address})" for name, email_address in parsed_authors)
|
||||
else:
|
||||
joined_authors = author
|
||||
url = f"https://peps.python.org/pep-{pep_num:0>4}/"
|
||||
|
||||
item = f"""\
|
||||
<item>
|
||||
<title>PEP {pep_num}: {escape(title, quote=False)}</title>
|
||||
<link>{escape(url, quote=False)}</link>
|
||||
<description>{escape(pep_abstract(full_path), quote=False)}</description>
|
||||
<author>{escape(joined_authors, quote=False)}</author>
|
||||
<guid isPermaLink="true">{url}</guid>
|
||||
<pubDate>{_format_rfc_2822(dt)}</pubDate>
|
||||
</item>"""
|
||||
items.append(item)
|
||||
|
||||
# The rss envelope
|
||||
desc = """
|
||||
Newest Python Enhancement Proposals (PEPs) - Information on new
|
||||
language features, and some meta-information like release
|
||||
procedure and schedules.
|
||||
"""
|
||||
last_build_date = _format_rfc_2822(datetime.datetime.utcnow())
|
||||
items = "\n".join(reversed(items))
|
||||
output = f"""\
|
||||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<rss xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" version="2.0">
|
||||
<channel>
|
||||
<title>Newest Python PEPs</title>
|
||||
<link>https://peps.python.org/peps.rss</link>
|
||||
<description>{" ".join(desc.split())}</description>
|
||||
<atom:link href="https://peps.python.org/peps.rss" rel="self"/>
|
||||
<docs>https://cyber.harvard.edu/rss/rss.html</docs>
|
||||
<language>en</language>
|
||||
<lastBuildDate>{last_build_date}</lastBuildDate>
|
||||
{items}
|
||||
</channel>
|
||||
</rss>
|
||||
"""
|
||||
|
||||
# output directory for target HTML files
|
||||
out_dir = PEP_ROOT / args.output_dir
|
||||
out_dir.mkdir(exist_ok=True, parents=True)
|
||||
out_dir.joinpath("peps.rss").write_text(output)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
997
pep-0649.rst
997
pep-0649.rst
|
@ -1,997 +0,0 @@
|
|||
PEP: 649
|
||||
Title: Deferred Evaluation Of Annotations Using Descriptors
|
||||
Author: Larry Hastings <larry@hastings.org>
|
||||
Status: Draft
|
||||
Type: Standards Track
|
||||
Topic: Typing
|
||||
Content-Type: text/x-rst
|
||||
Created: 11-Jan-2021
|
||||
Post-History: 11-Jan-2021, 11-Apr-2021
|
||||
|
||||
|
||||
Abstract
|
||||
========
|
||||
|
||||
As of Python 3.9, Python supports two different behaviors
|
||||
for annotations:
|
||||
|
||||
* original or "stock" Python semantics, in which annotations
|
||||
are evaluated at the time they are bound, and
|
||||
* :pep:`563` semantics, currently enabled per-module by
|
||||
``from __future__ import annotations``, in which annotations
|
||||
are converted back into strings and must be reparsed and
|
||||
executed by ``eval()`` to be used.
|
||||
|
||||
Original Python semantics created a circular references problem
|
||||
for static typing analysis. :pep:`563` solved that problem--but
|
||||
its novel semantics introduced new problems, including its
|
||||
restriction that annotations can only reference names at
|
||||
module-level scope.
|
||||
|
||||
This PEP proposes a third way that embodies the best of both
|
||||
previous approaches. It solves the same circular reference
|
||||
problems solved by :pep:`563`, while otherwise preserving Python's
|
||||
original annotation semantics, including allowing annotations
|
||||
to refer to local and class variables.
|
||||
|
||||
In this new approach, the code to generate the annotations
|
||||
dict is written to its own function which computes and returns
|
||||
the annotations dict. Then, ``__annotations__`` is a "data
|
||||
descriptor" which calls this annotation function once and
|
||||
retains the result. This delays the evaluation of annotations
|
||||
expressions until the annotations are examined, at which point
|
||||
all circular references have likely been resolved. And if
|
||||
the annotations are never examined, the function is never
|
||||
called and the annotations are never computed.
|
||||
|
||||
Annotations defined using this PEP's semantics have the same
|
||||
visibility into the symbol table as annotations under "stock"
|
||||
semantics--any name visible to an annotation in Python 3.9
|
||||
is visible to an annotation under this PEP. In addition,
|
||||
annotations under this PEP can refer to names defined *after*
|
||||
the annotation is defined, as long as the name is defined in
|
||||
a scope visible to the annotation. Specifically, when this PEP
|
||||
is active:
|
||||
|
||||
* An annotation can refer to a local variable defined in the
|
||||
current function scope.
|
||||
* An annotation can refer to a local variable defined in an
|
||||
enclosing function scope.
|
||||
* An annotation can refer to a class variable defined in the
|
||||
current class scope.
|
||||
* An annotation can refer to a global variable.
|
||||
|
||||
And in all four of these cases, the variable referenced by
|
||||
the annotation needn't be defined at the time the annotation
|
||||
is defined--it can be defined afterwards. The only restriction
|
||||
is that the name or variable be defined before the annotation
|
||||
is *evaluated.*
|
||||
|
||||
If accepted, these new semantics for annotations would initially
|
||||
be gated behind ``from __future__ import co_annotations``.
|
||||
However, these semantics would eventually be promoted to be
|
||||
Python's default behavior. Thus this PEP would *supersede*
|
||||
:pep:`563`, and :pep:`563`'s behavior would be deprecated and
|
||||
eventually removed.
|
||||
|
||||
Overview
|
||||
========
|
||||
|
||||
.. note:: The code presented in this section is simplified
|
||||
for clarity. The intention is to communicate the high-level
|
||||
concepts involved without getting lost in with the details.
|
||||
The actual details are often quite different. See the
|
||||
Implementation_ section later in this PEP for a much more
|
||||
accurate description of how this PEP works.
|
||||
|
||||
Consider this example code:
|
||||
|
||||
.. code-block::
|
||||
|
||||
def foo(x: int = 3, y: MyType = None) -> float:
|
||||
...
|
||||
class MyType:
|
||||
...
|
||||
foo_y_type = foo.__annotations__['y']
|
||||
|
||||
As we see here, annotations are available at runtime through an
|
||||
``__annotations__`` attribute on functions, classes, and modules.
|
||||
When annotations are specified on one of these objects,
|
||||
``__annotations__`` is a dictionary mapping the names of the
|
||||
fields to the value specified as that field's annotation.
|
||||
|
||||
The default behavior in Python 3.9 is to evaluate the expressions
|
||||
for the annotations, and build the annotations dict, at the time
|
||||
the function, class, or module is bound. At runtime the above
|
||||
code actually works something like this:
|
||||
|
||||
.. code-block::
|
||||
|
||||
annotations = {'x': int, 'y': MyType, 'return': float}
|
||||
def foo(x = 3, y = "abc"):
|
||||
...
|
||||
foo.__annotations__ = annotations
|
||||
class MyType:
|
||||
...
|
||||
foo_y_type = foo.__annotations__['y']
|
||||
|
||||
The crucial detail here is that the values ``int``, ``MyType``,
|
||||
and ``float`` are looked up at the time the function object is
|
||||
bound, and these values are stored in the annotations dict.
|
||||
But this code doesn't run—it throws a ``NameError`` on the first
|
||||
line, because ``MyType`` hasn't been defined yet.
|
||||
|
||||
:pep:`563`'s solution is to decompile the expressions back
|
||||
into strings, and store those *strings* in the annotations dict.
|
||||
The equivalent runtime code would look something like this:
|
||||
|
||||
.. code-block::
|
||||
|
||||
annotations = {'x': 'int', 'y': 'MyType', 'return': 'float'}
|
||||
def foo(x = 3, y = "abc"):
|
||||
...
|
||||
foo.__annotations__ = annotations
|
||||
class MyType:
|
||||
...
|
||||
foo_y_type = foo.__annotations__['y']
|
||||
|
||||
This code now runs successfully. However, ``foo_y_type``
|
||||
is no longer a reference to ``MyType``, it is the *string*
|
||||
``'MyType'``. The code would have to be further modified to
|
||||
call ``eval()`` or ``typing.get_type_hints()`` to convert
|
||||
the string into a useful reference to the actual ``MyType``
|
||||
object.
|
||||
|
||||
This PEP proposes a third approach, delaying the evaluation of
|
||||
the annotations by computing them in their own function. If
|
||||
this PEP was active, the generated code would work something
|
||||
like this:
|
||||
|
||||
.. code-block::
|
||||
|
||||
class function:
|
||||
# __annotations__ on a function object is already a
|
||||
# "data descriptor" in Python, we're just changing what it does
|
||||
@property
|
||||
def __annotations__(self):
|
||||
return self.__co_annotations__()
|
||||
|
||||
# ...
|
||||
|
||||
def foo_annotations_fn():
|
||||
return {'x': int, 'y': MyType, 'return': float}
|
||||
def foo(x = 3, y = "abc"):
|
||||
...
|
||||
foo.__co_annotations__ = foo_annotations_fn
|
||||
class MyType:
|
||||
...
|
||||
foo_y_type = foo.__annotations__['y']
|
||||
|
||||
The important change is that the code constructing the
|
||||
annotations dict now lives in a function—here, called
|
||||
``foo_annotations_fn()``. But this function isn't called
|
||||
until we ask for the value of ``foo.__annotations__``,
|
||||
and we don't do that until *after* the definition of ``MyType``.
|
||||
So this code also runs successfully, and ``foo_y_type`` now
|
||||
has the correct value--the class ``MyType``--even though
|
||||
``MyType`` wasn't defined until *after* the annotation was
|
||||
defined.
|
||||
|
||||
|
||||
Motivation
|
||||
==========
|
||||
|
||||
Python's original semantics for annotations made its use for
|
||||
static type analysis painful due to forward reference problems.
|
||||
This was the main justification for :pep:`563`, and we need not
|
||||
revisit those arguments here.
|
||||
|
||||
However, :pep:`563`'s solution was to decompile code for Python
|
||||
annotations back into strings at compile time, requiring
|
||||
users of annotations to ``eval()`` those strings to restore
|
||||
them to their actual Python values. This has several drawbacks:
|
||||
|
||||
* It requires Python implementations to stringize their
|
||||
annotations. This is surprising behavior—unprecedented
|
||||
for a language-level feature. Also, adding this feature
|
||||
to CPython was complicated, and this complicated code would
|
||||
need to be reimplemented independently by every other Python
|
||||
implementation.
|
||||
* It requires that all annotations be evaluated at module-level
|
||||
scope. Annotations under :pep:`563` can no longer refer to
|
||||
|
||||
* class variables,
|
||||
* local variables in the current function, or
|
||||
* local variables in enclosing functions.
|
||||
|
||||
* It requires a code change every time existing code uses an
|
||||
annotation, to handle converting the stringized
|
||||
annotation back into a useful value.
|
||||
* ``eval()`` is slow.
|
||||
* ``eval()`` isn't always available; it's sometimes removed
|
||||
from Python for space reasons.
|
||||
* In order to evaluate the annotations on a class,
|
||||
it requires obtaining a reference to that class's globals,
|
||||
which :pep:`563` suggests should be done by looking up that class
|
||||
by name in ``sys.modules``—another surprising requirement for
|
||||
a language-level feature.
|
||||
* It adds an ongoing maintenance burden to Python implementations.
|
||||
Every time the language adds a new feature available in expressions,
|
||||
the implementation's stringizing code must be updated in
|
||||
tandem in order to support decompiling it.
|
||||
|
||||
This PEP also solves the forward reference problem outlined in
|
||||
:pep:`563` while avoiding the problems listed above:
|
||||
|
||||
* Python implementations would generate annotations as code
|
||||
objects. This is simpler than stringizing, and is something
|
||||
Python implementations are already quite good at. This means:
|
||||
|
||||
- alternate implementations would need to write less code to
|
||||
implement this feature, and
|
||||
- the implementation would be simpler overall, which should
|
||||
reduce its ongoing maintenance cost.
|
||||
|
||||
* Existing annotations would not need to be changed to only
|
||||
use global scope. Actually, annotations would become much
|
||||
easier to use, as they would now also handle forward
|
||||
references.
|
||||
* Code examining annotations at runtime would no longer need
|
||||
to use ``eval()`` or anything else—it would automatically
|
||||
see the correct values. This is easier, faster, and
|
||||
removes the dependency on ``eval()``.
|
||||
|
||||
|
||||
Backwards Compatibility
|
||||
=======================
|
||||
|
||||
:pep:`563` changed the semantics of annotations. When its semantics
|
||||
are active, annotations must assume they will be evaluated in
|
||||
*module-level* scope. They may no longer refer directly
|
||||
to local variables or class attributes.
|
||||
|
||||
This PEP removes that restriction; annotations may refer to globals,
|
||||
local variables inside functions, local variables defined in enclosing
|
||||
functions, and class members in the current class. In addition,
|
||||
annotations may refer to any of these that haven't been defined yet
|
||||
at the time the annotation is defined, as long as the not-yet-defined
|
||||
name is created normally (in such a way that it is known to the symbol
|
||||
table for the relevant block, or is a global or class variable found
|
||||
using normal name resolution). Thus, this PEP demonstrates *improved*
|
||||
backwards compatibility over :pep:`563`.
|
||||
|
||||
:pep:`563` also requires using ``eval()`` or ``typing.get_type_hints()``
|
||||
to examine annotations. Code updated to work with :pep:`563` that calls
|
||||
``eval()`` directly would have to be updated simply to remove the
|
||||
``eval()`` call. Code using ``typing.get_type_hints()`` would
|
||||
continue to work unchanged, though future use of that function
|
||||
would become optional in most cases.
|
||||
|
||||
Because this PEP makes semantic changes to how annotations are
|
||||
evaluated, this PEP will be initially gated with a per-module
|
||||
``from __future__ import co_annotations`` before it eventually
|
||||
becomes the default behavior.
|
||||
|
||||
Apart from the delay in evaluating values stored in annotations
|
||||
dicts, this PEP preserves nearly all existing behavior of
|
||||
annotations dicts. Specifically:
|
||||
|
||||
* Annotations dicts are mutable, and any changes to them are
|
||||
preserved.
|
||||
* The ``__annotations__`` attribute can be explicitly set,
|
||||
and any value set this way will be preserved.
|
||||
* The ``__annotations__`` attribute can be deleted using
|
||||
the ``del`` statement.
|
||||
|
||||
However, there are two uncommon interactions possible with class
|
||||
and module annotations that work today—both with stock semantics,
|
||||
and with :pep:`563` semantics—that would no longer work when this PEP
|
||||
was active. These two interactions would have to be prohibited.
|
||||
The good news is, neither is common, and neither is considered good
|
||||
practice. In fact, they're rarely seen outside of Python's own
|
||||
regression test suite. They are:
|
||||
|
||||
* *Code that sets annotations on module or class attributes
|
||||
from inside any kind of flow control statement.* It's
|
||||
currently possible to set module and class attributes with
|
||||
annotations inside an ``if`` or ``try`` statement, and it works
|
||||
as one would expect. It's untenable to support this behavior
|
||||
when this PEP is active.
|
||||
* *Code in module or class scope that references or modifies the
|
||||
local* ``__annotations__`` *dict directly.* Currently, when
|
||||
setting annotations on module or class attributes, the generated
|
||||
code simply creates a local ``__annotations__`` dict, then sets
|
||||
mappings in it as needed. It's also possible for user code
|
||||
to directly modify this dict, though this doesn't seem like it's
|
||||
an intentional feature. Although it would be possible to support
|
||||
this after a fashion when this PEP was active, the semantics
|
||||
would likely be surprising and wouldn't make anyone happy.
|
||||
|
||||
Note that these are both also pain points for static type checkers,
|
||||
and are unsupported by those checkers. It seems reasonable to
|
||||
declare that both are at the very least unsupported, and their
|
||||
use results in undefined behavior. It might be worth making a
|
||||
small effort to explicitly prohibit them with compile-time checks.
|
||||
|
||||
In addition, there are a few operators that would no longer be
|
||||
valid for use in annotations, because their side effects would
|
||||
affect the *annotation function* instead of the
|
||||
class/function/module the annotation was nominally defined in:
|
||||
|
||||
* ``:=`` (aka the "walrus operator"),
|
||||
* ``yield`` and ``yield from``, and
|
||||
* ``await``.
|
||||
|
||||
Use of any of these operators in an annotation will result in a
|
||||
compile-time error.
|
||||
|
||||
Since delaying the evaluation of annotations until they are
|
||||
evaluated changes the semantics of the language, it's observable
|
||||
from within the language. Therefore it's possible to write code
|
||||
that behaves differently based on whether annotations are
|
||||
evaluated at binding time or at access time, e.g.
|
||||
|
||||
.. code-block::
|
||||
|
||||
mytype = str
|
||||
def foo(a:mytype): pass
|
||||
mytype = int
|
||||
print(foo.__annotations__['a'])
|
||||
|
||||
This will print ``<class 'str'>`` with stock semantics
|
||||
and ``<class 'int'>`` when this PEP is active. Since
|
||||
this is poor programming style to begin with, it seems
|
||||
acceptable that this PEP changes its behavior.
|
||||
|
||||
Finally, there's a standard idiom that's actually somewhat common
|
||||
when accessing class annotations, and which will become more
|
||||
problematic when this PEP is active: code often accesses class
|
||||
annotations via ``cls.__dict__.get("__annotations__", {})``
|
||||
rather than simply ``cls.__annotations__``. It's due to a flaw
|
||||
in the original design of annotations themselves. This topic
|
||||
will be examined in a separate discussion; the outcome of
|
||||
that discussion will likely guide the future evolution of this
|
||||
PEP.
|
||||
|
||||
|
||||
Mistaken Rejection Of This Approach In November 2017
|
||||
====================================================
|
||||
|
||||
During the early days of discussion around :pep:`563`,
|
||||
using code to delay the evaluation of annotations was
|
||||
briefly discussed, in a November 2017 thread in
|
||||
``comp.lang.python-dev``. At the time the
|
||||
technique was termed an "implicit lambda expression".
|
||||
|
||||
Guido van Rossum—Python's BDFL at the time—replied,
|
||||
asserting that these "implicit lambda expression" wouldn't
|
||||
work, because they'd only be able to resolve symbols at
|
||||
module-level scope:
|
||||
|
||||
IMO the inability of referencing class-level definitions
|
||||
from annotations on methods pretty much kills this idea.
|
||||
|
||||
https://mail.python.org/pipermail/python-dev/2017-November/150109.html
|
||||
|
||||
This led to a short discussion about extending lambda-ized
|
||||
annotations for methods to be able to refer to class-level
|
||||
definitions, by maintaining a reference to the class-level
|
||||
scope. This idea, too, was quickly rejected.
|
||||
|
||||
:pep:`PEP 563 summarizes the above discussion
|
||||
<563#keeping-the-ability-to-use-function-local-state-when-defining-annotations>`
|
||||
|
||||
What's puzzling is :pep:`563`'s own changes to the scoping rules
|
||||
of annotations—it *also* doesn't permit annotations to reference
|
||||
class-level definitions. It's not immediately clear why an
|
||||
inability to reference class-level definitions was enough to
|
||||
reject using "implicit lambda expressions" for annotations,
|
||||
but was acceptable for stringized annotations.
|
||||
|
||||
In retrospect there was probably a pivot during the development
|
||||
of :pep:`563`. It seems that, early on, there was a prevailing
|
||||
assumption that :pep:`563` would support references to class-level
|
||||
definitions. But by the time :pep:`563` was finalized, this
|
||||
assumption had apparently been abandoned. And it looks like
|
||||
"implicit lambda expressions" were never reconsidered in this
|
||||
new light.
|
||||
|
||||
In any case, annotations are still able to refer to class-level
|
||||
definitions under this PEP, rendering the objection moot.
|
||||
|
||||
.. _Implementation:
|
||||
|
||||
Implementation
|
||||
==============
|
||||
|
||||
There's a prototype implementation of this PEP, here:
|
||||
|
||||
https://github.com/larryhastings/co_annotations/
|
||||
|
||||
As of this writing, all features described in this PEP are
|
||||
implemented, and there are some rudimentary tests in the
|
||||
test suite. There are still some broken tests, and the
|
||||
``co_annotations`` repo is many months behind the
|
||||
CPython repo.
|
||||
|
||||
|
||||
from __future__ import co_annotations
|
||||
-------------------------------------
|
||||
|
||||
In the prototype, the semantics presented in this PEP are gated with:
|
||||
|
||||
.. code-block::
|
||||
|
||||
from __future__ import co_annotations
|
||||
|
||||
|
||||
|
||||
__co_annotations__
|
||||
------------------
|
||||
|
||||
Python supports runtime metadata for annotations for three different
|
||||
types: function, classes, and modules. The basic approach to
|
||||
implement this PEP is much the same for all three with only minor
|
||||
variations.
|
||||
|
||||
With this PEP, each of these types adds a new attribute,
|
||||
``__co_annotations__``. ``__co_annotations__`` is a function:
|
||||
it takes no arguments, and must return either ``None`` or a dict
|
||||
(or subclass of dict). It adds the following semantics:
|
||||
|
||||
* ``__co_annotations__`` is always set, and may contain either
|
||||
``None`` or a callable.
|
||||
* ``__co_annotations__`` cannot be deleted.
|
||||
* ``__annotations__`` and ``__co_annotations__`` can't both
|
||||
be set to a useful value simultaneously:
|
||||
|
||||
- If you set ``__annotations__`` to a dict, this also sets
|
||||
``__co_annotations__`` to None.
|
||||
- If you set ``__co_annotations__`` to a callable, this also
|
||||
deletes ``__annotations__``
|
||||
|
||||
Internally, ``__co_annotations__`` is a "data descriptor",
|
||||
where functions are called whenever user code gets, sets,
|
||||
or deletes the attribute. In all three cases, the object
|
||||
has separate internal storage for the current value
|
||||
of the ``__co_annotations__`` attribute.
|
||||
|
||||
``__annotations__`` is also as a data descriptor, with its own
|
||||
separate internal storage for its internal value. The code
|
||||
implementing the "get" for ``__annotations__`` works something
|
||||
like this:
|
||||
|
||||
.. code-block::
|
||||
|
||||
if (the internal value is set)
|
||||
return the internal annotations dict
|
||||
if (__co_annotations__ is not None)
|
||||
call the __co_annotations__ function
|
||||
if the result is a dict:
|
||||
store the result as the internal value
|
||||
set __co_annotations__ to None
|
||||
return the internal value
|
||||
do whatever this object does when there are no annotations
|
||||
|
||||
|
||||
Unbound code objects
|
||||
--------------------
|
||||
|
||||
When Python code defines one of these three objects with
|
||||
annotations, the Python compiler generates a separate code
|
||||
object which builds and returns the appropriate annotations
|
||||
dict. Wherever possible, the "annotation code object" is
|
||||
then stored *unbound* as the internal value of
|
||||
``__co_annotations__``; it is then bound on demand when
|
||||
the user asks for ``__annotations__``.
|
||||
|
||||
This is a useful optimization for both speed and memory
|
||||
consumption. Python processes rarely examine annotations
|
||||
at runtime. Therefore, pre-binding these code objects to
|
||||
function objects would usually be a waste of resources.
|
||||
|
||||
When is this optimization not possible?
|
||||
|
||||
* When an annotation function contains references to
|
||||
free variables, in the current function or in an
|
||||
outer function.
|
||||
* When an annotation function is defined on a method
|
||||
(a function defined inside a class) and the annotations
|
||||
possibly refer directly to class variables.
|
||||
|
||||
Note that user code isn't permitted to directly access these
|
||||
unbound code objects. If the user "gets" the value of
|
||||
``__co_annotations__``, and the internal value of
|
||||
``__co_annotations__`` is an unbound code object,
|
||||
it immediately binds the code object, and the resulting
|
||||
function object is stored as the new value of
|
||||
``__co_annotations__`` and returned.
|
||||
|
||||
(However, these unbound code objects *are* stored in the
|
||||
``.pyc`` file. So a determined user could examine them
|
||||
should that be necessary for some reason.)
|
||||
|
||||
|
||||
|
||||
|
||||
Function Annotations
|
||||
--------------------
|
||||
|
||||
When compiling a function, the CPython bytecode compiler
|
||||
visits the annotations for the function all in one place,
|
||||
starting with ``compiler_visit_annotations()`` in ``compile.c``.
|
||||
If there are any annotations, they create the scope for
|
||||
the annotations function on demand, and
|
||||
``compiler_visit_annotations()`` assembles it.
|
||||
|
||||
The code object is passed in place of the annotations dict
|
||||
for the ``MAKE_FUNCTION`` bytecode instruction.
|
||||
``MAKE_FUNCTION`` supports a new bit in its oparg
|
||||
bitfield, ``0x10``, which tells it to expect a
|
||||
``co_annotations`` code object on the stack.
|
||||
The bitfields for ``annotations`` (``0x04``) and
|
||||
``co_annotations`` (``0x10``) are mutually exclusive.
|
||||
|
||||
When binding an unbound annotation code object, a function will
|
||||
use its own ``__globals__`` as the new function's globals.
|
||||
|
||||
One quirk of Python: you can't actually remove the annotations
|
||||
from a function object. If you delete the ``__annotations__``
|
||||
attribute of a function, then get its ``__annotations__`` member,
|
||||
it will create an empty dict and use that as its
|
||||
``__annotations__``. The implementation of this PEP maintains
|
||||
this quirk for backwards compatibility.
|
||||
|
||||
|
||||
Class Annotations
|
||||
-----------------
|
||||
|
||||
When compiling a class body, the compiler maintains two scopes:
|
||||
one for the normal class body code, and one for annotations.
|
||||
(This is facilitated by four new functions: ``compiler.c``
|
||||
adds ``compiler_push_scope()`` and ``compiler_pop_scope()``,
|
||||
and ``symtable.c`` adds ``symtable_push_scope()`` and
|
||||
``symtable_pop_scope()``.)
|
||||
Once the code generator reaches the end of the class body,
|
||||
but before it generates the bytecode for the class body,
|
||||
it assembles the bytecode for ``__co_annotations__``, then
|
||||
assigns that to ``__co_annotations__`` using ``STORE_NAME``.
|
||||
|
||||
It also sets a new ``__globals__`` attribute. Currently it
|
||||
does this by calling ``globals()`` and storing the result.
|
||||
(Surely there's a more elegant way to find the class's
|
||||
globals--but this was good enough for the prototype.) When
|
||||
binding an unbound annotation code object, a class will use
|
||||
the value of this ``__globals__`` attribute. When the class
|
||||
drops its reference to the unbound code object--either because
|
||||
it has bound it to a function, or because ``__annotations__``
|
||||
has been explicitly set--it also deletes its ``__globals__``
|
||||
attribute.
|
||||
|
||||
As discussed above, examination or modification of
|
||||
``__annotations__`` from within the class body is no
|
||||
longer supported. Also, any flow control (``if`` or ``try`` blocks)
|
||||
around declarations of members with annotations is unsupported.
|
||||
|
||||
If you delete the ``__annotations__`` attribute of a class,
|
||||
then get its ``__annotations__`` member, it will return the
|
||||
annotations dict of the first base class with annotations set.
|
||||
If no base classes have annotations set, it will raise
|
||||
``AttributeError``.
|
||||
|
||||
Although it's an implementation-specific detail, currently
|
||||
classes store the internal value of ``__co_annotations__``
|
||||
in their ``tp_dict`` under the same name.
|
||||
|
||||
|
||||
Module Annotations
|
||||
------------------
|
||||
|
||||
Module annotations work much the same as class annotations.
|
||||
The main difference is, a module uses its own dict as the
|
||||
``__globals__`` when binding the function.
|
||||
|
||||
If you delete the ``__annotations__`` attribute of a class,
|
||||
then get its ``__annotations__`` member, the module will
|
||||
raise ``AttributeError``.
|
||||
|
||||
Annotations With Closures
|
||||
-------------------------
|
||||
|
||||
It's possible to write annotations that refer to
|
||||
free variables, and even free variables that have yet
|
||||
to be defined. For example:
|
||||
|
||||
.. code-block::
|
||||
|
||||
from __future__ import co_annotations
|
||||
|
||||
def outer():
|
||||
def middle():
|
||||
def inner(a:mytype, b:mytype2): pass
|
||||
mytype = str
|
||||
return inner
|
||||
mytype2 = int
|
||||
return middle()
|
||||
|
||||
fn = outer()
|
||||
print(fn.__annotations__)
|
||||
|
||||
At the time ``fn`` is set, ``inner.__co_annotations__()``
|
||||
hasn't been run. So it has to retain a reference to
|
||||
the *future* definitions of ``mytype`` and ``mytype2`` if
|
||||
it is to correctly evaluate its annotations.
|
||||
|
||||
If an annotation function refers to a local variable
|
||||
from the current function scope, or a free variable
|
||||
from an enclosing function scope--if, in CPython, the
|
||||
annotation function code object contains one or more
|
||||
``LOAD_DEREF`` opcodes--then the annotation code object
|
||||
is bound at definition time with references to these
|
||||
variables. ``LOAD_DEREF`` instructions require the annotation
|
||||
function to be bound with special run-time information
|
||||
(in CPython, a ``freevars`` array). Rather than store
|
||||
that separately and use that to later lazy-bind the
|
||||
function object, the current implementation simply
|
||||
early-binds the function object.
|
||||
|
||||
Note that, since the annotation function ``inner.__co_annotations__()``
|
||||
is defined while parsing ``outer()``, from Python's perspective
|
||||
the annotation function is a "nested function". So "local
|
||||
variable inside the 'current' function" and "free variable
|
||||
from an enclosing function" are, from the perspective of
|
||||
the annotation function, the same thing.
|
||||
|
||||
|
||||
Annotations That Refer To Class Variables
|
||||
-----------------------------------------
|
||||
|
||||
It's possible to write annotations that refer to
|
||||
class variables, and even class variables that haven't
|
||||
yet been defined. For example:
|
||||
|
||||
.. code-block::
|
||||
|
||||
from __future__ import co_annotations
|
||||
|
||||
class C:
|
||||
def method(a:mytype): pass
|
||||
mytype = str
|
||||
|
||||
print(C.method.__annotations__)
|
||||
|
||||
Internally, annotation functions are defined as
|
||||
a new type of "block" in CPython's symbol table
|
||||
called an ``AnnotationBlock``. An ``AnnotationBlock``
|
||||
is almost identical to a ``FunctionBlock``. It differs
|
||||
in that it's permitted to see names from an enclosing
|
||||
class scope. (Again: annotation functions are functions,
|
||||
and they're defined *inside* the same scope as
|
||||
the thing they're being defined on. So in the above
|
||||
example, the annotation function for ``C.method()``
|
||||
is defined inside ``C``.)
|
||||
|
||||
If it's possible that an annotation function refers
|
||||
to class variables--if all these conditions are true:
|
||||
|
||||
* The annotation function is being defined inside
|
||||
a class scope.
|
||||
* The generated code for the annotation function
|
||||
has at least one ``LOAD_NAME`` instruction.
|
||||
|
||||
Then the annotation function is bound at the time
|
||||
it's set on the class/function, and this binding
|
||||
includes a reference to the class dict. The class
|
||||
dict is pushed on the stack, and the ``MAKE_FUNCTION``
|
||||
bytecode instruction takes a new second bitfield (0x20)
|
||||
indicating that it should consume that stack argument
|
||||
and store it as ``__locals__`` on the newly created
|
||||
function object.
|
||||
|
||||
Then, at the time the function is executed, the
|
||||
``f_locals`` field of the frame object is set to
|
||||
the function's ``__locals__``, if set. This permits
|
||||
``LOAD_NAME`` opcodes to work normally, which means
|
||||
the code generated for annotation functions is nearly
|
||||
identical to that generated for conventional Python
|
||||
functions.
|
||||
|
||||
|
||||
Interactive REPL Shell
|
||||
----------------------
|
||||
|
||||
Everything works the same inside Python's interactive REPL shell,
|
||||
except for module annotations in the interactive module (``__main__``)
|
||||
itself. Since that module is never "finished", there's no specific
|
||||
point where we can compile the ``__co_annotations__`` function.
|
||||
|
||||
For the sake of simplicity, in this case we forego delayed evaluation.
|
||||
Module-level annotations in the REPL shell will continue to work
|
||||
exactly as they do today, evaluating immediately and setting the
|
||||
result directly inside the ``__annotations__`` dict.
|
||||
|
||||
(It might be possible to support delayed evaluation here.
|
||||
But it gets complicated quickly, and for a nearly-non-existent
|
||||
use case.)
|
||||
|
||||
|
||||
Annotations On Local Variables Inside Functions
|
||||
-----------------------------------------------
|
||||
|
||||
Python supports syntax for local variable annotations inside
|
||||
functions. However, these annotations have no runtime
|
||||
effect--they're discarded at compile-time. Therefore, this
|
||||
PEP doesn't need to do anything to support them, the same
|
||||
as stock semantics and :pep:`563`.
|
||||
|
||||
|
||||
|
||||
Performance Comparison
|
||||
----------------------
|
||||
|
||||
Performance with this PEP should be favorable, when compared with either
|
||||
stock behavior or :pep:`563`. In general, resources are only consumed
|
||||
on demand—"you only pay for what you use".
|
||||
|
||||
There are three scenarios to consider:
|
||||
|
||||
* the runtime cost when annotations aren't defined,
|
||||
* the runtime cost when annotations are defined but *not* referenced, and
|
||||
* the runtime cost when annotations are defined *and* referenced.
|
||||
|
||||
We'll examine each of these scenarios in the context of all three
|
||||
semantics for annotations: stock, :pep:`563`, and this PEP.
|
||||
|
||||
When there are no annotations, all three semantics have the same
|
||||
runtime cost: zero. No annotations dict is created and no code is
|
||||
generated for it. This requires no runtime processor time and
|
||||
consumes no memory.
|
||||
|
||||
When annotations are defined but not referenced, the runtime cost
|
||||
of Python with this PEP should be roughly equal to or slightly better
|
||||
than :pep:`563` semantics, and slightly better than "stock" Python
|
||||
semantics. The specifics depend on the object being annotated:
|
||||
|
||||
* With stock semantics, the annotations dict is always built, and
|
||||
set as an attribute of the object being annotated.
|
||||
* In :pep:`563` semantics, for function objects, a single constant
|
||||
(a tuple) is set as an attribute of the function. For class and
|
||||
module objects, the annotations dict is always built and set as
|
||||
an attribute of the class or module.
|
||||
* With this PEP, a single object is set as an attribute of the
|
||||
object being annotated. Most often, this object is a constant
|
||||
(a code object). In cases where the annotation refers to local
|
||||
variables or class variables, the code object will be bound to
|
||||
a function object, and the function object is set as the attribute
|
||||
of the object being annotated.
|
||||
|
||||
When annotations are both defined and referenced, code using
|
||||
this PEP should be much faster than code using :pep:`563` semantics,
|
||||
and equivalent to or slightly improved over original Python
|
||||
semantics. :pep:`563` semantics requires invoking ``eval()`` for
|
||||
every value inside an annotations dict, which is enormously slow.
|
||||
And, as already mentioned, this PEP generates measurably more
|
||||
efficient bytecode for class and module annotations than stock
|
||||
semantics; for function annotations, this PEP and stock semantics
|
||||
should be roughly equivalent.
|
||||
|
||||
Memory use should also be comparable in all three scenarios across
|
||||
all three semantic contexts. In the first and third scenarios,
|
||||
memory usage should be roughly equivalent in all cases.
|
||||
In the second scenario, when annotations are defined but not
|
||||
referenced, using this PEP's semantics will mean the
|
||||
function/class/module will store one unused code object (possibly
|
||||
bound to an unused function object); with the other two semantics,
|
||||
they'll store one unused dictionary (or constant tuple).
|
||||
|
||||
Bytecode Comparison
|
||||
-------------------
|
||||
|
||||
The bytecode generated for annotations functions with
|
||||
this PEP uses the efficient ``BUILD_CONST_KEY_MAP`` opcode
|
||||
to build the dict for all annotatable objects:
|
||||
functions, classes, and modules.
|
||||
|
||||
Stock semantics also uses ``BUILD_CONST_KEY_MAP`` bytecode
|
||||
for function annotations. :pep:`563` has an even more efficient
|
||||
method for building annotations dicts on functions, leveraging
|
||||
the fact that its annotations dicts only contain strings for
|
||||
both keys and values. At compile-time it constructs a tuple
|
||||
containing pairs of keys and values at compile-time, then
|
||||
at runtime it converts that tuple into a dict on demand.
|
||||
This is a faster technique than either stock semantics
|
||||
or this PEP can employ, because in those two cases
|
||||
annotations dicts can contain Python values of any type.
|
||||
Of course, this performance win is negated if the
|
||||
annotations are examined, due to the overhead of ``eval()``.
|
||||
|
||||
For class and module annotations, both stock semantics
|
||||
and :pep:`563` generate a longer and slightly-less-efficient
|
||||
stanza of bytecode, creating the dict and setting the
|
||||
annotations individually.
|
||||
|
||||
|
||||
For Future Discussion
|
||||
=====================
|
||||
|
||||
Circular Imports
|
||||
----------------
|
||||
|
||||
There is one unfortunately-common scenario where :pep:`563`
|
||||
currently provides a better experience, and it has to do
|
||||
with large code bases, with circular dependencies and
|
||||
imports, that examine their annotations at run-time.
|
||||
|
||||
:pep:`563` permitted defining *and examining* invalid
|
||||
expressions as annotations. Its implementation requires
|
||||
annotations to be legal Python expressions, which it then
|
||||
converts into strings at compile-time. But legal Python
|
||||
expressions may not be computable at runtime, if for
|
||||
example the expression references a name that isn't defined.
|
||||
This is a problem for stringized annotations if they're
|
||||
evaluated, e.g. with ``typing.get_type_hints()``. But
|
||||
any stringized annotation may be examined harmlessly at
|
||||
any time--as long as you don't evaluate it, and only
|
||||
examine it as a string.
|
||||
|
||||
Some large organizations have code bases that unfortunately
|
||||
have circular dependency problems with their annotations--class
|
||||
A has methods annotated with class B, but class B has methods
|
||||
annotated with class A--that can be difficult to resolve.
|
||||
Since :pep:`563` stringizes their annotations, it allows them
|
||||
to leave these circular dependencies in place, and they can
|
||||
sidestep the circular import problem by never importing the
|
||||
module that defines the types used in the annotations. Their
|
||||
annotations can no longer be evaluated, but this appears not
|
||||
to be a concern in practice. They can then examine the
|
||||
stringized form of the annotations at runtime and this seems
|
||||
to be sufficient for their needs.
|
||||
|
||||
This PEP allows for many of the same behaviors.
|
||||
Annotations must be legal Python expressions, which
|
||||
are compiled into a function at compile-time.
|
||||
And if the code never examines an annotation, it won't
|
||||
have any runtime effect, so here too annotations can
|
||||
harmlessly refer to undefined names. (It's exactly
|
||||
like defining a function that refers to undefined
|
||||
names--then never calling that function. Until you
|
||||
call the function, nothing bad will happen.)
|
||||
|
||||
But examining an annotation when this PEP is active
|
||||
means evaluating it, which means the names evaluated
|
||||
in that expression must be defined. An undefined name
|
||||
will throw a ``NameError`` in an annotation function,
|
||||
just as it would with a stringized annotation passed
|
||||
in to ``typing.get_type_hints()``, and just like any
|
||||
other context in Python where an expression is evaluated.
|
||||
|
||||
In discussions we have yet to find a solution to this
|
||||
problem that makes all the participants in the
|
||||
conversation happy. There are various avenues to explore
|
||||
here:
|
||||
|
||||
* One workaround is to continue to stringize one's
|
||||
annotations, either by hand or done automatically
|
||||
by the Python compiler (as it does today with
|
||||
``from __future__ import annotations``). This might
|
||||
mean preserving Python's current stringizing annotations
|
||||
going forward, although leaving it turned off by default,
|
||||
only available by explicit request (though likely with
|
||||
a different mechanism than
|
||||
``from __future__ import annotations``).
|
||||
* Another possible workaround involves importing
|
||||
the circularly-dependent modules separately, then
|
||||
externally adding ("monkey-patching") their dependencies
|
||||
to each other after the modules are loaded. As long
|
||||
as the modules don't examine their annotations until
|
||||
after they are completely loaded, this should work fine
|
||||
and be maintainable with a minimum of effort.
|
||||
* A third and more radical approach would be to change the
|
||||
semantics of annotations so that they don't raise a
|
||||
``NameError`` when an unknown name is evaluated,
|
||||
but instead create some sort of proxy "reference" object.
|
||||
* Of course, even if we do deprecate :pep:`563`, it will be
|
||||
several releases before the functionality is removed,
|
||||
giving us several years in which to research and innovate
|
||||
new solutions for this problem.
|
||||
|
||||
In any case, the participants of the discussion agree that
|
||||
this PEP should still move forward, even as this issue remains
|
||||
currently unresolved [1]_.
|
||||
|
||||
.. [1] https://github.com/larryhastings/co_annotations/issues/1
|
||||
|
||||
|
||||
cls.__globals__ and fn.__locals__
|
||||
---------------------------------
|
||||
|
||||
Is it permissible to add the ``__globals__`` reference to class
|
||||
objects as proposed here? It's not clear why this hasn't already
|
||||
been done; :pep:`563` could have made use of class globals, but instead
|
||||
made do with looking up classes inside ``sys.modules``. Python
|
||||
seems strangely allergic to adding a ``__globals__`` reference to
|
||||
class objects.
|
||||
|
||||
If adding ``__globals__`` to class objects is indeed a bad idea
|
||||
(for reasons I don't know), here are two alternatives as to
|
||||
how classes could get a reference to their globals for the
|
||||
implementation of this PEP:
|
||||
|
||||
* The generate code for a class could bind its annotations code
|
||||
object to a function at the time the class is bound, rather than
|
||||
waiting for ``__annotations__`` to be referenced, making them an
|
||||
exception to the rule (even though "special cases aren't special
|
||||
enough to break the rules"). This would result in a small
|
||||
additional runtime cost when annotations were defined but not
|
||||
referenced on class objects. Honestly I'm more worried about
|
||||
the lack of symmetry in semantics. (But I wouldn't want to
|
||||
pre-bind all annotations code objects, as that would become
|
||||
much more costly for function objects, even as annotations are
|
||||
rarely used at runtime.)
|
||||
* Use the class's ``__module__`` attribute to look up its module
|
||||
by name in ``sys.modules``. This is what :pep:`563` advises.
|
||||
While this is passable for userspace or library code, it seems
|
||||
like a little bit of a code smell for this to be defined semantics
|
||||
baked into the language itself.
|
||||
|
||||
Also, the prototype gets globals for class objects by calling
|
||||
``globals()`` then storing the result. I'm sure there's a much
|
||||
faster way to do this, I just didn't know what it was when I was
|
||||
prototyping. I'm sure we can revise this to something much faster
|
||||
and much more sanitary. I'd prefer to make it completely internal
|
||||
anyway, and not make it visible to the user (via this new
|
||||
__globals__ attribute). There's possibly already a good place to
|
||||
put it anyway--``ht_module``.
|
||||
|
||||
Similarly, this PEP adds one new dunder member to functions,
|
||||
classes, and modules (``__co_annotations__``), and a second new
|
||||
dunder member to functions (``__locals__``). This might be
|
||||
considered excessive.
|
||||
|
||||
|
||||
Bikeshedding the name
|
||||
---------------------
|
||||
|
||||
During most of the development of this PEP, user code actually
|
||||
could see the raw annotation code objects. ``__co_annotations__``
|
||||
could only be set to a code object; functions and other callables
|
||||
weren't permitted. In that context the name ``co_annotations``
|
||||
makes a lot of sense. But with this last-minute pivot where
|
||||
``__co_annotations__`` now presents itself as a callable,
|
||||
perhaps the name of the attribute and the name of the
|
||||
``from __future__ import`` needs a re-think.
|
||||
|
||||
|
||||
Acknowledgements
|
||||
================
|
||||
|
||||
Thanks to Barry Warsaw, Eric V. Smith, Mark Shannon,
|
||||
and Guido van Rossum for feedback and encouragement.
|
||||
Thanks in particular to Mark Shannon for two key
|
||||
suggestions—build the entire annotations dict inside
|
||||
a single code object, and only bind it to a function
|
||||
on demand—that quickly became among the best aspects
|
||||
of this proposal. Also, thanks in particular to Guido
|
||||
van Rossum for suggesting that ``__co_annotations__``
|
||||
functions should duplicate the name visibility rules of
|
||||
annotations under "stock" semantics--this resulted in
|
||||
a sizeable improvement to the second draft. Finally,
|
||||
special thanks to Jelle Zijlstra, who contributed not
|
||||
just feedback--but code!
|
||||
|
||||
|
||||
Copyright
|
||||
=========
|
||||
|
||||
This document is placed in the public domain or under the
|
||||
CC0-1.0-Universal license, whichever is more permissive.
|
||||
|
||||
|
||||
..
|
||||
Local Variables:
|
||||
mode: indented-text
|
||||
indent-tabs-mode: nil
|
||||
sentence-end-double-space: t
|
||||
fill-column: 70
|
||||
coding: utf-8
|
||||
End:
|
|
@ -7,6 +7,7 @@ from typing import TYPE_CHECKING
|
|||
from docutils.writers.html5_polyglot import HTMLTranslator
|
||||
from sphinx import environment
|
||||
|
||||
from pep_sphinx_extensions.generate_rss import create_rss_feed
|
||||
from pep_sphinx_extensions.pep_processor.html import pep_html_builder
|
||||
from pep_sphinx_extensions.pep_processor.html import pep_html_translator
|
||||
from pep_sphinx_extensions.pep_processor.parsing import pep_banner_directive
|
||||
|
@ -27,11 +28,9 @@ def _update_config_for_builder(app: Sphinx) -> None:
|
|||
app.env.document_ids = {} # For PEPReferenceRoleTitleText
|
||||
app.env.settings["builder"] = app.builder.name
|
||||
if app.builder.name == "dirhtml":
|
||||
app.env.settings["pep_url"] = "pep-{:0>4}"
|
||||
app.env.settings["pep_url"] = "pep-{:0>4}/"
|
||||
|
||||
# internal_builder exists if Sphinx is run by build.py
|
||||
if "internal_builder" not in app.tags:
|
||||
app.connect("build-finished", _post_build) # Post-build tasks
|
||||
app.connect("build-finished", _post_build) # Post-build tasks
|
||||
|
||||
|
||||
def _post_build(app: Sphinx, exception: Exception | None) -> None:
|
||||
|
@ -41,7 +40,11 @@ def _post_build(app: Sphinx, exception: Exception | None) -> None:
|
|||
|
||||
if exception is not None:
|
||||
return
|
||||
create_index_file(Path(app.outdir), app.builder.name)
|
||||
|
||||
# internal_builder exists if Sphinx is run by build.py
|
||||
if "internal_builder" not in app.tags:
|
||||
create_index_file(Path(app.outdir), app.builder.name)
|
||||
create_rss_feed(app.doctreedir, app.outdir)
|
||||
|
||||
|
||||
def setup(app: Sphinx) -> dict[str, bool]:
|
||||
|
|
|
@ -0,0 +1,117 @@
|
|||
# This file is placed in the public domain or under the
|
||||
# CC0-1.0-Universal license, whichever is more permissive.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime as dt
|
||||
import pickle
|
||||
from email.utils import format_datetime, getaddresses
|
||||
from html import escape
|
||||
from pathlib import Path
|
||||
|
||||
from docutils import nodes
|
||||
|
||||
RSS_DESCRIPTION = (
|
||||
"Newest Python Enhancement Proposals (PEPs): "
|
||||
"Information on new language features "
|
||||
"and some meta-information like release procedure and schedules."
|
||||
)
|
||||
|
||||
|
||||
def _format_rfc_2822(datetime: dt.datetime) -> str:
|
||||
datetime = datetime.replace(tzinfo=dt.timezone.utc)
|
||||
return format_datetime(datetime, usegmt=True)
|
||||
|
||||
|
||||
document_cache: dict[Path, dict[str, str]] = {}
|
||||
|
||||
|
||||
def get_from_doctree(full_path: Path, text: str) -> str:
|
||||
# Try and retrieve from cache
|
||||
if full_path in document_cache:
|
||||
return document_cache[full_path].get(text, "")
|
||||
|
||||
# Else load doctree
|
||||
document = pickle.loads(full_path.read_bytes())
|
||||
# Store the headers (populated in the PEPHeaders transform)
|
||||
document_cache[full_path] = path_cache = document.get("headers", {})
|
||||
# Store the Abstract
|
||||
path_cache["Abstract"] = pep_abstract(document)
|
||||
# Return the requested key
|
||||
return path_cache.get(text, "")
|
||||
|
||||
|
||||
def pep_creation(full_path: Path) -> dt.datetime:
|
||||
created_str = get_from_doctree(full_path, "Created")
|
||||
try:
|
||||
return dt.datetime.strptime(created_str, "%d-%b-%Y")
|
||||
except ValueError:
|
||||
return dt.datetime.min
|
||||
|
||||
|
||||
def pep_abstract(document: nodes.document) -> str:
|
||||
"""Return the first paragraph of the PEP abstract"""
|
||||
for node in document.findall(nodes.section):
|
||||
title_node = node.next_node(nodes.title)
|
||||
if title_node is None:
|
||||
continue
|
||||
if title_node.astext() == "Abstract":
|
||||
return node.next_node(nodes.paragraph).astext().strip().replace("\n", " ")
|
||||
return ""
|
||||
|
||||
|
||||
def _generate_items(doctree_dir: Path):
|
||||
# get list of peps with creation time (from "Created:" string in pep source)
|
||||
peps_with_dt = sorted((pep_creation(path), path) for path in doctree_dir.glob("pep-????.doctree"))
|
||||
|
||||
# generate rss items for 10 most recent peps (in reverse order)
|
||||
for datetime, full_path in reversed(peps_with_dt[-10:]):
|
||||
try:
|
||||
pep_num = int(get_from_doctree(full_path, "PEP"))
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
title = get_from_doctree(full_path, "Title")
|
||||
url = f"https://peps.python.org/pep-{pep_num:0>4}/"
|
||||
abstract = get_from_doctree(full_path, "Abstract")
|
||||
author = get_from_doctree(full_path, "Author")
|
||||
if "@" in author or " at " in author:
|
||||
parsed_authors = getaddresses([author])
|
||||
joined_authors = ", ".join(f"{name} ({email_address})" for name, email_address in parsed_authors)
|
||||
else:
|
||||
joined_authors = author
|
||||
|
||||
item = f"""\
|
||||
<item>
|
||||
<title>PEP {pep_num}: {escape(title, quote=False)}</title>
|
||||
<link>{escape(url, quote=False)}</link>
|
||||
<description>{escape(abstract, quote=False)}</description>
|
||||
<author>{escape(joined_authors, quote=False)}</author>
|
||||
<guid isPermaLink="true">{url}</guid>
|
||||
<pubDate>{_format_rfc_2822(datetime)}</pubDate>
|
||||
</item>"""
|
||||
yield item
|
||||
|
||||
|
||||
def create_rss_feed(doctree_dir: Path, output_dir: Path):
|
||||
# The rss envelope
|
||||
last_build_date = _format_rfc_2822(dt.datetime.now(dt.timezone.utc))
|
||||
items = "\n".join(_generate_items(Path(doctree_dir)))
|
||||
output = f"""\
|
||||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<rss xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" version="2.0">
|
||||
<channel>
|
||||
<title>Newest Python PEPs</title>
|
||||
<link>https://peps.python.org/peps.rss</link>
|
||||
<description>{RSS_DESCRIPTION}</description>
|
||||
<atom:link href="https://peps.python.org/peps.rss" rel="self"/>
|
||||
<docs>https://cyber.harvard.edu/rss/rss.html</docs>
|
||||
<language>en</language>
|
||||
<lastBuildDate>{last_build_date}</lastBuildDate>
|
||||
{items}
|
||||
</channel>
|
||||
</rss>
|
||||
"""
|
||||
|
||||
# output directory for target HTML files
|
||||
Path(output_dir, "peps.rss").write_text(output, encoding="utf-8")
|
|
@ -1,5 +1,3 @@
|
|||
from pathlib import Path
|
||||
|
||||
from docutils import nodes
|
||||
from docutils.frontend import OptionParser
|
||||
from sphinx.builders.html import StandaloneHTMLBuilder
|
||||
|
@ -22,6 +20,7 @@ class FileBuilder(StandaloneHTMLBuilder):
|
|||
self.docwriter = HTMLWriter(self)
|
||||
_opt_parser = OptionParser([self.docwriter], defaults=self.env.settings, read_config_files=True)
|
||||
self.docsettings = _opt_parser.get_default_values()
|
||||
self._orig_css_files = self._orig_js_files = []
|
||||
|
||||
def get_doc_context(self, docname: str, body: str, _metatags: str) -> dict:
|
||||
"""Collect items for the template context of a page."""
|
||||
|
@ -30,10 +29,6 @@ class FileBuilder(StandaloneHTMLBuilder):
|
|||
except KeyError:
|
||||
title = ""
|
||||
|
||||
# source filename
|
||||
file_is_rst = Path(self.env.srcdir, docname + ".rst").exists()
|
||||
source_name = f"{docname}.rst" if file_is_rst else f"{docname}.txt"
|
||||
|
||||
# local table of contents
|
||||
toc_tree = self.env.tocs[docname].deepcopy()
|
||||
if len(toc_tree) and len(toc_tree[0]) > 1:
|
||||
|
@ -45,7 +40,7 @@ class FileBuilder(StandaloneHTMLBuilder):
|
|||
else:
|
||||
toc = "" # PEPs with no sections -- 9, 210
|
||||
|
||||
return {"title": title, "sourcename": source_name, "toc": toc, "body": body}
|
||||
return {"title": title, "toc": toc, "body": body}
|
||||
|
||||
|
||||
class DirectoryBuilder(FileBuilder):
|
||||
|
|
|
@ -5,7 +5,6 @@ from __future__ import annotations
|
|||
from docutils import nodes
|
||||
from docutils.parsers import rst
|
||||
|
||||
|
||||
PYPA_SPEC_BASE_URL = "https://packaging.python.org/en/latest/specifications/"
|
||||
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import datetime
|
||||
import time
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
|
||||
|
@ -23,7 +23,7 @@ class PEPFooter(transforms.Transform):
|
|||
|
||||
def apply(self) -> None:
|
||||
pep_source_path = Path(self.document["source"])
|
||||
if not pep_source_path.match("pep-*"):
|
||||
if not pep_source_path.match("pep-????.???"):
|
||||
return # not a PEP file, exit early
|
||||
|
||||
# Iterate through sections from the end of the document
|
||||
|
@ -40,7 +40,7 @@ class PEPFooter(transforms.Transform):
|
|||
types.add(type(node))
|
||||
if isinstance(node, nodes.target):
|
||||
to_hoist.append(node)
|
||||
if types <= {nodes.title, nodes.target}:
|
||||
if types <= {nodes.title, nodes.target, nodes.system_message}:
|
||||
section.parent.extend(to_hoist)
|
||||
section.parent.remove(section)
|
||||
|
||||
|
@ -54,7 +54,7 @@ class PEPFooter(transforms.Transform):
|
|||
|
||||
def _add_source_link(pep_source_path: Path) -> nodes.paragraph:
|
||||
"""Add link to source text on VCS (GitHub)"""
|
||||
source_link = f"https://github.com/python/peps/blob/main/{pep_source_path.name}"
|
||||
source_link = f"https://github.com/python/peps/blob/main/peps/{pep_source_path.name}"
|
||||
link_node = nodes.reference("", source_link, refuri=source_link)
|
||||
return nodes.paragraph("", "Source: ", link_node)
|
||||
|
||||
|
@ -62,11 +62,10 @@ def _add_source_link(pep_source_path: Path) -> nodes.paragraph:
|
|||
def _add_commit_history_info(pep_source_path: Path) -> nodes.paragraph:
|
||||
"""Use local git history to find last modified date."""
|
||||
try:
|
||||
since_epoch = LAST_MODIFIED_TIMES[pep_source_path.name]
|
||||
iso_time = _LAST_MODIFIED_TIMES[pep_source_path.stem]
|
||||
except KeyError:
|
||||
return nodes.paragraph()
|
||||
|
||||
iso_time = datetime.datetime.utcfromtimestamp(since_epoch).isoformat(sep=" ")
|
||||
commit_link = f"https://github.com/python/peps/commits/main/{pep_source_path.name}"
|
||||
link_node = nodes.reference("", f"{iso_time} GMT", refuri=commit_link)
|
||||
return nodes.paragraph("", "Last modified: ", link_node)
|
||||
|
@ -74,29 +73,36 @@ def _add_commit_history_info(pep_source_path: Path) -> nodes.paragraph:
|
|||
|
||||
def _get_last_modified_timestamps():
|
||||
# get timestamps and changed files from all commits (without paging results)
|
||||
args = ["git", "--no-pager", "log", "--format=#%at", "--name-only"]
|
||||
with subprocess.Popen(args, stdout=subprocess.PIPE) as process:
|
||||
all_modified = process.stdout.read().decode("utf-8")
|
||||
process.stdout.close()
|
||||
if process.wait(): # non-zero return code
|
||||
return {}
|
||||
args = ("git", "--no-pager", "log", "--format=#%at", "--name-only")
|
||||
ret = subprocess.run(args, stdout=subprocess.PIPE, text=True, encoding="utf-8")
|
||||
if ret.returncode: # non-zero return code
|
||||
return {}
|
||||
all_modified = ret.stdout
|
||||
|
||||
# remove "peps/" prefix from file names
|
||||
all_modified = all_modified.replace("\npeps/", "\n")
|
||||
|
||||
# set up the dictionary with the *current* files
|
||||
last_modified = {path.name: 0 for path in Path().glob("pep-*") if path.suffix in {".txt", ".rst"}}
|
||||
peps_dir = Path(__file__, "..", "..", "..", "..", "peps").resolve()
|
||||
last_modified = {path.stem: "" for path in peps_dir.glob("pep-????.rst")}
|
||||
|
||||
# iterate through newest to oldest, updating per file timestamps
|
||||
change_sets = all_modified.removeprefix("#").split("#")
|
||||
for change_set in change_sets:
|
||||
timestamp, files = change_set.split("\n", 1)
|
||||
for file in files.strip().split("\n"):
|
||||
if file.startswith("pep-") and file[-3:] in {"txt", "rst"}:
|
||||
if last_modified.get(file) == 0:
|
||||
try:
|
||||
last_modified[file] = float(timestamp)
|
||||
except ValueError:
|
||||
pass # if float conversion fails
|
||||
if not file.startswith("pep-") or not file.endswith((".rst", ".txt")):
|
||||
continue # not a PEP
|
||||
file = file[:-4]
|
||||
if last_modified.get(file) != "":
|
||||
continue # most recent modified date already found
|
||||
try:
|
||||
y, m, d, hh, mm, ss, *_ = time.gmtime(float(timestamp))
|
||||
except ValueError:
|
||||
continue # if float conversion fails
|
||||
last_modified[file] = f"{y:04}-{m:02}-{d:02} {hh:02}:{mm:02}:{ss:02}"
|
||||
|
||||
return last_modified
|
||||
|
||||
|
||||
LAST_MODIFIED_TIMES = _get_last_modified_timestamps()
|
||||
_LAST_MODIFIED_TIMES = _get_last_modified_timestamps()
|
||||
|
|
|
@ -72,11 +72,11 @@ class PEPHeaders(transforms.Transform):
|
|||
raise PEPParsingError("Document does not contain an RFC-2822 'PEP' header!")
|
||||
|
||||
# Extract PEP number
|
||||
value = pep_field[1].astext()
|
||||
pep_num_str = pep_field[1].astext()
|
||||
try:
|
||||
pep_num = int(value)
|
||||
pep_num = int(pep_num_str)
|
||||
except ValueError:
|
||||
raise PEPParsingError(f"'PEP' header must contain an integer. '{value}' is invalid!")
|
||||
raise PEPParsingError(f"PEP header must contain an integer. '{pep_num_str}' is invalid!")
|
||||
|
||||
# Special processing for PEP 0.
|
||||
if pep_num == 0:
|
||||
|
@ -89,7 +89,11 @@ class PEPHeaders(transforms.Transform):
|
|||
raise PEPParsingError("No title!")
|
||||
|
||||
fields_to_remove = []
|
||||
self.document["headers"] = headers = {}
|
||||
for field in header:
|
||||
row_attributes = {sub.tagname: sub.rawsource for sub in field}
|
||||
headers[row_attributes["field_name"]] = row_attributes["field_body"]
|
||||
|
||||
name = field[0].astext().lower()
|
||||
body = field[1]
|
||||
if len(body) == 0:
|
||||
|
|
|
@ -2,25 +2,10 @@
|
|||
|
||||
/* Media Queries */
|
||||
|
||||
/* Further reduce width of fixed elements for smallest screens */
|
||||
@media (max-width: 32em) {
|
||||
section#pep-page-section {
|
||||
padding: 0.25rem;
|
||||
}
|
||||
dl.footnote > dt,
|
||||
dl.footnote > dd {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
pre {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Reduce padding & margins for smaller screens */
|
||||
@media (max-width: 40em) {
|
||||
section#pep-page-section {
|
||||
padding: 0.5rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
section#pep-page-section > header > h1 {
|
||||
padding-right: 0;
|
||||
|
|
|
@ -61,11 +61,9 @@ img.invert-in-dark-mode {
|
|||
:root {color-scheme: light dark}
|
||||
html {
|
||||
overflow-y: scroll;
|
||||
margin: 0;
|
||||
line-height: 1.4;
|
||||
font-weight: normal;
|
||||
line-height: 1.5;
|
||||
font-size: 1rem;
|
||||
font-family: "Source Sans Pro", Arial, sans-serif;
|
||||
font-family: -apple-system, BlinkMacSystemFont, avenir next, avenir, segoe ui, helvetica neue, helvetica, Cantarell, Ubuntu, roboto, noto, arial, sans-serif;
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
|
@ -76,40 +74,27 @@ section#pep-page-section {
|
|||
padding: 0.25rem;
|
||||
}
|
||||
|
||||
/* Reduce margin sizes for body text */
|
||||
p {margin: .5rem 0}
|
||||
|
||||
/* Header rules */
|
||||
h1 {
|
||||
font-size: 2rem;
|
||||
font-weight: bold;
|
||||
margin-top: 1.25rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
h2 {
|
||||
font-size: 1.6rem;
|
||||
font-weight: bold;
|
||||
margin-top: 1rem;
|
||||
margin-bottom: .5rem;
|
||||
}
|
||||
h3 {
|
||||
font-size: 1.4rem;
|
||||
font-weight: normal;
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
h4 {
|
||||
font-size: 1.2rem;
|
||||
font-weight: normal;
|
||||
margin-top: .5rem;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
h5,
|
||||
h6 {
|
||||
font-size: 1rem;
|
||||
font-weight: bold;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* Anchor link rules */
|
||||
|
@ -118,7 +103,6 @@ a:active,
|
|||
a:visited {
|
||||
color: var(--colour-links);
|
||||
display: inline;
|
||||
overflow-wrap: break-word;
|
||||
overflow-wrap: anywhere;
|
||||
text-decoration-color: var(--colour-background-accent-strong);
|
||||
}
|
||||
|
@ -131,7 +115,6 @@ a:focus {
|
|||
blockquote {
|
||||
font-style: italic;
|
||||
border-left: 1px solid var(--colour-rule-strong);
|
||||
margin: .5rem;
|
||||
padding: .5rem 1rem;
|
||||
}
|
||||
blockquote em {
|
||||
|
@ -145,13 +128,12 @@ cite {
|
|||
/* Code rules (code literals and Pygments highlighting blocks) */
|
||||
code,
|
||||
pre {
|
||||
font-family: ui-monospace, "Cascadia Mono", "Segoe UI Mono", "DejaVu Sans Mono", Consolas, monospace;
|
||||
font-family: Menlo, Consolas, Monaco, Liberation Mono, Lucida Console, monospace;
|
||||
font-size: 0.875rem;
|
||||
-webkit-hyphens: none;
|
||||
hyphens: none;
|
||||
}
|
||||
code {
|
||||
overflow-wrap: break-word;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
code.literal {
|
||||
|
@ -189,9 +171,8 @@ dl dd {
|
|||
hr {
|
||||
border: 0;
|
||||
border-top: 1px solid var(--colour-rule-light);
|
||||
margin: 0;
|
||||
}
|
||||
/*Image rules */
|
||||
/* Image rules */
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
@ -201,13 +182,6 @@ a img {
|
|||
}
|
||||
|
||||
/* List rules */
|
||||
ul,
|
||||
ol {
|
||||
padding: 0;
|
||||
margin: 0 0 0 1.5rem;
|
||||
}
|
||||
ul {list-style: square}
|
||||
ol.arabic {list-style: decimal}
|
||||
ol.loweralpha {list-style: lower-alpha}
|
||||
ol.upperalpha {list-style: upper-alpha}
|
||||
ol.lowerroman {list-style: lower-roman}
|
||||
|
@ -256,6 +230,22 @@ table th + th,
|
|||
table td + td {
|
||||
border-left: 1px solid var(--colour-background-accent-strong);
|
||||
}
|
||||
/* Common column widths for PEP status tables */
|
||||
table.pep-zero-table tr td:nth-child(1) {
|
||||
width: 5.5%;
|
||||
}
|
||||
table.pep-zero-table tr td:nth-child(2) {
|
||||
width: 6.5%;
|
||||
}
|
||||
table.pep-zero-table tr td:nth-child(3),
|
||||
table.pep-zero-table tr td:nth-child(4){
|
||||
width: 44%;
|
||||
}
|
||||
/* Authors & Sponsors table */
|
||||
#authors-owners table td,
|
||||
#authors-owners table th {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
/* Breadcrumbs rules */
|
||||
section#pep-page-section > header {
|
||||
|
@ -263,7 +253,6 @@ section#pep-page-section > header {
|
|||
}
|
||||
section#pep-page-section > header > h1 {
|
||||
font-size: 1.1rem;
|
||||
line-height: 1.4;
|
||||
margin: 0;
|
||||
display: inline-block;
|
||||
padding-right: .6rem;
|
||||
|
@ -379,7 +368,15 @@ dl.footnote > dd {
|
|||
#pep-sidebar > h2 {
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
#contents ol,
|
||||
#contents ul,
|
||||
#pep-sidebar ol,
|
||||
#pep-sidebar ul {
|
||||
padding: 0;
|
||||
margin: 0 0 0 1.5rem;
|
||||
}
|
||||
#pep-sidebar ul {
|
||||
font-size: .9rem;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
#pep-sidebar ul a {
|
||||
|
@ -410,4 +407,5 @@ dl.footnote > dd {
|
|||
.sticky-banner {
|
||||
top: 0;
|
||||
position: sticky;
|
||||
z-index: 1;
|
||||
}
|
||||
|
|
|
@ -13,7 +13,6 @@
|
|||
<link rel="stylesheet" href="{{ pathto('_static/pygments.css', resource=True) }}" type="text/css" media="(prefers-color-scheme: light)" id="pyg-light">
|
||||
<link rel="stylesheet" href="{{ pathto('_static/pygments_dark.css', resource=True) }}" type="text/css" media="(prefers-color-scheme: dark)" id="pyg-dark">
|
||||
<link rel="alternate" type="application/rss+xml" title="Latest PEPs" href="https://peps.python.org/peps.rss">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Source+Sans+Pro:ital,wght@0,400;0,700;1,400&display=swap" rel="stylesheet">
|
||||
<meta name="description" content="Python Enhancement Proposals (PEPs)">
|
||||
</head>
|
||||
<body>
|
||||
|
@ -44,8 +43,8 @@
|
|||
<h2>Contents</h2>
|
||||
{{ toc }}
|
||||
<br>
|
||||
{%- if not (sourcename.startswith("pep-0000") or sourcename.startswith("topic")) %}
|
||||
<a id="source" href="https://github.com/python/peps/blob/main/{{sourcename}}">Page Source (GitHub)</a>
|
||||
{%- if not pagename.startswith(("pep-0000", "topic")) %}
|
||||
<a id="source" href="https://github.com/python/peps/blob/main/peps/{{pagename}}.rst">Page Source (GitHub)</a>
|
||||
{%- endif %}
|
||||
</nav>
|
||||
</section>
|
||||
|
|
|
@ -1,89 +0,0 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import NamedTuple
|
||||
|
||||
|
||||
class _Name(NamedTuple):
|
||||
mononym: str = None
|
||||
forename: str = None
|
||||
surname: str = None
|
||||
suffix: str = None
|
||||
|
||||
|
||||
class Author(NamedTuple):
|
||||
"""Represent PEP authors."""
|
||||
last_first: str # The author's name in Surname, Forename, Suffix order.
|
||||
nick: str # Author's nickname for PEP tables. Defaults to surname.
|
||||
email: str # The author's email address.
|
||||
|
||||
|
||||
def parse_author_email(author_email_tuple: tuple[str, str], authors_overrides: dict[str, dict[str, str]]) -> Author:
|
||||
"""Parse the name and email address of an author."""
|
||||
name, email = author_email_tuple
|
||||
_first_last = name.strip()
|
||||
email = email.lower()
|
||||
|
||||
if _first_last in authors_overrides:
|
||||
name_dict = authors_overrides[_first_last]
|
||||
last_first = name_dict["Surname First"]
|
||||
nick = name_dict["Name Reference"]
|
||||
return Author(last_first, nick, email)
|
||||
|
||||
name_parts = _parse_name(_first_last)
|
||||
if name_parts.mononym is not None:
|
||||
return Author(name_parts.mononym, name_parts.mononym, email)
|
||||
|
||||
if name_parts.suffix:
|
||||
last_first = f"{name_parts.surname}, {name_parts.forename}, {name_parts.suffix}"
|
||||
return Author(last_first, name_parts.surname, email)
|
||||
|
||||
last_first = f"{name_parts.surname}, {name_parts.forename}"
|
||||
return Author(last_first, name_parts.surname, email)
|
||||
|
||||
|
||||
def _parse_name(full_name: str) -> _Name:
|
||||
"""Decompose a full name into parts.
|
||||
|
||||
If a mononym (e.g, 'Aahz') then return the full name. If there are
|
||||
suffixes in the name (e.g. ', Jr.' or 'II'), then find and extract
|
||||
them. If there is a middle initial followed by a full stop, then
|
||||
combine the following words into a surname (e.g. N. Vander Weele). If
|
||||
there is a leading, lowercase portion to the last name (e.g. 'van' or
|
||||
'von') then include it in the surname.
|
||||
|
||||
"""
|
||||
possible_suffixes = {"Jr", "Jr.", "II", "III"}
|
||||
|
||||
pre_suffix, _, raw_suffix = full_name.partition(",")
|
||||
name_parts = pre_suffix.strip().split(" ")
|
||||
num_parts = len(name_parts)
|
||||
suffix = raw_suffix.strip()
|
||||
|
||||
if name_parts == [""]:
|
||||
raise ValueError("Name is empty!")
|
||||
elif num_parts == 1:
|
||||
return _Name(mononym=name_parts[0], suffix=suffix)
|
||||
elif num_parts == 2:
|
||||
return _Name(forename=name_parts[0].strip(), surname=name_parts[1], suffix=suffix)
|
||||
|
||||
# handles rogue uncaught suffixes
|
||||
if name_parts[-1] in possible_suffixes:
|
||||
suffix = f"{name_parts.pop(-1)} {suffix}".strip()
|
||||
|
||||
# handles von, van, v. etc.
|
||||
if name_parts[-2].islower():
|
||||
forename = " ".join(name_parts[:-2]).strip()
|
||||
surname = " ".join(name_parts[-2:])
|
||||
return _Name(forename=forename, surname=surname, suffix=suffix)
|
||||
|
||||
# handles double surnames after a middle initial (e.g. N. Vander Weele)
|
||||
elif any(s.endswith(".") for s in name_parts):
|
||||
split_position = [i for i, x in enumerate(name_parts) if x.endswith(".")][-1] + 1
|
||||
forename = " ".join(name_parts[:split_position]).strip()
|
||||
surname = " ".join(name_parts[split_position:])
|
||||
return _Name(forename=forename, surname=surname, suffix=suffix)
|
||||
|
||||
# default to using the last item as the surname
|
||||
else:
|
||||
forename = " ".join(name_parts[:-1]).strip()
|
||||
return _Name(forename=forename, surname=name_parts[-1], suffix=suffix)
|
|
@ -2,13 +2,10 @@
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
import csv
|
||||
import dataclasses
|
||||
from email.parser import HeaderParser
|
||||
from pathlib import Path
|
||||
import re
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from pep_sphinx_extensions.pep_zero_generator.author import parse_author_email
|
||||
from pep_sphinx_extensions.pep_zero_generator.constants import ACTIVE_ALLOWED
|
||||
from pep_sphinx_extensions.pep_zero_generator.constants import HIDE_STATUS
|
||||
from pep_sphinx_extensions.pep_zero_generator.constants import SPECIAL_STATUSES
|
||||
|
@ -19,16 +16,12 @@ from pep_sphinx_extensions.pep_zero_generator.constants import TYPE_STANDARDS
|
|||
from pep_sphinx_extensions.pep_zero_generator.constants import TYPE_VALUES
|
||||
from pep_sphinx_extensions.pep_zero_generator.errors import PEPError
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pep_sphinx_extensions.pep_zero_generator.author import Author
|
||||
|
||||
|
||||
# AUTHOR_OVERRIDES.csv is an exception file for PEP 0 name parsing
|
||||
AUTHOR_OVERRIDES: dict[str, dict[str, str]] = {}
|
||||
with open("AUTHOR_OVERRIDES.csv", "r", encoding="utf-8") as f:
|
||||
for line in csv.DictReader(f):
|
||||
full_name = line.pop("Overridden Name")
|
||||
AUTHOR_OVERRIDES[full_name] = line
|
||||
@dataclasses.dataclass(order=True, frozen=True)
|
||||
class _Author:
|
||||
"""Represent PEP authors."""
|
||||
full_name: str # The author's name.
|
||||
email: str # The author's email address.
|
||||
|
||||
|
||||
class PEP:
|
||||
|
@ -97,7 +90,9 @@ class PEP:
|
|||
self.status: str = status
|
||||
|
||||
# Parse PEP authors
|
||||
self.authors: list[Author] = _parse_authors(self, metadata["Author"], AUTHOR_OVERRIDES)
|
||||
self.authors: list[_Author] = _parse_author(metadata["Author"])
|
||||
if not self.authors:
|
||||
raise _raise_pep_error(self, "no authors found", pep_num=True)
|
||||
|
||||
# Topic (for sub-indices)
|
||||
_topic = metadata.get("Topic", "").lower().split(",")
|
||||
|
@ -144,7 +139,7 @@ class PEP:
|
|||
# a tooltip representing the type and status
|
||||
"shorthand": self.shorthand,
|
||||
# the author list as a comma-separated with only last names
|
||||
"authors": ", ".join(author.nick for author in self.authors),
|
||||
"authors": ", ".join(author.full_name for author in self.authors),
|
||||
}
|
||||
|
||||
@property
|
||||
|
@ -153,7 +148,7 @@ class PEP:
|
|||
return {
|
||||
"number": self.number,
|
||||
"title": self.title,
|
||||
"authors": ", ".join(author.nick for author in self.authors),
|
||||
"authors": ", ".join(author.full_name for author in self.authors),
|
||||
"discussions_to": self.discussions_to,
|
||||
"status": self.status,
|
||||
"type": self.pep_type,
|
||||
|
@ -175,41 +170,27 @@ def _raise_pep_error(pep: PEP, msg: str, pep_num: bool = False) -> None:
|
|||
raise PEPError(msg, pep.filename)
|
||||
|
||||
|
||||
def _parse_authors(pep: PEP, author_header: str, authors_overrides: dict) -> list[Author]:
|
||||
"""Parse Author header line"""
|
||||
authors_and_emails = _parse_author(author_header)
|
||||
if not authors_and_emails:
|
||||
raise _raise_pep_error(pep, "no authors found", pep_num=True)
|
||||
return [parse_author_email(author_tuple, authors_overrides) for author_tuple in authors_and_emails]
|
||||
jr_placeholder = ",Jr"
|
||||
|
||||
|
||||
author_angled = re.compile(r"(?P<author>.+?) <(?P<email>.+?)>(,\s*)?")
|
||||
author_paren = re.compile(r"(?P<email>.+?) \((?P<author>.+?)\)(,\s*)?")
|
||||
author_simple = re.compile(r"(?P<author>[^,]+)(,\s*)?")
|
||||
|
||||
|
||||
def _parse_author(data: str) -> list[tuple[str, str]]:
|
||||
def _parse_author(data: str) -> list[_Author]:
|
||||
"""Return a list of author names and emails."""
|
||||
|
||||
author_list = []
|
||||
for regex in (author_angled, author_paren, author_simple):
|
||||
for match in regex.finditer(data):
|
||||
# Watch out for suffixes like 'Jr.' when they are comma-separated
|
||||
# from the name and thus cause issues when *all* names are only
|
||||
# separated by commas.
|
||||
match_dict = match.groupdict()
|
||||
author = match_dict["author"]
|
||||
if not author.partition(" ")[1] and author.endswith("."):
|
||||
prev_author = author_list.pop()
|
||||
author = ", ".join([prev_author, author])
|
||||
if "email" not in match_dict:
|
||||
email = ""
|
||||
else:
|
||||
email = match_dict["email"]
|
||||
author_list.append((author, email))
|
||||
data = (data.replace("\n", " ")
|
||||
.replace(", Jr", jr_placeholder)
|
||||
.rstrip().removesuffix(","))
|
||||
for author_email in data.split(", "):
|
||||
if ' <' in author_email:
|
||||
author, email = author_email.removesuffix(">").split(" <")
|
||||
else:
|
||||
author, email = author_email, ""
|
||||
|
||||
# If authors were found then stop searching as only expect one
|
||||
# style of author citation.
|
||||
if author_list:
|
||||
break
|
||||
author = author.strip()
|
||||
if author == "":
|
||||
raise ValueError("Name is empty!")
|
||||
|
||||
author = author.replace(jr_placeholder, ", Jr")
|
||||
email = email.lower()
|
||||
author_list.append(_Author(author, email))
|
||||
return author_list
|
||||
|
|
|
@ -18,22 +18,22 @@ to allow it to be processed as normal.
|
|||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from pep_sphinx_extensions.pep_zero_generator.constants import SUBINDICES_BY_TOPIC
|
||||
from pep_sphinx_extensions.pep_zero_generator import parser
|
||||
from pep_sphinx_extensions.pep_zero_generator import subindices
|
||||
from pep_sphinx_extensions.pep_zero_generator import writer
|
||||
from pep_sphinx_extensions.pep_zero_generator.constants import SUBINDICES_BY_TOPIC
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sphinx.application import Sphinx
|
||||
from sphinx.environment import BuildEnvironment
|
||||
|
||||
|
||||
def _parse_peps() -> list[parser.PEP]:
|
||||
def _parse_peps(path: Path) -> list[parser.PEP]:
|
||||
# Read from root directory
|
||||
path = Path(".")
|
||||
peps: list[parser.PEP] = []
|
||||
|
||||
for file_path in path.iterdir():
|
||||
|
@ -41,7 +41,7 @@ def _parse_peps() -> list[parser.PEP]:
|
|||
continue # Skip directories etc.
|
||||
if file_path.match("pep-0000*"):
|
||||
continue # Skip pre-existing PEP 0 files
|
||||
if file_path.match("pep-????.???") and file_path.suffix in {".txt", ".rst"}:
|
||||
if file_path.match("pep-????.rst"):
|
||||
pep = parser.PEP(path.joinpath(file_path).absolute())
|
||||
peps.append(pep)
|
||||
|
||||
|
@ -52,8 +52,16 @@ def create_pep_json(peps: list[parser.PEP]) -> str:
|
|||
return json.dumps({pep.number: pep.full_details for pep in peps}, indent=1)
|
||||
|
||||
|
||||
def write_peps_json(peps: list[parser.PEP], path: Path) -> None:
|
||||
# Create peps.json
|
||||
json_peps = create_pep_json(peps)
|
||||
Path(path, "peps.json").write_text(json_peps, encoding="utf-8")
|
||||
os.makedirs(os.path.join(path, "api"), exist_ok=True)
|
||||
Path(path, "api", "peps.json").write_text(json_peps, encoding="utf-8")
|
||||
|
||||
|
||||
def create_pep_zero(app: Sphinx, env: BuildEnvironment, docnames: list[str]) -> None:
|
||||
peps = _parse_peps()
|
||||
peps = _parse_peps(Path(app.srcdir))
|
||||
|
||||
pep0_text = writer.PEPZeroWriter().write_pep0(peps, builder=env.settings["builder"])
|
||||
pep0_path = subindices.update_sphinx("pep-0000", pep0_text, docnames, env)
|
||||
|
@ -61,7 +69,4 @@ def create_pep_zero(app: Sphinx, env: BuildEnvironment, docnames: list[str]) ->
|
|||
|
||||
subindices.generate_subindices(SUBINDICES_BY_TOPIC, peps, docnames, env)
|
||||
|
||||
# Create peps.json
|
||||
json_path = Path(app.outdir, "api", "peps.json").resolve()
|
||||
json_path.parent.mkdir(exist_ok=True)
|
||||
json_path.write_text(create_pep_json(peps), encoding="utf-8")
|
||||
write_peps_json(peps, Path(app.outdir))
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
|
@ -14,8 +15,7 @@ if TYPE_CHECKING:
|
|||
|
||||
|
||||
def update_sphinx(filename: str, text: str, docnames: list[str], env: BuildEnvironment) -> Path:
|
||||
file_path = Path(f"{filename}.rst").resolve()
|
||||
file_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
file_path = Path(env.srcdir, f"{filename}.rst")
|
||||
file_path.write_text(text, encoding="utf-8")
|
||||
|
||||
# Add to files for builder
|
||||
|
@ -32,6 +32,9 @@ def generate_subindices(
|
|||
docnames: list[str],
|
||||
env: BuildEnvironment,
|
||||
) -> None:
|
||||
# create topic directory
|
||||
os.makedirs(os.path.join(env.srcdir, "topic"), exist_ok=True)
|
||||
|
||||
# Create sub index page
|
||||
generate_topic_contents(docnames, env)
|
||||
|
||||
|
|
|
@ -2,14 +2,11 @@
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
from typing import TYPE_CHECKING
|
||||
import unicodedata
|
||||
|
||||
from pep_sphinx_extensions.pep_processor.transforms.pep_headers import (
|
||||
ABBREVIATED_STATUSES,
|
||||
ABBREVIATED_TYPES,
|
||||
)
|
||||
from pep_sphinx_extensions.pep_processor.transforms.pep_headers import ABBREVIATED_STATUSES
|
||||
from pep_sphinx_extensions.pep_processor.transforms.pep_headers import ABBREVIATED_TYPES
|
||||
from pep_sphinx_extensions.pep_zero_generator.constants import DEAD_STATUSES
|
||||
from pep_sphinx_extensions.pep_zero_generator.constants import STATUS_ACCEPTED
|
||||
from pep_sphinx_extensions.pep_zero_generator.constants import STATUS_ACTIVE
|
||||
|
@ -29,11 +26,10 @@ from pep_sphinx_extensions.pep_zero_generator.errors import PEPError
|
|||
if TYPE_CHECKING:
|
||||
from pep_sphinx_extensions.pep_zero_generator.parser import PEP
|
||||
|
||||
HEADER = f"""\
|
||||
HEADER = """\
|
||||
PEP: 0
|
||||
Title: Index of Python Enhancement Proposals (PEPs)
|
||||
Last-Modified: {datetime.date.today()}
|
||||
Author: python-dev <python-dev@python.org>
|
||||
Author: The PEP Editors
|
||||
Status: Active
|
||||
Type: Informational
|
||||
Content-Type: text/x-rst
|
||||
|
@ -149,7 +145,7 @@ class PEPZeroWriter:
|
|||
target = (
|
||||
f"topic/{subindex}.html"
|
||||
if builder == "html"
|
||||
else f"topic/{subindex}"
|
||||
else f"../topic/{subindex}/"
|
||||
)
|
||||
self.emit_text(f"* `{subindex.title()} PEPs <{target}>`_")
|
||||
self.emit_newline()
|
||||
|
@ -241,7 +237,7 @@ class PEPZeroWriter:
|
|||
self.emit_newline()
|
||||
self.emit_newline()
|
||||
|
||||
pep0_string = "\n".join([str(s) for s in self.output])
|
||||
pep0_string = "\n".join(map(str, self.output))
|
||||
return pep0_string
|
||||
|
||||
|
||||
|
@ -297,22 +293,22 @@ def _verify_email_addresses(peps: list[PEP]) -> dict[str, str]:
|
|||
for pep in peps:
|
||||
for author in pep.authors:
|
||||
# If this is the first time we have come across an author, add them.
|
||||
if author.last_first not in authors_dict:
|
||||
authors_dict[author.last_first] = set()
|
||||
if author.full_name not in authors_dict:
|
||||
authors_dict[author.full_name] = set()
|
||||
|
||||
# If the new email is an empty string, move on.
|
||||
if not author.email:
|
||||
continue
|
||||
# If the email has not been seen, add it to the list.
|
||||
authors_dict[author.last_first].add(author.email)
|
||||
authors_dict[author.full_name].add(author.email)
|
||||
|
||||
valid_authors_dict: dict[str, str] = {}
|
||||
too_many_emails: list[tuple[str, set[str]]] = []
|
||||
for last_first, emails in authors_dict.items():
|
||||
for full_name, emails in authors_dict.items():
|
||||
if len(emails) > 1:
|
||||
too_many_emails.append((last_first, emails))
|
||||
too_many_emails.append((full_name, emails))
|
||||
else:
|
||||
valid_authors_dict[last_first] = next(iter(emails), "")
|
||||
valid_authors_dict[full_name] = next(iter(emails), "")
|
||||
if too_many_emails:
|
||||
err_output = []
|
||||
for author, emails in too_many_emails:
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
import importlib.util
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
_ROOT_PATH = Path(__file__, "..", "..", "..").resolve()
|
||||
PEP_ROOT = _ROOT_PATH / "peps"
|
||||
|
||||
# Import "check-peps.py" as "check_peps"
|
||||
CHECK_PEPS_PATH = _ROOT_PATH / "check-peps.py"
|
||||
spec = importlib.util.spec_from_file_location("check_peps", CHECK_PEPS_PATH)
|
||||
sys.modules["check_peps"] = check_peps = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(check_peps)
|
|
@ -0,0 +1,105 @@
|
|||
import datetime as dt
|
||||
|
||||
import check_peps # NoQA: inserted into sys.modules in conftest.py
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"line",
|
||||
[
|
||||
# valid entries
|
||||
"01-Jan-2000",
|
||||
"29-Feb-2016",
|
||||
"31-Dec-2000",
|
||||
"01-Apr-2003",
|
||||
"01-Apr-2007",
|
||||
"01-Apr-2009",
|
||||
"01-Jan-1990",
|
||||
],
|
||||
)
|
||||
def test_validate_created(line: str):
|
||||
warnings = [warning for (_, warning) in check_peps._validate_created(1, line)]
|
||||
assert warnings == [], warnings
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"date_str",
|
||||
[
|
||||
# valid entries
|
||||
"01-Jan-2000",
|
||||
"29-Feb-2016",
|
||||
"31-Dec-2000",
|
||||
"01-Apr-2003",
|
||||
"01-Apr-2007",
|
||||
"01-Apr-2009",
|
||||
"01-Jan-1990",
|
||||
],
|
||||
)
|
||||
def test_date_checker_valid(date_str: str):
|
||||
warnings = [warning for (_, warning) in check_peps._date(1, date_str, "<Prefix>")]
|
||||
assert warnings == [], warnings
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"date_str",
|
||||
[
|
||||
# malformed
|
||||
"2000-01-01",
|
||||
"01 January 2000",
|
||||
"1 Jan 2000",
|
||||
"1-Jan-2000",
|
||||
"1-January-2000",
|
||||
"Jan-1-2000",
|
||||
"January 1 2000",
|
||||
"January 01 2000",
|
||||
"01/01/2000",
|
||||
"01/Jan/2000", # 🇬🇧, 🇦🇺, 🇨🇦, 🇳🇿, 🇮🇪 , ...
|
||||
"Jan/01/2000", # 🇺🇸
|
||||
"1st January 2000",
|
||||
"The First day of January in the year of Our Lord Two Thousand",
|
||||
"Jan, 1, 2000",
|
||||
"2000-Jan-1",
|
||||
"2000-Jan-01",
|
||||
"2000-January-1",
|
||||
"2000-January-01",
|
||||
"00 Jan 2000",
|
||||
"00-Jan-2000",
|
||||
],
|
||||
)
|
||||
def test_date_checker_malformed(date_str: str):
|
||||
warnings = [warning for (_, warning) in check_peps._date(1, date_str, "<Prefix>")]
|
||||
expected = f"<Prefix> must be a 'DD-mmm-YYYY' date: {date_str!r}"
|
||||
assert warnings == [expected], warnings
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"date_str",
|
||||
[
|
||||
# too early
|
||||
"31-Dec-1989",
|
||||
"01-Apr-1916",
|
||||
"01-Jan-0020",
|
||||
"01-Jan-0023",
|
||||
],
|
||||
)
|
||||
def test_date_checker_too_early(date_str: str):
|
||||
warnings = [warning for (_, warning) in check_peps._date(1, date_str, "<Prefix>")]
|
||||
expected = f"<Prefix> must not be before Python was invented: {date_str!r}"
|
||||
assert warnings == [expected], warnings
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"date_str",
|
||||
[
|
||||
# the future
|
||||
"31-Dec-2999",
|
||||
"01-Jan-2042",
|
||||
"01-Jan-2100",
|
||||
(dt.datetime.now() + dt.timedelta(days=15)).strftime("%d-%b-%Y"),
|
||||
(dt.datetime.now() + dt.timedelta(days=100)).strftime("%d-%b-%Y"),
|
||||
],
|
||||
)
|
||||
def test_date_checker_too_late(date_str: str):
|
||||
warnings = [warning for (_, warning) in check_peps._date(1, date_str, "<Prefix>")]
|
||||
expected = f"<Prefix> must not be in the future: {date_str!r}"
|
||||
assert warnings == [expected], warnings
|
|
@ -0,0 +1,30 @@
|
|||
import check_peps # NoQA: inserted into sys.modules in conftest.py
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"line",
|
||||
[
|
||||
"http://www.python.org/dev/peps/pep-0000/",
|
||||
"https://www.python.org/dev/peps/pep-0000/",
|
||||
"http://peps.python.org/pep-0000/",
|
||||
"https://peps.python.org/pep-0000/",
|
||||
],
|
||||
)
|
||||
def test_check_direct_links_pep(line: str):
|
||||
warnings = [warning for (_, warning) in check_peps.check_direct_links(1, line)]
|
||||
assert warnings == ["Use the :pep:`NNN` role to refer to PEPs"], warnings
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"line",
|
||||
[
|
||||
"http://www.rfc-editor.org/rfc/rfc2324",
|
||||
"https://www.rfc-editor.org/rfc/rfc2324",
|
||||
"http://datatracker.ietf.org/doc/html/rfc2324",
|
||||
"https://datatracker.ietf.org/doc/html/rfc2324",
|
||||
],
|
||||
)
|
||||
def test_check_direct_links_rfc(line: str):
|
||||
warnings = [warning for (_, warning) in check_peps.check_direct_links(1, line)]
|
||||
assert warnings == ["Use the :rfc:`NNN` role to refer to RFCs"], warnings
|
|
@ -0,0 +1,238 @@
|
|||
import check_peps # NoQA: inserted into sys.modules in conftest.py
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"line",
|
||||
[
|
||||
"Alice",
|
||||
"Alice,",
|
||||
"Alice, Bob, Charlie",
|
||||
"Alice,\nBob,\nCharlie",
|
||||
"Alice,\n Bob,\n Charlie",
|
||||
"Alice,\n Bob,\n Charlie",
|
||||
"Cardinal Ximénez",
|
||||
"Alice <alice@domain.example>",
|
||||
"Cardinal Ximénez <Cardinal.Ximenez@spanish.inquisition>",
|
||||
],
|
||||
ids=repr, # the default calls str and renders newlines.
|
||||
)
|
||||
def test_validate_author(line: str):
|
||||
warnings = [warning for (_, warning) in check_peps._validate_author(1, line)]
|
||||
assert warnings == [], warnings
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"line",
|
||||
[
|
||||
"Alice,\n Bob,\n Charlie",
|
||||
"Alice,\n Bob,\n Charlie",
|
||||
"Alice,\n Bob,\n Charlie",
|
||||
"Alice,\n Bob",
|
||||
],
|
||||
ids=repr, # the default calls str and renders newlines.
|
||||
)
|
||||
def test_validate_author_over__indented(line: str):
|
||||
warnings = [warning for (_, warning) in check_peps._validate_author(1, line)]
|
||||
assert {*warnings} == {"Author line must not be over-indented"}, warnings
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"line",
|
||||
[
|
||||
"Cardinal Ximénez\nCardinal Biggles\nCardinal Fang",
|
||||
"Cardinal Ximénez,\nCardinal Biggles\nCardinal Fang",
|
||||
"Cardinal Ximénez\nCardinal Biggles,\nCardinal Fang",
|
||||
],
|
||||
ids=repr, # the default calls str and renders newlines.
|
||||
)
|
||||
def test_validate_author_continuation(line: str):
|
||||
warnings = [warning for (_, warning) in check_peps._validate_author(1, line)]
|
||||
assert {*warnings} == {"Author continuation lines must end with a comma"}, warnings
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"line",
|
||||
[
|
||||
"Alice",
|
||||
"Cardinal Ximénez",
|
||||
"Alice <alice@domain.example>",
|
||||
"Cardinal Ximénez <Cardinal.Ximenez@spanish.inquisition>",
|
||||
],
|
||||
)
|
||||
def test_validate_sponsor(line: str):
|
||||
warnings = [warning for (_, warning) in check_peps._validate_sponsor(1, line)]
|
||||
assert warnings == [], warnings
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"line",
|
||||
[
|
||||
"",
|
||||
"Alice, Bob, Charlie",
|
||||
"Alice, Bob, Charlie,",
|
||||
"Alice <alice@domain.example>",
|
||||
"Cardinal Ximénez <Cardinal.Ximenez@spanish.inquisition>",
|
||||
],
|
||||
)
|
||||
def test_validate_delegate(line: str):
|
||||
warnings = [warning for (_, warning) in check_peps._validate_delegate(1, line)]
|
||||
assert warnings == [], warnings
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("email", "expected_warnings"),
|
||||
[
|
||||
# ... entries must not contain multiple '...'
|
||||
("Cardinal Ximénez <<", {"multiple <"}),
|
||||
("Cardinal Ximénez <<<", {"multiple <"}),
|
||||
("Cardinal Ximénez >>", {"multiple >"}),
|
||||
("Cardinal Ximénez >>>", {"multiple >"}),
|
||||
("Cardinal Ximénez <<<>>>", {"multiple <", "multiple >"}),
|
||||
("Cardinal Ximénez @@", {"multiple @"}),
|
||||
("Cardinal Ximénez <<@@@>", {"multiple <", "multiple @"}),
|
||||
("Cardinal Ximénez <@@@>>", {"multiple >", "multiple @"}),
|
||||
("Cardinal Ximénez <<@@>>", {"multiple <", "multiple >", "multiple @"}),
|
||||
# valid names
|
||||
("Cardinal Ximénez", set()),
|
||||
(" Cardinal Ximénez", set()),
|
||||
("\t\tCardinal Ximénez", set()),
|
||||
("Cardinal Ximénez ", set()),
|
||||
("Cardinal Ximénez\t\t", set()),
|
||||
("Cardinal O'Ximénez", set()),
|
||||
("Cardinal Ximénez, Inquisitor", set()),
|
||||
("Cardinal Ximénez-Biggles", set()),
|
||||
("Cardinal Ximénez-Biggles, Inquisitor", set()),
|
||||
("Cardinal T. S. I. Ximénez", set()),
|
||||
# ... entries must have a valid 'Name'
|
||||
("Cardinal_Ximénez", {"valid name"}),
|
||||
("Cardinal Ximénez 3", {"valid name"}),
|
||||
("~ Cardinal Ximénez ~", {"valid name"}),
|
||||
("Cardinal Ximénez!", {"valid name"}),
|
||||
("@Cardinal Ximénez", {"valid name"}),
|
||||
("Cardinal_Ximénez <>", {"valid name"}),
|
||||
("Cardinal Ximénez 3 <>", {"valid name"}),
|
||||
("~ Cardinal Ximénez ~ <>", {"valid name"}),
|
||||
("Cardinal Ximénez! <>", {"valid name"}),
|
||||
("@Cardinal Ximénez <>", {"valid name"}),
|
||||
# ... entries must be formatted as 'Name <email@example.com>'
|
||||
("Cardinal Ximénez<>", {"name <email>"}),
|
||||
("Cardinal Ximénez<", {"name <email>"}),
|
||||
("Cardinal Ximénez <", {"name <email>"}),
|
||||
("Cardinal Ximénez <", {"name <email>"}),
|
||||
("Cardinal Ximénez <>", {"name <email>"}),
|
||||
# ... entries must contain a valid email address (missing)
|
||||
("Cardinal Ximénez <>", {"valid email"}),
|
||||
("Cardinal Ximénez <> ", {"valid email"}),
|
||||
("Cardinal Ximénez <@> ", {"valid email"}),
|
||||
("Cardinal Ximénez <at> ", {"valid email"}),
|
||||
("Cardinal Ximénez < at > ", {"valid email"}),
|
||||
# ... entries must contain a valid email address (local)
|
||||
("Cardinal Ximénez <Cardinal.Ximénez@spanish.inquisition>", {"valid email"}),
|
||||
("Cardinal Ximénez <Cardinal.Ximénez at spanish.inquisition>", {"valid email"}),
|
||||
("Cardinal Ximénez <Cardinal.Ximenez AT spanish.inquisition>", {"valid email"}),
|
||||
("Cardinal Ximénez <Cardinal.Ximenez @spanish.inquisition> ", {"valid email"}),
|
||||
("Cardinal Ximénez <Cardinal Ximenez@spanish.inquisition> ", {"valid email"}),
|
||||
("Cardinal Ximénez < Cardinal Ximenez @spanish.inquisition> ", {"valid email"}),
|
||||
("Cardinal Ximénez <(Cardinal.Ximenez)@spanish.inquisition>", {"valid email"}),
|
||||
("Cardinal Ximénez <Cardinal,Ximenez@spanish.inquisition>", {"valid email"}),
|
||||
("Cardinal Ximénez <Cardinal:Ximenez@spanish.inquisition>", {"valid email"}),
|
||||
("Cardinal Ximénez <Cardinal;Ximenez@spanish.inquisition>", {"valid email"}),
|
||||
(
|
||||
"Cardinal Ximénez <Cardinal><Ximenez@spanish.inquisition>",
|
||||
{"multiple <", "multiple >", "valid email"},
|
||||
),
|
||||
(
|
||||
"Cardinal Ximénez <Cardinal@Ximenez@spanish.inquisition>",
|
||||
{"multiple @", "valid email"},
|
||||
),
|
||||
(r"Cardinal Ximénez <Cardinal\Ximenez@spanish.inquisition>", {"valid email"}),
|
||||
("Cardinal Ximénez <[Cardinal.Ximenez]@spanish.inquisition>", {"valid email"}),
|
||||
('Cardinal Ximénez <"Cardinal"Ximenez"@spanish.inquisition>', {"valid email"}),
|
||||
("Cardinal Ximenez <Cardinal;Ximenez@spanish.inquisition>", {"valid email"}),
|
||||
("Cardinal Ximénez <Cardinal£Ximénez@spanish.inquisition>", {"valid email"}),
|
||||
("Cardinal Ximénez <Cardinal§Ximenez@spanish.inquisition>", {"valid email"}),
|
||||
# ... entries must contain a valid email address (domain)
|
||||
(
|
||||
"Cardinal Ximénez <Cardinal.Ximenez@spanish+american.inquisition>",
|
||||
{"valid email"},
|
||||
),
|
||||
("Cardinal Ximénez <Cardinal.Ximenez@spani$h.inquisition>", {"valid email"}),
|
||||
("Cardinal Ximénez <Cardinal.Ximenez@spanish.inquisitioñ>", {"valid email"}),
|
||||
(
|
||||
"Cardinal Ximénez <Cardinal.Ximenez@th£.spanish.inquisition>",
|
||||
{"valid email"},
|
||||
),
|
||||
# valid name-emails
|
||||
("Cardinal Ximénez <Cardinal.Ximenez@spanish.inquisition>", set()),
|
||||
("Cardinal Ximénez <Cardinal.Ximenez at spanish.inquisition>", set()),
|
||||
("Cardinal Ximénez <Cardinal_Ximenez@spanish.inquisition>", set()),
|
||||
("Cardinal Ximénez <Cardinal-Ximenez@spanish.inquisition>", set()),
|
||||
("Cardinal Ximénez <Cardinal!Ximenez@spanish.inquisition>", set()),
|
||||
("Cardinal Ximénez <Cardinal#Ximenez@spanish.inquisition>", set()),
|
||||
("Cardinal Ximénez <Cardinal$Ximenez@spanish.inquisition>", set()),
|
||||
("Cardinal Ximénez <Cardinal%Ximenez@spanish.inquisition>", set()),
|
||||
("Cardinal Ximénez <Cardinal&Ximenez@spanish.inquisition>", set()),
|
||||
("Cardinal Ximénez <Cardinal'Ximenez@spanish.inquisition>", set()),
|
||||
("Cardinal Ximénez <Cardinal*Ximenez@spanish.inquisition>", set()),
|
||||
("Cardinal Ximénez <Cardinal+Ximenez@spanish.inquisition>", set()),
|
||||
("Cardinal Ximénez <Cardinal/Ximenez@spanish.inquisition>", set()),
|
||||
("Cardinal Ximénez <Cardinal=Ximenez@spanish.inquisition>", set()),
|
||||
("Cardinal Ximénez <Cardinal?Ximenez@spanish.inquisition>", set()),
|
||||
("Cardinal Ximénez <Cardinal^Ximenez@spanish.inquisition>", set()),
|
||||
("Cardinal Ximénez <{Cardinal.Ximenez}@spanish.inquisition>", set()),
|
||||
("Cardinal Ximénez <Cardinal|Ximenez@spanish.inquisition>", set()),
|
||||
("Cardinal Ximénez <Cardinal~Ximenez@spanish.inquisition>", set()),
|
||||
("Cardinal Ximénez <Cardinal.Ximenez@español.inquisition>", set()),
|
||||
("Cardinal Ximénez <Cardinal.Ximenez at español.inquisition>", set()),
|
||||
("Cardinal Ximénez <Cardinal.Ximenez@spanish-american.inquisition>", set()),
|
||||
],
|
||||
# call str() on each parameterised value in the test ID.
|
||||
ids=str,
|
||||
)
|
||||
def test_email_checker(email: str, expected_warnings: set):
|
||||
warnings = [warning for (_, warning) in check_peps._email(1, email, "<Prefix>")]
|
||||
|
||||
found_warnings = set()
|
||||
email = email.strip()
|
||||
|
||||
if "multiple <" in expected_warnings:
|
||||
found_warnings.add("multiple <")
|
||||
expected = f"<Prefix> entries must not contain multiple '<': {email!r}"
|
||||
matching = [w for w in warnings if w == expected]
|
||||
assert matching == [expected], warnings
|
||||
|
||||
if "multiple >" in expected_warnings:
|
||||
found_warnings.add("multiple >")
|
||||
expected = f"<Prefix> entries must not contain multiple '>': {email!r}"
|
||||
matching = [w for w in warnings if w == expected]
|
||||
assert matching == [expected], warnings
|
||||
|
||||
if "multiple @" in expected_warnings:
|
||||
found_warnings.add("multiple @")
|
||||
expected = f"<Prefix> entries must not contain multiple '@': {email!r}"
|
||||
matching = [w for w in warnings if w == expected]
|
||||
assert matching == [expected], warnings
|
||||
|
||||
if "valid name" in expected_warnings:
|
||||
found_warnings.add("valid name")
|
||||
expected = f"<Prefix> entries must begin with a valid 'Name': {email!r}"
|
||||
matching = [w for w in warnings if w == expected]
|
||||
assert matching == [expected], warnings
|
||||
|
||||
if "name <email>" in expected_warnings:
|
||||
found_warnings.add("name <email>")
|
||||
expected = f"<Prefix> entries must be formatted as 'Name <email@example.com>': {email!r}"
|
||||
matching = [w for w in warnings if w == expected]
|
||||
assert matching == [expected], warnings
|
||||
|
||||
if "valid email" in expected_warnings:
|
||||
found_warnings.add("valid email")
|
||||
expected = f"<Prefix> entries must contain a valid email address: {email!r}"
|
||||
matching = [w for w in warnings if w == expected]
|
||||
assert matching == [expected], warnings
|
||||
|
||||
if expected_warnings == set():
|
||||
assert warnings == [], warnings
|
||||
|
||||
assert found_warnings == expected_warnings
|
|
@ -0,0 +1,408 @@
|
|||
import check_peps # NoQA: inserted into sys.modules in conftest.py
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("test_input", "expected"),
|
||||
[
|
||||
# capitalisation
|
||||
("Header:", "Header"),
|
||||
("header:", "header"),
|
||||
("hEADER:", "hEADER"),
|
||||
("hEaDeR:", "hEaDeR"),
|
||||
# trailing spaces
|
||||
("Header: ", "Header"),
|
||||
("Header: ", "Header"),
|
||||
("Header: \t", "Header"),
|
||||
# trailing content
|
||||
("Header: Text", "Header"),
|
||||
("Header: 123", "Header"),
|
||||
("Header: !", "Header"),
|
||||
# separators
|
||||
("Hyphenated-Header:", "Hyphenated-Header"),
|
||||
],
|
||||
)
|
||||
def test_header_pattern(test_input, expected):
|
||||
assert check_peps.HEADER_PATTERN.match(test_input)[1] == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"test_input",
|
||||
[
|
||||
# trailing content
|
||||
"Header:Text",
|
||||
"Header:123",
|
||||
"Header:!",
|
||||
# colon position
|
||||
"Header",
|
||||
"Header : ",
|
||||
"Header :",
|
||||
"SemiColonHeader;",
|
||||
# separators
|
||||
"Underscored_Header:",
|
||||
"Spaced Header:",
|
||||
"Plus+Header:",
|
||||
],
|
||||
)
|
||||
def test_header_pattern_no_match(test_input):
|
||||
assert check_peps.HEADER_PATTERN.match(test_input) is None
|
||||
|
||||
|
||||
def test_validate_required_headers():
|
||||
found_headers = dict.fromkeys(
|
||||
("PEP", "Title", "Author", "Status", "Type", "Created")
|
||||
)
|
||||
warnings = [
|
||||
warning for (_, warning) in check_peps._validate_required_headers(found_headers)
|
||||
]
|
||||
assert warnings == [], warnings
|
||||
|
||||
|
||||
def test_validate_required_headers_missing():
|
||||
found_headers = dict.fromkeys(("PEP", "Title", "Author", "Type"))
|
||||
warnings = [
|
||||
warning for (_, warning) in check_peps._validate_required_headers(found_headers)
|
||||
]
|
||||
assert warnings == [
|
||||
"Must have required header: Status",
|
||||
"Must have required header: Created",
|
||||
], warnings
|
||||
|
||||
|
||||
def test_validate_required_headers_order():
|
||||
found_headers = dict.fromkeys(
|
||||
("PEP", "Title", "Sponsor", "Author", "Type", "Status", "Replaces", "Created")
|
||||
)
|
||||
warnings = [
|
||||
warning for (_, warning) in check_peps._validate_required_headers(found_headers)
|
||||
]
|
||||
assert warnings == [
|
||||
"Headers must be in PEP 12 order. Correct order: PEP, Title, Author, Sponsor, Status, Type, Created, Replaces"
|
||||
], warnings
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"line",
|
||||
[
|
||||
"!",
|
||||
"The Zen of Python",
|
||||
"A title that is exactly 79 characters long, but shorter than 80 characters long",
|
||||
],
|
||||
)
|
||||
def test_validate_title(line: str):
|
||||
warnings = [warning for (_, warning) in check_peps._validate_title(1, line)]
|
||||
assert warnings == [], warnings
|
||||
|
||||
|
||||
def test_validate_title_blank():
|
||||
warnings = [warning for (_, warning) in check_peps._validate_title(1, "-" * 80)]
|
||||
assert warnings == ["PEP title must be less than 80 characters"], warnings
|
||||
|
||||
|
||||
def test_validate_title_too_long():
|
||||
warnings = [warning for (_, warning) in check_peps._validate_title(1, "")]
|
||||
assert warnings == ["PEP must have a title"], warnings
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"line",
|
||||
[
|
||||
"Accepted",
|
||||
"Active",
|
||||
"April Fool!",
|
||||
"Deferred",
|
||||
"Draft",
|
||||
"Final",
|
||||
"Provisional",
|
||||
"Rejected",
|
||||
"Superseded",
|
||||
"Withdrawn",
|
||||
],
|
||||
)
|
||||
def test_validate_status_valid(line: str):
|
||||
warnings = [warning for (_, warning) in check_peps._validate_status(1, line)]
|
||||
assert warnings == [], warnings
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"line",
|
||||
[
|
||||
"Standards Track",
|
||||
"Informational",
|
||||
"Process",
|
||||
"accepted",
|
||||
"active",
|
||||
"april fool!",
|
||||
"deferred",
|
||||
"draft",
|
||||
"final",
|
||||
"provisional",
|
||||
"rejected",
|
||||
"superseded",
|
||||
"withdrawn",
|
||||
],
|
||||
)
|
||||
def test_validate_status_invalid(line: str):
|
||||
warnings = [warning for (_, warning) in check_peps._validate_status(1, line)]
|
||||
assert warnings == ["Status must be a valid PEP status"], warnings
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"line",
|
||||
[
|
||||
"Standards Track",
|
||||
"Informational",
|
||||
"Process",
|
||||
],
|
||||
)
|
||||
def test_validate_type_valid(line: str):
|
||||
warnings = [warning for (_, warning) in check_peps._validate_type(1, line)]
|
||||
assert warnings == [], warnings
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"line",
|
||||
[
|
||||
"standards track",
|
||||
"informational",
|
||||
"process",
|
||||
"Accepted",
|
||||
"Active",
|
||||
"April Fool!",
|
||||
"Deferred",
|
||||
"Draft",
|
||||
"Final",
|
||||
"Provisional",
|
||||
"Rejected",
|
||||
"Superseded",
|
||||
"Withdrawn",
|
||||
],
|
||||
)
|
||||
def test_validate_type_invalid(line: str):
|
||||
warnings = [warning for (_, warning) in check_peps._validate_type(1, line)]
|
||||
assert warnings == ["Type must be a valid PEP type"], warnings
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("line", "expected_warnings"),
|
||||
[
|
||||
# valid entries
|
||||
("Governance", set()),
|
||||
("Packaging", set()),
|
||||
("Typing", set()),
|
||||
("Release", set()),
|
||||
("Governance, Packaging", set()),
|
||||
("Packaging, Typing", set()),
|
||||
# duplicates
|
||||
("Governance, Governance", {"duplicates"}),
|
||||
("Release, Release", {"duplicates"}),
|
||||
("Packaging, Packaging", {"duplicates"}),
|
||||
("Spam, Spam", {"duplicates", "valid"}),
|
||||
("lobster, lobster", {"duplicates", "capitalisation", "valid"}),
|
||||
("governance, governance", {"duplicates", "capitalisation"}),
|
||||
# capitalisation
|
||||
("governance", {"capitalisation"}),
|
||||
("packaging", {"capitalisation"}),
|
||||
("typing", {"capitalisation"}),
|
||||
("release", {"capitalisation"}),
|
||||
("Governance, release", {"capitalisation"}),
|
||||
# validity
|
||||
("Spam", {"valid"}),
|
||||
("lobster", {"capitalisation", "valid"}),
|
||||
# sorted
|
||||
("Packaging, Governance", {"sorted"}),
|
||||
("Typing, Release", {"sorted"}),
|
||||
("Release, Governance", {"sorted"}),
|
||||
("spam, packaging", {"capitalisation", "valid", "sorted"}),
|
||||
],
|
||||
# call str() on each parameterised value in the test ID.
|
||||
ids=str,
|
||||
)
|
||||
def test_validate_topic(line: str, expected_warnings: set):
|
||||
warnings = [warning for (_, warning) in check_peps._validate_topic(1, line)]
|
||||
|
||||
found_warnings = set()
|
||||
|
||||
if "duplicates" in expected_warnings:
|
||||
found_warnings.add("duplicates")
|
||||
expected = "Topic must not contain duplicates"
|
||||
matching = [w for w in warnings if w == expected]
|
||||
assert matching == [expected], warnings
|
||||
|
||||
if "capitalisation" in expected_warnings:
|
||||
found_warnings.add("capitalisation")
|
||||
expected = "Topic must be properly capitalised (Title Case)"
|
||||
matching = [w for w in warnings if w == expected]
|
||||
assert matching == [expected], warnings
|
||||
|
||||
if "valid" in expected_warnings:
|
||||
found_warnings.add("valid")
|
||||
expected = "Topic must be for a valid sub-index"
|
||||
matching = [w for w in warnings if w == expected]
|
||||
assert matching == [expected], warnings
|
||||
|
||||
if "sorted" in expected_warnings:
|
||||
found_warnings.add("sorted")
|
||||
expected = "Topic must be sorted lexicographically"
|
||||
matching = [w for w in warnings if w == expected]
|
||||
assert matching == [expected], warnings
|
||||
|
||||
if expected_warnings == set():
|
||||
assert warnings == [], warnings
|
||||
|
||||
assert found_warnings == expected_warnings
|
||||
|
||||
|
||||
def test_validate_content_type_valid():
|
||||
warnings = [
|
||||
warning for (_, warning) in check_peps._validate_content_type(1, "text/x-rst")
|
||||
]
|
||||
assert warnings == [], warnings
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"line",
|
||||
[
|
||||
"text/plain",
|
||||
"text/markdown",
|
||||
"text/csv",
|
||||
"text/rtf",
|
||||
"text/javascript",
|
||||
"text/html",
|
||||
"text/xml",
|
||||
],
|
||||
)
|
||||
def test_validate_content_type_invalid(line: str):
|
||||
warnings = [warning for (_, warning) in check_peps._validate_content_type(1, line)]
|
||||
assert warnings == ["Content-Type must be 'text/x-rst'"], warnings
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"line",
|
||||
[
|
||||
"0, 1, 8, 12, 20,",
|
||||
"101, 801,",
|
||||
"3099, 9999",
|
||||
],
|
||||
)
|
||||
def test_validate_pep_references(line: str):
|
||||
warnings = [
|
||||
warning for (_, warning) in check_peps._validate_pep_references(1, line)
|
||||
]
|
||||
assert warnings == [], warnings
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"line",
|
||||
[
|
||||
"0,1,8, 12, 20,",
|
||||
"101,801,",
|
||||
"3099, 9998,9999",
|
||||
],
|
||||
)
|
||||
def test_validate_pep_references_separators(line: str):
|
||||
warnings = [
|
||||
warning for (_, warning) in check_peps._validate_pep_references(1, line)
|
||||
]
|
||||
assert warnings == [
|
||||
"PEP references must be separated by comma-spaces (', ')"
|
||||
], warnings
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("line", "expected_warnings"),
|
||||
[
|
||||
# valid entries
|
||||
("1.0, 2.4, 2.7, 2.8, 3.0, 3.1, 3.4, 3.7, 3.11, 3.14", set()),
|
||||
("2.x", set()),
|
||||
("3.x", set()),
|
||||
("3.0.1", set()),
|
||||
# segments
|
||||
("", {"segments"}),
|
||||
("1", {"segments"}),
|
||||
("1.2.3.4", {"segments"}),
|
||||
# major
|
||||
("0.0", {"major"}),
|
||||
("4.0", {"major"}),
|
||||
("9.0", {"major"}),
|
||||
# minor number
|
||||
("3.a", {"minor numeric"}),
|
||||
("3.spam", {"minor numeric"}),
|
||||
("3.0+", {"minor numeric"}),
|
||||
("3.0-9", {"minor numeric"}),
|
||||
("9.Z", {"major", "minor numeric"}),
|
||||
# minor leading zero
|
||||
("3.01", {"minor zero"}),
|
||||
("0.00", {"major", "minor zero"}),
|
||||
# micro empty
|
||||
("3.x.1", {"micro empty"}),
|
||||
("9.x.1", {"major", "micro empty"}),
|
||||
# micro leading zero
|
||||
("3.3.0", {"micro zero"}),
|
||||
("3.3.00", {"micro zero"}),
|
||||
("3.3.01", {"micro zero"}),
|
||||
("3.0.0", {"micro zero"}),
|
||||
("3.00.0", {"minor zero", "micro zero"}),
|
||||
("0.00.0", {"major", "minor zero", "micro zero"}),
|
||||
# micro number
|
||||
("3.0.a", {"micro numeric"}),
|
||||
("0.3.a", {"major", "micro numeric"}),
|
||||
],
|
||||
# call str() on each parameterised value in the test ID.
|
||||
ids=str,
|
||||
)
|
||||
def test_validate_python_version(line: str, expected_warnings: set):
|
||||
warnings = [
|
||||
warning for (_, warning) in check_peps._validate_python_version(1, line)
|
||||
]
|
||||
|
||||
found_warnings = set()
|
||||
|
||||
if "segments" in expected_warnings:
|
||||
found_warnings.add("segments")
|
||||
expected = f"Python-Version must have two or three segments: {line}"
|
||||
matching = [w for w in warnings if w == expected]
|
||||
assert matching == [expected], warnings
|
||||
|
||||
if "major" in expected_warnings:
|
||||
found_warnings.add("major")
|
||||
expected = f"Python-Version major part must be 1, 2, or 3: {line}"
|
||||
matching = [w for w in warnings if w == expected]
|
||||
assert matching == [expected], warnings
|
||||
|
||||
if "minor numeric" in expected_warnings:
|
||||
found_warnings.add("minor numeric")
|
||||
expected = f"Python-Version minor part must be numeric: {line}"
|
||||
matching = [w for w in warnings if w == expected]
|
||||
assert matching == [expected], warnings
|
||||
|
||||
if "minor zero" in expected_warnings:
|
||||
found_warnings.add("minor zero")
|
||||
expected = f"Python-Version minor part must not have leading zeros: {line}"
|
||||
matching = [w for w in warnings if w == expected]
|
||||
assert matching == [expected], warnings
|
||||
|
||||
if "micro empty" in expected_warnings:
|
||||
found_warnings.add("micro empty")
|
||||
expected = (
|
||||
f"Python-Version micro part must be empty if minor part is 'x': {line}"
|
||||
)
|
||||
matching = [w for w in warnings if w == expected]
|
||||
assert matching == [expected], warnings
|
||||
|
||||
if "micro zero" in expected_warnings:
|
||||
found_warnings.add("micro zero")
|
||||
expected = f"Python-Version micro part must not have leading zeros: {line}"
|
||||
matching = [w for w in warnings if w == expected]
|
||||
assert matching == [expected], warnings
|
||||
|
||||
if "micro numeric" in expected_warnings:
|
||||
found_warnings.add("micro numeric")
|
||||
expected = f"Python-Version micro part must be numeric: {line}"
|
||||
matching = [w for w in warnings if w == expected]
|
||||
assert matching == [expected], warnings
|
||||
|
||||
if expected_warnings == set():
|
||||
assert warnings == [], warnings
|
||||
|
||||
assert found_warnings == expected_warnings
|
|
@ -0,0 +1,48 @@
|
|||
from pathlib import Path
|
||||
|
||||
import check_peps # NoQA: inserted into sys.modules in conftest.py
|
||||
|
||||
PEP_9002 = Path(__file__).parent.parent / "peps" / "pep-9002.rst"
|
||||
|
||||
|
||||
def test_with_fake_pep():
|
||||
content = PEP_9002.read_text(encoding="utf-8").splitlines()
|
||||
warnings = list(check_peps.check_peps(PEP_9002, content))
|
||||
assert warnings == [
|
||||
(1, "PEP must begin with the 'PEP:' header"),
|
||||
(9, "Must not have duplicate header: Sponsor "),
|
||||
(10, "Must not have invalid header: Horse-Guards"),
|
||||
(1, "Must have required header: PEP"),
|
||||
(1, "Must have required header: Type"),
|
||||
(
|
||||
1,
|
||||
"Headers must be in PEP 12 order. Correct order: Title, Version, "
|
||||
"Author, Sponsor, BDFL-Delegate, Discussions-To, Status, Topic, "
|
||||
"Content-Type, Requires, Created, Python-Version, Post-History, "
|
||||
"Resolution",
|
||||
),
|
||||
(4, "Author continuation lines must end with a comma"),
|
||||
(5, "Author line must not be over-indented"),
|
||||
(7, "Python-Version major part must be 1, 2, or 3: 4.0"),
|
||||
(
|
||||
8,
|
||||
"Sponsor entries must begin with a valid 'Name': "
|
||||
r"'Sponsor:\nHorse-Guards: Parade'",
|
||||
),
|
||||
(11, "Created must be a 'DD-mmm-YYYY' date: '1-Jan-1989'"),
|
||||
(12, "Delegate entries must begin with a valid 'Name': 'Barry!'"),
|
||||
(13, "Status must be a valid PEP status"),
|
||||
(14, "Topic must not contain duplicates"),
|
||||
(14, "Topic must be properly capitalised (Title Case)"),
|
||||
(14, "Topic must be for a valid sub-index"),
|
||||
(14, "Topic must be sorted lexicographically"),
|
||||
(15, "Content-Type must be 'text/x-rst'"),
|
||||
(16, "PEP references must be separated by comma-spaces (', ')"),
|
||||
(17, "Discussions-To must be a valid thread URL or mailing list"),
|
||||
(18, "Post-History must be a 'DD-mmm-YYYY' date: '2-Feb-2000'"),
|
||||
(18, "Post-History must be a valid thread URL"),
|
||||
(19, "Post-History must be a 'DD-mmm-YYYY' date: '3-Mar-2001'"),
|
||||
(19, "Post-History must be a valid thread URL"),
|
||||
(20, "Resolution must be a valid thread URL"),
|
||||
(23, "Use the :pep:`NNN` role to refer to PEPs"),
|
||||
]
|
|
@ -0,0 +1,108 @@
|
|||
import check_peps # NoQA: inserted into sys.modules in conftest.py
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"line",
|
||||
[
|
||||
"PEP: 0",
|
||||
"PEP: 12",
|
||||
],
|
||||
)
|
||||
def test_validate_pep_number(line: str):
|
||||
warnings = [warning for (_, warning) in check_peps._validate_pep_number(line)]
|
||||
assert warnings == [], warnings
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"line",
|
||||
[
|
||||
"0",
|
||||
"PEP:12",
|
||||
"PEP 0",
|
||||
"PEP 12",
|
||||
"PEP:0",
|
||||
],
|
||||
)
|
||||
def test_validate_pep_number_invalid_header(line: str):
|
||||
warnings = [warning for (_, warning) in check_peps._validate_pep_number(line)]
|
||||
assert warnings == ["PEP must begin with the 'PEP:' header"], warnings
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("pep_number", "expected_warnings"),
|
||||
[
|
||||
# valid entries
|
||||
("0", set()),
|
||||
("1", set()),
|
||||
("12", set()),
|
||||
("20", set()),
|
||||
("101", set()),
|
||||
("801", set()),
|
||||
("3099", set()),
|
||||
("9999", set()),
|
||||
# empty
|
||||
("", {"not blank"}),
|
||||
# leading zeros
|
||||
("01", {"leading zeros"}),
|
||||
("001", {"leading zeros"}),
|
||||
("0001", {"leading zeros"}),
|
||||
("00001", {"leading zeros"}),
|
||||
# non-numeric
|
||||
("a", {"non-numeric"}),
|
||||
("123abc", {"non-numeric"}),
|
||||
("0123A", {"leading zeros", "non-numeric"}),
|
||||
("0", {"non-numeric"}),
|
||||
("101", {"non-numeric"}),
|
||||
("9999", {"non-numeric"}),
|
||||
("𝟎", {"non-numeric"}),
|
||||
("𝟘", {"non-numeric"}),
|
||||
("𝟏𝟚", {"non-numeric"}),
|
||||
("𝟸𝟬", {"non-numeric"}),
|
||||
("-1", {"non-numeric"}),
|
||||
("+1", {"non-numeric"}),
|
||||
# out of bounds
|
||||
("10000", {"range"}),
|
||||
("54321", {"range"}),
|
||||
("99999", {"range"}),
|
||||
("32768", {"range"}),
|
||||
],
|
||||
# call str() on each parameterised value in the test ID.
|
||||
ids=str,
|
||||
)
|
||||
def test_pep_num_checker(pep_number: str, expected_warnings: set):
|
||||
warnings = [
|
||||
warning for (_, warning) in check_peps._pep_num(1, pep_number, "<Prefix>")
|
||||
]
|
||||
|
||||
found_warnings = set()
|
||||
pep_number = pep_number.strip()
|
||||
|
||||
if "not blank" in expected_warnings:
|
||||
found_warnings.add("not blank")
|
||||
expected = f"<Prefix> must not be blank: {pep_number!r}"
|
||||
matching = [w for w in warnings if w == expected]
|
||||
assert matching == [expected], warnings
|
||||
|
||||
if "leading zeros" in expected_warnings:
|
||||
found_warnings.add("leading zeros")
|
||||
expected = f"<Prefix> must not contain leading zeros: {pep_number!r}"
|
||||
matching = [w for w in warnings if w == expected]
|
||||
assert matching == [expected], warnings
|
||||
|
||||
if "non-numeric" in expected_warnings:
|
||||
found_warnings.add("non-numeric")
|
||||
expected = f"<Prefix> must be numeric: {pep_number!r}"
|
||||
matching = [w for w in warnings if w == expected]
|
||||
assert matching == [expected], warnings
|
||||
|
||||
if "range" in expected_warnings:
|
||||
found_warnings.add("range")
|
||||
expected = f"<Prefix> must be between 0 and 9999: {pep_number!r}"
|
||||
matching = [w for w in warnings if w == expected]
|
||||
assert matching == [expected], warnings
|
||||
|
||||
if expected_warnings == set():
|
||||
assert warnings == [], warnings
|
||||
|
||||
assert found_warnings == expected_warnings
|
|
@ -0,0 +1,305 @@
|
|||
import check_peps # NoQA: inserted into sys.modules in conftest.py
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"line",
|
||||
[
|
||||
"list-name@python.org",
|
||||
"distutils-sig@python.org",
|
||||
"csv@python.org",
|
||||
"python-3000@python.org",
|
||||
"ipaddr-py-dev@googlegroups.com",
|
||||
"python-tulip@googlegroups.com",
|
||||
"https://discuss.python.org/t/thread-name/123456",
|
||||
"https://discuss.python.org/t/thread-name/123456/",
|
||||
"https://discuss.python.org/t/thread_name/123456",
|
||||
"https://discuss.python.org/t/thread_name/123456/",
|
||||
"https://discuss.python.org/t/123456/",
|
||||
"https://discuss.python.org/t/123456",
|
||||
],
|
||||
)
|
||||
def test_validate_discussions_to_valid(line: str):
|
||||
warnings = [
|
||||
warning for (_, warning) in check_peps._validate_discussions_to(1, line)
|
||||
]
|
||||
assert warnings == [], warnings
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"line",
|
||||
[
|
||||
"$pecial+chars@python.org",
|
||||
"a-discussions-to-list!@googlegroups.com",
|
||||
],
|
||||
)
|
||||
def test_validate_discussions_to_list_name(line: str):
|
||||
warnings = [
|
||||
warning for (_, warning) in check_peps._validate_discussions_to(1, line)
|
||||
]
|
||||
assert warnings == ["Discussions-To must be a valid mailing list"], warnings
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"line",
|
||||
[
|
||||
"list-name@python.org.uk",
|
||||
"distutils-sig@mail-server.example",
|
||||
],
|
||||
)
|
||||
def test_validate_discussions_to_invalid_list_domain(line: str):
|
||||
warnings = [
|
||||
warning for (_, warning) in check_peps._validate_discussions_to(1, line)
|
||||
]
|
||||
assert warnings == [
|
||||
"Discussions-To must be a valid thread URL or mailing list"
|
||||
], warnings
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"body",
|
||||
[
|
||||
"",
|
||||
(
|
||||
"01-Jan-2001, 02-Feb-2002,\n "
|
||||
"03-Mar-2003, 04-Apr-2004,\n "
|
||||
"05-May-2005,"
|
||||
),
|
||||
(
|
||||
"`01-Jan-2000 <https://mail.python.org/pipermail/list-name/0000-Month/0123456.html>`__,\n "
|
||||
"`11-Mar-2005 <https://mail.python.org/archives/list/list-name@python.org/thread/abcdef0123456789/>`__,\n "
|
||||
"`21-May-2010 <https://discuss.python.org/t/thread-name/123456/654321>`__,\n "
|
||||
"`31-Jul-2015 <https://discuss.python.org/t/123456>`__,"
|
||||
),
|
||||
"01-Jan-2001, `02-Feb-2002 <https://discuss.python.org/t/123456>`__,\n03-Mar-2003",
|
||||
],
|
||||
)
|
||||
def test_validate_post_history_valid(body: str):
|
||||
warnings = [warning for (_, warning) in check_peps._validate_post_history(1, body)]
|
||||
assert warnings == [], warnings
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"line",
|
||||
[
|
||||
"https://mail.python.org/archives/list/list-name@python.org/thread/abcXYZ123",
|
||||
"https://mail.python.org/archives/list/list-name@python.org/thread/abcXYZ123/",
|
||||
"https://mail.python.org/archives/list/list-name@python.org/message/abcXYZ123",
|
||||
"https://mail.python.org/archives/list/list-name@python.org/message/abcXYZ123/",
|
||||
"https://mail.python.org/archives/list/list-name@python.org/message/abcXYZ123#Anchor",
|
||||
"https://mail.python.org/archives/list/list-name@python.org/message/abcXYZ123/#Anchor",
|
||||
"https://mail.python.org/archives/list/list-name@python.org/message/abcXYZ123#Anchor123",
|
||||
"https://mail.python.org/archives/list/list-name@python.org/message/abcXYZ123/#Anchor123",
|
||||
],
|
||||
)
|
||||
def test_validate_resolution_valid(line: str):
|
||||
warnings = [warning for (_, warning) in check_peps._validate_resolution(1, line)]
|
||||
assert warnings == [], warnings
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"line",
|
||||
[
|
||||
"https://mail.python.org/archives/list/list-name@python.org/thread",
|
||||
"https://mail.python.org/archives/list/list-name@python.org/message",
|
||||
"https://mail.python.org/archives/list/list-name@python.org/thread/",
|
||||
"https://mail.python.org/archives/list/list-name@python.org/message/",
|
||||
"https://mail.python.org/archives/list/list-name@python.org/thread/abcXYZ123#anchor",
|
||||
"https://mail.python.org/archives/list/list-name@python.org/thread/abcXYZ123/#anchor",
|
||||
"https://mail.python.org/archives/list/list-name@python.org/message/#abcXYZ123",
|
||||
"https://mail.python.org/archives/list/list-name@python.org/message/#abcXYZ123/",
|
||||
"https://mail.python.org/archives/list/list-name@python.org/spam/abcXYZ123",
|
||||
"https://mail.python.org/archives/list/list-name@python.org/spam/abcXYZ123/",
|
||||
],
|
||||
)
|
||||
def test_validate_resolution_invalid(line: str):
|
||||
warnings = [warning for (_, warning) in check_peps._validate_resolution(1, line)]
|
||||
assert warnings == ["Resolution must be a valid thread URL"], warnings
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"thread_url",
|
||||
[
|
||||
"https://discuss.python.org/t/thread-name/123456",
|
||||
"https://discuss.python.org/t/thread-name/123456/",
|
||||
"https://discuss.python.org/t/thread_name/123456",
|
||||
"https://discuss.python.org/t/thread_name/123456/",
|
||||
"https://discuss.python.org/t/thread-name/123456/654321/",
|
||||
"https://discuss.python.org/t/thread-name/123456/654321",
|
||||
"https://discuss.python.org/t/123456",
|
||||
"https://discuss.python.org/t/123456/",
|
||||
"https://discuss.python.org/t/123456/654321/",
|
||||
"https://discuss.python.org/t/123456/654321",
|
||||
"https://discuss.python.org/t/1",
|
||||
"https://discuss.python.org/t/1/",
|
||||
"https://mail.python.org/pipermail/list-name/0000-Month/0123456.html",
|
||||
"https://mail.python.org/archives/list/list-name@python.org/thread/abcXYZ123",
|
||||
"https://mail.python.org/archives/list/list-name@python.org/thread/abcXYZ123/",
|
||||
],
|
||||
)
|
||||
def test_thread_checker_valid(thread_url: str):
|
||||
warnings = [
|
||||
warning for (_, warning) in check_peps._thread(1, thread_url, "<Prefix>")
|
||||
]
|
||||
assert warnings == [], warnings
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"thread_url",
|
||||
[
|
||||
"http://link.example",
|
||||
"list-name@python.org",
|
||||
"distutils-sig@python.org",
|
||||
"csv@python.org",
|
||||
"python-3000@python.org",
|
||||
"ipaddr-py-dev@googlegroups.com",
|
||||
"python-tulip@googlegroups.com",
|
||||
"https://link.example",
|
||||
"https://discuss.python.org",
|
||||
"https://discuss.python.org/",
|
||||
"https://discuss.python.org/c/category",
|
||||
"https://discuss.python.org/t/thread_name/123456//",
|
||||
"https://discuss.python.org/t/thread+name/123456",
|
||||
"https://discuss.python.org/t/thread+name/123456#",
|
||||
"https://discuss.python.org/t/thread+name/123456/#",
|
||||
"https://discuss.python.org/t/thread+name/123456/#anchor",
|
||||
"https://discuss.python.org/t/thread+name/",
|
||||
"https://discuss.python.org/t/thread+name",
|
||||
"https://discuss.python.org/t/thread-name/123abc",
|
||||
"https://discuss.python.org/t/thread-name/123abc/",
|
||||
"https://discuss.python.org/t/thread-name/123456/123abc",
|
||||
"https://discuss.python.org/t/thread-name/123456/123abc/",
|
||||
"https://discuss.python.org/t/123/456/789",
|
||||
"https://discuss.python.org/t/123/456/789/",
|
||||
"https://discuss.python.org/t/#/",
|
||||
"https://discuss.python.org/t/#",
|
||||
"https://mail.python.org/pipermail/list+name/0000-Month/0123456.html",
|
||||
"https://mail.python.org/pipermail/list-name/YYYY-Month/0123456.html",
|
||||
"https://mail.python.org/pipermail/list-name/0123456/0123456.html",
|
||||
"https://mail.python.org/pipermail/list-name/0000-Month/0123456",
|
||||
"https://mail.python.org/pipermail/list-name/0000-Month/0123456/",
|
||||
"https://mail.python.org/pipermail/list-name/0000-Month/",
|
||||
"https://mail.python.org/pipermail/list-name/0000-Month",
|
||||
"https://mail.python.org/archives/list/list-name@python.org/thread/abcXYZ123#anchor",
|
||||
"https://mail.python.org/archives/list/list-name@python.org/thread/abcXYZ123/#anchor",
|
||||
"https://mail.python.org/archives/list/list-name@python.org/message/abcXYZ123",
|
||||
"https://mail.python.org/archives/list/list-name@python.org/message/abcXYZ123/",
|
||||
"https://mail.python.org/archives/list/list-name@python.org/message/abcXYZ123#anchor",
|
||||
"https://mail.python.org/archives/list/list-name@python.org/message/abcXYZ123/#anchor",
|
||||
"https://mail.python.org/archives/list/list-name@python.org/spam/abcXYZ123",
|
||||
"https://mail.python.org/archives/list/list-name@python.org/spam/abcXYZ123/",
|
||||
],
|
||||
)
|
||||
def test_thread_checker_invalid(thread_url: str):
|
||||
warnings = [
|
||||
warning for (_, warning) in check_peps._thread(1, thread_url, "<Prefix>")
|
||||
]
|
||||
assert warnings == ["<Prefix> must be a valid thread URL"], warnings
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"thread_url",
|
||||
[
|
||||
"https://mail.python.org/archives/list/list-name@python.org/thread/abcXYZ123",
|
||||
"https://mail.python.org/archives/list/list-name@python.org/thread/abcXYZ123/",
|
||||
"https://mail.python.org/archives/list/list-name@python.org/message/abcXYZ123",
|
||||
"https://mail.python.org/archives/list/list-name@python.org/message/abcXYZ123/",
|
||||
"https://mail.python.org/archives/list/list-name@python.org/message/abcXYZ123#Anchor",
|
||||
"https://mail.python.org/archives/list/list-name@python.org/message/abcXYZ123/#Anchor",
|
||||
"https://mail.python.org/archives/list/list-name@python.org/message/abcXYZ123#Anchor123",
|
||||
"https://mail.python.org/archives/list/list-name@python.org/message/abcXYZ123/#Anchor123",
|
||||
],
|
||||
)
|
||||
def test_thread_checker_valid_allow_message(thread_url: str):
|
||||
warnings = [
|
||||
warning
|
||||
for (_, warning) in check_peps._thread(
|
||||
1, thread_url, "<Prefix>", allow_message=True
|
||||
)
|
||||
]
|
||||
assert warnings == [], warnings
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"thread_url",
|
||||
[
|
||||
"https://mail.python.org/archives/list/list-name@python.org/thread",
|
||||
"https://mail.python.org/archives/list/list-name@python.org/message",
|
||||
"https://mail.python.org/archives/list/list-name@python.org/thread/",
|
||||
"https://mail.python.org/archives/list/list-name@python.org/message/",
|
||||
"https://mail.python.org/archives/list/list-name@python.org/thread/abcXYZ123#anchor",
|
||||
"https://mail.python.org/archives/list/list-name@python.org/thread/abcXYZ123/#anchor",
|
||||
"https://mail.python.org/archives/list/list-name@python.org/message/#abcXYZ123",
|
||||
"https://mail.python.org/archives/list/list-name@python.org/message/#abcXYZ123/",
|
||||
"https://mail.python.org/archives/list/list-name@python.org/spam/abcXYZ123",
|
||||
"https://mail.python.org/archives/list/list-name@python.org/spam/abcXYZ123/",
|
||||
],
|
||||
)
|
||||
def test_thread_checker_invalid_allow_message(thread_url: str):
|
||||
warnings = [
|
||||
warning
|
||||
for (_, warning) in check_peps._thread(
|
||||
1, thread_url, "<Prefix>", allow_message=True
|
||||
)
|
||||
]
|
||||
assert warnings == ["<Prefix> must be a valid thread URL"], warnings
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"thread_url",
|
||||
[
|
||||
"list-name@python.org",
|
||||
"distutils-sig@python.org",
|
||||
"csv@python.org",
|
||||
"python-3000@python.org",
|
||||
"ipaddr-py-dev@googlegroups.com",
|
||||
"python-tulip@googlegroups.com",
|
||||
"https://discuss.python.org/t/thread-name/123456",
|
||||
"https://discuss.python.org/t/thread-name/123456/",
|
||||
"https://discuss.python.org/t/thread_name/123456",
|
||||
"https://discuss.python.org/t/thread_name/123456/",
|
||||
"https://discuss.python.org/t/123456/",
|
||||
"https://discuss.python.org/t/123456",
|
||||
],
|
||||
)
|
||||
def test_thread_checker_valid_discussions_to(thread_url: str):
|
||||
warnings = [
|
||||
warning
|
||||
for (_, warning) in check_peps._thread(
|
||||
1, thread_url, "<Prefix>", discussions_to=True
|
||||
)
|
||||
]
|
||||
assert warnings == [], warnings
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"thread_url",
|
||||
[
|
||||
"https://discuss.python.org/t/thread-name/123456/000",
|
||||
"https://discuss.python.org/t/thread-name/123456/000/",
|
||||
"https://discuss.python.org/t/thread_name/123456/000",
|
||||
"https://discuss.python.org/t/thread_name/123456/000/",
|
||||
"https://discuss.python.org/t/123456/000/",
|
||||
"https://discuss.python.org/t/12345656/000",
|
||||
"https://discuss.python.org/t/thread-name",
|
||||
"https://discuss.python.org/t/thread_name",
|
||||
"https://discuss.python.org/t/thread+name",
|
||||
],
|
||||
)
|
||||
def test_thread_checker_invalid_discussions_to(thread_url: str):
|
||||
warnings = [
|
||||
warning
|
||||
for (_, warning) in check_peps._thread(
|
||||
1, thread_url, "<Prefix>", discussions_to=True
|
||||
)
|
||||
]
|
||||
assert warnings == ["<Prefix> must be a valid thread URL"], warnings
|
||||
|
||||
|
||||
def test_thread_checker_allow_message_discussions_to():
|
||||
with pytest.raises(ValueError, match="cannot both be True"):
|
||||
list(
|
||||
check_peps._thread(
|
||||
1, "", "<Prefix>", allow_message=True, discussions_to=True
|
||||
)
|
||||
)
|
|
@ -1,27 +1,29 @@
|
|||
from pathlib import Path
|
||||
import datetime as dt
|
||||
|
||||
from pep_sphinx_extensions.pep_processor.transforms import pep_footer
|
||||
|
||||
from ...conftest import PEP_ROOT
|
||||
|
||||
|
||||
def test_add_source_link():
|
||||
out = pep_footer._add_source_link(Path("pep-0008.txt"))
|
||||
out = pep_footer._add_source_link(PEP_ROOT / "pep-0008.rst")
|
||||
|
||||
assert "https://github.com/python/peps/blob/main/pep-0008.txt" in str(out)
|
||||
assert "https://github.com/python/peps/blob/main/peps/pep-0008.rst" in str(out)
|
||||
|
||||
|
||||
def test_add_commit_history_info():
|
||||
out = pep_footer._add_commit_history_info(Path("pep-0008.txt"))
|
||||
out = pep_footer._add_commit_history_info(PEP_ROOT / "pep-0008.rst")
|
||||
|
||||
assert str(out).startswith(
|
||||
"<paragraph>Last modified: "
|
||||
'<reference refuri="https://github.com/python/peps/commits/main/pep-0008.txt">'
|
||||
'<reference refuri="https://github.com/python/peps/commits/main/pep-0008.rst">'
|
||||
)
|
||||
# A variable timestamp comes next, don't test that
|
||||
assert str(out).endswith("</reference></paragraph>")
|
||||
|
||||
|
||||
def test_add_commit_history_info_invalid():
|
||||
out = pep_footer._add_commit_history_info(Path("pep-not-found.txt"))
|
||||
out = pep_footer._add_commit_history_info(PEP_ROOT / "pep-not-found.rst")
|
||||
|
||||
assert str(out) == "<paragraph/>"
|
||||
|
||||
|
@ -31,4 +33,4 @@ def test_get_last_modified_timestamps():
|
|||
|
||||
assert len(out) >= 585
|
||||
# Should be a Unix timestamp and at least this
|
||||
assert out["pep-0008.txt"] >= 1643124055
|
||||
assert dt.datetime.fromisoformat(out["pep-0008"]).timestamp() >= 1643124055
|
||||
|
|
|
@ -18,7 +18,7 @@ from pep_sphinx_extensions.pep_zero_generator.constants import (
|
|||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"test_input, expected",
|
||||
("test_input", "expected"),
|
||||
[
|
||||
("my-mailing-list@example.com", "my-mailing-list@example.com"),
|
||||
("python-tulip@googlegroups.com", "https://groups.google.com/g/python-tulip"),
|
||||
|
@ -37,7 +37,7 @@ def test_generate_list_url(test_input, expected):
|
|||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"test_input, expected",
|
||||
("test_input", "expected"),
|
||||
[
|
||||
(
|
||||
"https://mail.python.org/pipermail/python-3000/2006-November/004190.html",
|
||||
|
@ -72,7 +72,7 @@ def test_process_pretty_url(test_input, expected):
|
|||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"test_input, expected",
|
||||
("test_input", "expected"),
|
||||
[
|
||||
(
|
||||
"https://example.com/",
|
||||
|
@ -94,7 +94,7 @@ def test_process_pretty_url_invalid(test_input, expected):
|
|||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"test_input, expected",
|
||||
("test_input", "expected"),
|
||||
[
|
||||
(
|
||||
"https://mail.python.org/pipermail/python-3000/2006-November/004190.html",
|
||||
|
@ -129,7 +129,7 @@ def test_make_link_pretty(test_input, expected):
|
|||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"test_input, expected",
|
||||
("test_input", "expected"),
|
||||
[
|
||||
(STATUS_ACCEPTED, "Normative proposal accepted for implementation"),
|
||||
(STATUS_ACTIVE, "Currently valid informational guidance, or an in-use process"),
|
||||
|
@ -155,7 +155,7 @@ def test_abbreviate_status_unknown():
|
|||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"test_input, expected",
|
||||
("test_input", "expected"),
|
||||
[
|
||||
(
|
||||
TYPE_INFO,
|
||||
|
|
|
@ -5,7 +5,7 @@ from pep_sphinx_extensions.pep_processor.transforms import pep_zero
|
|||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"test_input, expected",
|
||||
("test_input", "expected"),
|
||||
[
|
||||
(
|
||||
nodes.reference(
|
||||
|
|
|
@ -1,69 +0,0 @@
|
|||
import pytest
|
||||
|
||||
from pep_sphinx_extensions.pep_zero_generator import author
|
||||
from pep_sphinx_extensions.tests.utils import AUTHORS_OVERRIDES
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"test_input, expected",
|
||||
[
|
||||
(
|
||||
("First Last", "first@example.com"),
|
||||
author.Author(
|
||||
last_first="Last, First", nick="Last", email="first@example.com"
|
||||
),
|
||||
),
|
||||
(
|
||||
("Guido van Rossum", "guido@example.com"),
|
||||
author.Author(
|
||||
last_first="van Rossum, Guido (GvR)",
|
||||
nick="GvR",
|
||||
email="guido@example.com",
|
||||
),
|
||||
),
|
||||
(
|
||||
("Hugo van Kemenade", "hugo@example.com"),
|
||||
author.Author(
|
||||
last_first="van Kemenade, Hugo",
|
||||
nick="van Kemenade",
|
||||
email="hugo@example.com",
|
||||
),
|
||||
),
|
||||
(
|
||||
("Eric N. Vander Weele", "eric@example.com"),
|
||||
author.Author(
|
||||
last_first="Vander Weele, Eric N.",
|
||||
nick="Vander Weele",
|
||||
email="eric@example.com",
|
||||
),
|
||||
),
|
||||
(
|
||||
("Mariatta", "mariatta@example.com"),
|
||||
author.Author(
|
||||
last_first="Mariatta", nick="Mariatta", email="mariatta@example.com"
|
||||
),
|
||||
),
|
||||
(
|
||||
("First Last Jr.", "first@example.com"),
|
||||
author.Author(
|
||||
last_first="Last, First, Jr.", nick="Last", email="first@example.com"
|
||||
),
|
||||
),
|
||||
pytest.param(
|
||||
("First Last", "first at example.com"),
|
||||
author.Author(
|
||||
last_first="Last, First", nick="Last", email="first@example.com"
|
||||
),
|
||||
marks=pytest.mark.xfail,
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_parse_author_email(test_input, expected):
|
||||
out = author.parse_author_email(test_input, AUTHORS_OVERRIDES)
|
||||
|
||||
assert out == expected
|
||||
|
||||
|
||||
def test_parse_author_email_empty_name():
|
||||
with pytest.raises(ValueError, match="Name is empty!"):
|
||||
author.parse_author_email(("", "user@example.com"), AUTHORS_OVERRIDES)
|
|
@ -1,9 +1,6 @@
|
|||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from pep_sphinx_extensions.pep_zero_generator import parser
|
||||
from pep_sphinx_extensions.pep_zero_generator.author import Author
|
||||
from pep_sphinx_extensions.pep_zero_generator.constants import (
|
||||
STATUS_ACCEPTED,
|
||||
STATUS_ACTIVE,
|
||||
|
@ -18,35 +15,36 @@ from pep_sphinx_extensions.pep_zero_generator.constants import (
|
|||
TYPE_PROCESS,
|
||||
TYPE_STANDARDS,
|
||||
)
|
||||
from pep_sphinx_extensions.pep_zero_generator.errors import PEPError
|
||||
from pep_sphinx_extensions.tests.utils import AUTHORS_OVERRIDES
|
||||
from pep_sphinx_extensions.pep_zero_generator.parser import _Author
|
||||
|
||||
from ..conftest import PEP_ROOT
|
||||
|
||||
|
||||
def test_pep_repr():
|
||||
pep8 = parser.PEP(Path("pep-0008.txt"))
|
||||
pep8 = parser.PEP(PEP_ROOT / "pep-0008.rst")
|
||||
|
||||
assert repr(pep8) == "<PEP 0008 - Style Guide for Python Code>"
|
||||
|
||||
|
||||
def test_pep_less_than():
|
||||
pep8 = parser.PEP(Path("pep-0008.txt"))
|
||||
pep3333 = parser.PEP(Path("pep-3333.txt"))
|
||||
pep8 = parser.PEP(PEP_ROOT / "pep-0008.rst")
|
||||
pep3333 = parser.PEP(PEP_ROOT / "pep-3333.rst")
|
||||
|
||||
assert pep8 < pep3333
|
||||
|
||||
|
||||
def test_pep_equal():
|
||||
pep_a = parser.PEP(Path("pep-0008.txt"))
|
||||
pep_b = parser.PEP(Path("pep-0008.txt"))
|
||||
pep_a = parser.PEP(PEP_ROOT / "pep-0008.rst")
|
||||
pep_b = parser.PEP(PEP_ROOT / "pep-0008.rst")
|
||||
|
||||
assert pep_a == pep_b
|
||||
|
||||
|
||||
def test_pep_details(monkeypatch):
|
||||
pep8 = parser.PEP(Path("pep-0008.txt"))
|
||||
pep8 = parser.PEP(PEP_ROOT / "pep-0008.rst")
|
||||
|
||||
assert pep8.details == {
|
||||
"authors": "GvR, Warsaw, Coghlan",
|
||||
"authors": "Guido van Rossum, Barry Warsaw, Nick Coghlan",
|
||||
"number": 8,
|
||||
"shorthand": ":abbr:`PA (Process, Active)`",
|
||||
"title": "Style Guide for Python Code",
|
||||
|
@ -54,48 +52,43 @@ def test_pep_details(monkeypatch):
|
|||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"test_input, expected",
|
||||
("test_input", "expected"),
|
||||
[
|
||||
(
|
||||
"First Last <user@example.com>",
|
||||
[Author(last_first="Last, First", nick="Last", email="user@example.com")],
|
||||
[_Author(full_name="First Last", email="user@example.com")],
|
||||
),
|
||||
(
|
||||
"First Last",
|
||||
[Author(last_first="Last, First", nick="Last", email="")],
|
||||
),
|
||||
(
|
||||
"user@example.com (First Last)",
|
||||
[Author(last_first="Last, First", nick="Last", email="user@example.com")],
|
||||
[_Author(full_name="First Last", email="")],
|
||||
),
|
||||
pytest.param(
|
||||
"First Last <user at example.com>",
|
||||
[Author(last_first="Last, First", nick="Last", email="user@example.com")],
|
||||
[_Author(full_name="First Last", email="user@example.com")],
|
||||
marks=pytest.mark.xfail,
|
||||
),
|
||||
pytest.param(
|
||||
" , First Last,",
|
||||
{"First Last": ""},
|
||||
marks=pytest.mark.xfail(raises=ValueError),
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_parse_authors(test_input, expected):
|
||||
# Arrange
|
||||
dummy_object = parser.PEP(Path("pep-0160.txt"))
|
||||
|
||||
# Act
|
||||
out = parser._parse_authors(dummy_object, test_input, AUTHORS_OVERRIDES)
|
||||
out = parser._parse_author(test_input)
|
||||
|
||||
# Assert
|
||||
assert out == expected
|
||||
|
||||
|
||||
def test_parse_authors_invalid():
|
||||
|
||||
pep = parser.PEP(Path("pep-0008.txt"))
|
||||
|
||||
with pytest.raises(PEPError, match="no authors found"):
|
||||
parser._parse_authors(pep, "", AUTHORS_OVERRIDES)
|
||||
with pytest.raises(ValueError, match="Name is empty!"):
|
||||
assert parser._parse_author("")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"test_type, test_status, expected",
|
||||
("test_type", "test_status", "expected"),
|
||||
[
|
||||
(TYPE_INFO, STATUS_DRAFT, ":abbr:`I (Informational, Draft)`"),
|
||||
(TYPE_INFO, STATUS_ACTIVE, ":abbr:`IA (Informational, Active)`"),
|
||||
|
@ -113,7 +106,7 @@ def test_parse_authors_invalid():
|
|||
)
|
||||
def test_abbreviate_type_status(test_type, test_status, expected):
|
||||
# set up dummy PEP object and monkeypatch attributes
|
||||
pep = parser.PEP(Path("pep-0008.txt"))
|
||||
pep = parser.PEP(PEP_ROOT / "pep-0008.rst")
|
||||
pep.pep_type = test_type
|
||||
pep.status = test_status
|
||||
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
from pathlib import Path
|
||||
|
||||
from pep_sphinx_extensions.pep_zero_generator import parser, pep_index_generator
|
||||
|
||||
from ..conftest import PEP_ROOT
|
||||
|
||||
|
||||
def test_create_pep_json():
|
||||
peps = [parser.PEP(Path("pep-0008.txt"))]
|
||||
peps = [parser.PEP(PEP_ROOT / "pep-0008.rst")]
|
||||
|
||||
out = pep_index_generator.create_pep_json(peps)
|
||||
|
||||
|
|
|
@ -30,18 +30,18 @@ def test_pep_zero_writer_emit_title():
|
|||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"test_input, expected",
|
||||
("test_input", "expected"),
|
||||
[
|
||||
(
|
||||
"pep-9000.rst",
|
||||
{
|
||||
"Fussyreverend, Francis": "one@example.com",
|
||||
"Soulfulcommodore, Javier": "two@example.com",
|
||||
"Francis Fussyreverend": "one@example.com",
|
||||
"Javier Soulfulcommodore": "two@example.com",
|
||||
},
|
||||
),
|
||||
(
|
||||
"pep-9001.rst",
|
||||
{"Fussyreverend, Francis": "", "Soulfulcommodore, Javier": ""},
|
||||
{"Francis Fussyreverend": "", "Javier Soulfulcommodore": ""},
|
||||
),
|
||||
],
|
||||
)
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
PEP:9002
|
||||
Title: Nobody expects the example PEP!
|
||||
Author: Cardinal Ximénez <Cardinal.Ximenez@spanish.inquisition>,
|
||||
Cardinal Biggles
|
||||
Cardinal Fang
|
||||
Version: 4.0
|
||||
Python-Version: 4.0
|
||||
Sponsor:
|
||||
Sponsor:
|
||||
Horse-Guards: Parade
|
||||
Created: 1-Jan-1989
|
||||
BDFL-Delegate: Barry!
|
||||
Status: Draught
|
||||
Topic: Inquisiting, Governance, Governance, packaging
|
||||
Content-Type: video/quicktime
|
||||
Requires: 0020,1,2,3, 7, 8
|
||||
Discussions-To: MR ALBERT SPIM, I,OOO,OO8 LONDON ROAD, OXFORD
|
||||
Post-History: `2-Feb-2000 <FLIGHT LT. & PREBENDARY ETHEL MORRIS; THE DIMPLES; THAXTED; NR BUENOS AIRES>`__
|
||||
`3-Mar-2001 <The Royal Frog Trampling Institute; 16 Rayners Lane; London>`__
|
||||
Resolution:
|
||||
|
||||
|
||||
https://peps.python.org/pep-9002.html
|
|
@ -1,6 +0,0 @@
|
|||
AUTHORS_OVERRIDES = {
|
||||
"Guido van Rossum": {
|
||||
"Surname First": "van Rossum, Guido (GvR)",
|
||||
"Name Reference": "GvR",
|
||||
},
|
||||
}
|
|
@ -3,10 +3,12 @@
|
|||
|
||||
"""Configuration for building PEPs using Sphinx."""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
sys.path.append(str(Path("pep_sphinx_extensions").absolute()))
|
||||
_ROOT = Path(__file__).resolve().parent.parent
|
||||
sys.path.append(os.fspath(_ROOT))
|
||||
|
||||
# -- Project information -----------------------------------------------------
|
||||
|
||||
|
@ -25,7 +27,6 @@ extensions = [
|
|||
# The file extensions of source files. Sphinx uses these suffixes as sources.
|
||||
source_suffix = {
|
||||
".rst": "pep",
|
||||
".txt": "pep",
|
||||
}
|
||||
|
||||
# List of patterns (relative to source dir) to ignore when looking for source files.
|
||||
|
@ -34,7 +35,6 @@ include_patterns = [
|
|||
"contents.rst",
|
||||
# PEP files
|
||||
"pep-????.rst",
|
||||
"pep-????.txt",
|
||||
# PEP ancillary files
|
||||
"pep-????/*.rst",
|
||||
# Documentation
|
||||
|
@ -45,10 +45,14 @@ exclude_patterns = [
|
|||
"pep-0012/pep-NNNN.rst",
|
||||
]
|
||||
|
||||
# Warn on missing references
|
||||
nitpicky = True
|
||||
|
||||
# Intersphinx configuration
|
||||
intersphinx_mapping = {
|
||||
'python': ('https://docs.python.org/3/', None),
|
||||
'packaging': ('https://packaging.python.org/en/latest/', None),
|
||||
'devguide': ('https://devguide.python.org/', None),
|
||||
'py3.11': ('https://docs.python.org/3.11/', None),
|
||||
'py3.12': ('https://docs.python.org/3.12/', None),
|
||||
}
|
||||
|
@ -56,11 +60,13 @@ intersphinx_disabled_reftypes = []
|
|||
|
||||
# -- Options for HTML output -------------------------------------------------
|
||||
|
||||
_PSE_PATH = _ROOT / "pep_sphinx_extensions"
|
||||
|
||||
# HTML output settings
|
||||
html_math_renderer = "maths_to_html" # Maths rendering
|
||||
|
||||
# Theme settings
|
||||
html_theme_path = ["pep_sphinx_extensions"]
|
||||
html_theme_path = [os.fspath(_PSE_PATH)]
|
||||
html_theme = "pep_theme" # The actual theme directory (child of html_theme_path)
|
||||
html_use_index = False # Disable index (we use PEP 0)
|
||||
html_style = "" # must be defined here or in theme.conf, but is unused
|
||||
|
@ -68,4 +74,4 @@ html_permalinks = False # handled in the PEPContents transform
|
|||
html_baseurl = "https://peps.python.org" # to create the CNAME file
|
||||
gettext_auto_build = False # speed-ups
|
||||
|
||||
templates_path = ["pep_sphinx_extensions/pep_theme/templates"] # Theme template relative paths from `confdir`
|
||||
templates_path = [os.fspath(_PSE_PATH / "pep_theme" / "templates")] # Theme template relative paths from `confdir`
|
|
@ -14,6 +14,5 @@ This is an internal Sphinx page; please go to the :doc:`PEP Index <pep-0000>`.
|
|||
:glob:
|
||||
:caption: PEP Table of Contents (needed for Sphinx):
|
||||
|
||||
docs/*
|
||||
pep-*
|
||||
topic/*
|
|
@ -185,8 +185,11 @@ corrected by the editors).
|
|||
The standard PEP workflow is:
|
||||
|
||||
* You, the PEP author, fork the `PEP repository`_, and create a file named
|
||||
``pep-9999.rst`` that contains your new PEP. Use "9999" as your draft PEP
|
||||
number.
|
||||
:file:`pep-{NNNN}.rst` that contains your new PEP. :samp:`{NNNN}` should be the next
|
||||
available PEP number not used by a published or in-PR PEP.
|
||||
|
||||
* In the "PEP:" header field, enter the PEP number that matches your filename
|
||||
as your draft PEP number.
|
||||
|
||||
* In the "Type:" header field, enter "Standards Track",
|
||||
"Informational", or "Process" as appropriate, and for the "Status:"
|
||||
|
@ -204,7 +207,7 @@ The standard PEP workflow is:
|
|||
It also provides a complete introduction to reST markup that is used
|
||||
in PEPs. Approval criteria are:
|
||||
|
||||
* It sound and complete. The ideas must make technical sense. The
|
||||
* It is sound and complete. The ideas must make technical sense. The
|
||||
editors do not consider whether they seem likely to be accepted.
|
||||
* The title accurately describes the content.
|
||||
* The PEP's language (spelling, grammar, sentence structure, etc.)
|
||||
|
@ -293,7 +296,7 @@ pointing to this new thread.
|
|||
|
||||
If it is not chosen as the discussion venue,
|
||||
a brief announcement post should be made to the `PEPs category`_
|
||||
with at least a link to the rendered PEP and the `Discussions-To` thread
|
||||
with at least a link to the rendered PEP and the ``Discussions-To`` thread
|
||||
when the draft PEP is committed to the repository
|
||||
and if a major-enough change is made to trigger a new thread.
|
||||
|
||||
|
@ -791,12 +794,11 @@ problem, ask the author(s) to use :pep:`12` as a template and resubmit.
|
|||
|
||||
Once the PEP is ready for the repository, a PEP editor will:
|
||||
|
||||
* Assign a PEP number (almost always just the next available number,
|
||||
but sometimes it's a special/joke number, like 666 or 3141).
|
||||
(Clarification: For Python 3, numbers in the 3000s were used for
|
||||
Py3k-specific proposals. But now that all new features go into
|
||||
Python 3 only, the process is back to using numbers in the 100s again.
|
||||
Remember that numbers below 100 are meta-PEPs.)
|
||||
* Check that the author has selected a valid PEP number or assign them a
|
||||
number if they have not (almost always just the next available number, but
|
||||
sometimes it's a special/joke number, like 666 or 3141).
|
||||
|
||||
Remember that numbers below 100 are meta-PEPs.
|
||||
|
||||
* Check that the author has correctly labeled the PEP's type
|
||||
("Standards Track", "Informational", or "Process"), and marked its
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
|
@ -45,7 +45,7 @@ Prohibitions
|
|||
|
||||
Bug fix releases are required to adhere to the following restrictions:
|
||||
|
||||
1. There must be zero syntax changes. All `.pyc` and `.pyo` files must
|
||||
1. There must be zero syntax changes. All ``.pyc`` and ``.pyo`` files must
|
||||
work (no regeneration needed) with all bugfix releases forked off
|
||||
from a major release.
|
||||
|
|
@ -490,7 +490,7 @@ Avoid extraneous whitespace in the following situations:
|
|||
# Wrong:
|
||||
ham[lower + offset:upper + offset]
|
||||
ham[1: 9], ham[1 :9], ham[1:9 :3]
|
||||
ham[lower : : upper]
|
||||
ham[lower : : step]
|
||||
ham[ : upper]
|
||||
|
||||
- Immediately before the open parenthesis that starts the argument
|
|
@ -8,7 +8,7 @@ Content-Type: text/x-rst
|
|||
Created: 07-Jul-2002
|
||||
Post-History: `18-Aug-2007 <https://mail.python.org/archives/list/python-dev@python.org/thread/DSSGXU5LBCMKYMZBRVB6RF3YAB6ST5AV/>`__,
|
||||
`14-May-2014 <https://mail.python.org/archives/list/python-dev@python.org/thread/T7WTUJ6TD3IGYGWV3M4PHJWNLM2WPZAW/>`__,
|
||||
`20-Feb-2015 <https://mail.python.org/archives/list/python-dev@python.org/thread/OEQHRR2COYZDL6LZ42RBZOMIUB32WI34/#L3K7IKGVT4ND45SKAJPJ3Q2ADVK5KP52>`__,
|
||||
`20-Feb-2015 <https://mail.python.org/archives/list/python-dev@python.org/thread/OEQHRR2COYZDL6LZ42RBZOMIUB32WI34/>`__,
|
||||
`10-Mar-2022 <https://mail.python.org/archives/list/python-committers@python.org/thread/K757345KX6W5ZLTWYBUXOXQTJJTL7GW5/>`__,
|
||||
|
||||
|
||||
|
@ -42,7 +42,7 @@ platform to be considered supported by CPython as well as providing a
|
|||
procedure to remove code for platforms with few or no CPython
|
||||
users.
|
||||
|
||||
This PEP also lists what plaforms *are* supported by the CPython
|
||||
This PEP also lists what platforms *are* supported by the CPython
|
||||
interpreter. This lets people know what platforms are directly
|
||||
supported by the CPython development team.
|
||||
|
|
@ -60,14 +60,10 @@ Once you've decided which type of PEP yours is going to be, follow the
|
|||
directions below.
|
||||
|
||||
- Make a copy of this file (the ``.rst`` file, **not** the HTML!) and
|
||||
perform the following edits. Name the new file ``pep-9999.rst`` if
|
||||
you don't yet have a PEP number assignment, or ``pep-NNNN.rst`` if
|
||||
you do. Those with push permissions are welcome to claim the next
|
||||
available number (ignoring the special blocks 3000 and 8000, and a
|
||||
handful of special allocations - currently 666, 754, and 801) and
|
||||
push it directly.
|
||||
perform the following edits. Name the new file :file:`pep-{NNNN}.rst`, using
|
||||
the next available number (not used by a published or in-PR PEP).
|
||||
|
||||
- Replace the "PEP: 12" header with "PEP: 9999" or "PEP: NNNN",
|
||||
- Replace the "PEP: 12" header with "PEP: NNNN",
|
||||
matching the file name. Note that the file name should be padded with
|
||||
zeros (eg ``pep-0012.rst``), but the header should not (``PEP: 12``).
|
||||
|
||||
|
@ -158,7 +154,7 @@ directions below.
|
|||
non-inline link targets referenced by the text.
|
||||
|
||||
- Run ``./build.py`` to ensure the PEP is rendered without errors,
|
||||
and check that the output in ``build/pep-9999.html`` looks as you intend.
|
||||
and check that the output in :file:`build/pep-{NNNN}.html` looks as you intend.
|
||||
|
||||
- Create a pull request against the `PEPs repository`_.
|
||||
|
|
@ -78,6 +78,11 @@ Here's a hopefully-complete list.
|
|||
* A subscription to the super secret release manager mailing list, which may
|
||||
or may not be called ``python-cabal``. Bug Barry about this.
|
||||
|
||||
* A ``@python.org`` email address that you will use to sign your releases
|
||||
with. Ask ``postmaster@`` for an address; you can either get a full
|
||||
account, or a redirecting alias + SMTP credentials to send email from
|
||||
this address that looks legit to major email providers.
|
||||
|
||||
Types of Releases
|
||||
=================
|
||||
|
||||
|
@ -121,9 +126,10 @@ release. The roles and their current experts are:
|
|||
|
||||
* RM = Release Manager
|
||||
|
||||
- Łukasz Langa <lukasz@python.org> (Central Europe)
|
||||
- Ned Deily <nad@python.org> (US)
|
||||
- Thomas Wouters <thomas@python.org> (NL)
|
||||
- Pablo Galindo Salgado <pablogsal@python.org> (UK)
|
||||
- Łukasz Langa <lukasz@python.org> (PL)
|
||||
- Ned Deily <nad@python.org> (US)
|
||||
|
||||
* WE = Windows - Steve Dower <steve.dower@python.org>
|
||||
* ME = Mac - Ned Deily <nad@python.org> (US)
|
||||
|
@ -206,7 +212,7 @@ to perform some manual editing steps.
|
|||
within it (called the "release clone" from now on). You can use the same
|
||||
GitHub fork you use for cpython development. Using the standard setup
|
||||
recommended in the Python Developer's Guide, your fork would be referred
|
||||
to as `origin` and the standard cpython repo as `upstream`. You will
|
||||
to as ``origin`` and the standard cpython repo as ``upstream``. You will
|
||||
use the branch on your fork to do the release engineering work, including
|
||||
tagging the release, and you will use it to share with the other experts
|
||||
for making the binaries.
|
||||
|
@ -296,7 +302,7 @@ to perform some manual editing steps.
|
|||
|
||||
$ .../release-tools/release.py --tag X.Y.ZaN
|
||||
|
||||
This executes a `git tag` command with the `-s` option so that the
|
||||
This executes a ``git tag`` command with the ``-s`` option so that the
|
||||
release tag in the repo is signed with your gpg key. When prompted
|
||||
choose the private key you use for signing release tarballs etc.
|
||||
|
||||
|
@ -321,6 +327,10 @@ to perform some manual editing steps.
|
|||
tarballs and signatures in a subdirectory called ``X.Y.ZaN/src``, and the
|
||||
built docs in ``X.Y.ZaN/docs`` (for **final** releases).
|
||||
|
||||
Note that the script will sign your release with Sigstore. Please use
|
||||
your **@python.org** email address for this. See here for more information:
|
||||
https://www.python.org/download/sigstore/.
|
||||
|
||||
- Now you want to perform the very important step of checking the
|
||||
tarball you just created, to make sure a completely clean,
|
||||
virgin build passes the regression test. Here are the best
|
||||
|
@ -528,7 +538,7 @@ the main repo.
|
|||
do some post-merge cleanup. Check the top-level ``README.rst``
|
||||
and ``include/patchlevel.h`` files to ensure they now reflect
|
||||
the desired post-release values for on-going development.
|
||||
The patchlevel should be the release tag with a `+`.
|
||||
The patchlevel should be the release tag with a ``+``.
|
||||
Also, if you cherry-picked changes from the standard release
|
||||
branch into the release engineering branch for this release,
|
||||
you will now need to manual remove each blurb entry from
|
||||
|
@ -536,8 +546,8 @@ the main repo.
|
|||
into the release you are working on since that blurb entry
|
||||
is now captured in the merged x.y.z.rst file for the new
|
||||
release. Otherwise, the blurb entry will appear twice in
|
||||
the `changelog.html` file, once under `Python next` and again
|
||||
under `x.y.z`.
|
||||
the ``changelog.html`` file, once under ``Python next`` and again
|
||||
under ``x.y.z``.
|
||||
|
||||
- Review and commit these changes::
|
||||
|
||||
|
@ -689,6 +699,11 @@ with RevSys.)
|
|||
(It's best to update add-to-pydotorg.py when file types
|
||||
are removed, too.)
|
||||
|
||||
The script will also sign any remaining files that were not
|
||||
signed with Sigstore until this point. Again, if this happens,
|
||||
do use your @python.org address for this process. More info:
|
||||
https://www.python.org/download/sigstore/
|
||||
|
||||
- In case the CDN already cached a version of the Downloads page
|
||||
without the files present, you can invalidate the cache using::
|
||||
|
||||
|
@ -697,19 +712,19 @@ with RevSys.)
|
|||
- If this is a **final** release:
|
||||
|
||||
- Add the new version to the *Python Documentation by Version*
|
||||
page `https://www.python.org/doc/versions/` and
|
||||
page ``https://www.python.org/doc/versions/`` and
|
||||
remove the current version from any 'in development' section.
|
||||
|
||||
- For X.Y.Z, edit all the previous X.Y releases' page(s) to
|
||||
point to the new release. This includes the content field of the
|
||||
`Downloads -> Releases` entry for the release::
|
||||
``Downloads -> Releases`` entry for the release::
|
||||
|
||||
Note: Python x.y.m has been superseded by
|
||||
`Python x.y.n </downloads/release/python-xyn/>`_.
|
||||
|
||||
And, for those releases having separate release page entries
|
||||
(phasing these out?), update those pages as well,
|
||||
e.g. `download/releases/x.y.z`::
|
||||
e.g. ``download/releases/x.y.z``::
|
||||
|
||||
Note: Python x.y.m has been superseded by
|
||||
`Python x.y.n </download/releases/x.y.n/>`_.
|
||||
|
@ -893,8 +908,8 @@ else does them. Some of those tasks include:
|
|||
|
||||
- Remove the release from the list of "Active Python Releases" on the Downloads
|
||||
page. To do this, log in to the admin page for python.org, navigate to Boxes,
|
||||
and edit the `downloads-active-releases` entry. Simply strip out the relevant
|
||||
paragraph of HTML for your release. (You'll probably have to do the `curl -X PURGE`
|
||||
and edit the ``downloads-active-releases`` entry. Simply strip out the relevant
|
||||
paragraph of HTML for your release. (You'll probably have to do the ``curl -X PURGE``
|
||||
trick to purge the cache if you want to confirm you made the change correctly.)
|
||||
|
||||
- Add retired notice to each release page on python.org for the retired branch.
|
|
@ -46,8 +46,8 @@ Lockstep For-Loops
|
|||
Lockstep for-loops are non-nested iterations over two or more
|
||||
sequences, such that at each pass through the loop, one element from
|
||||
each sequence is taken to compose the target. This behavior can
|
||||
already be accomplished in Python through the use of the map() built-
|
||||
in function::
|
||||
already be accomplished in Python through the use of the map() built-in
|
||||
function::
|
||||
|
||||
>>> a = (1, 2, 3)
|
||||
>>> b = (4, 5, 6)
|
|
@ -185,8 +185,8 @@ Implementation Strategy
|
|||
=======================
|
||||
|
||||
The implementation of weak references will include a list of
|
||||
reference containers that must be cleared for each weakly-
|
||||
referencable object. If the reference is from a weak dictionary,
|
||||
reference containers that must be cleared for each weakly-referencable
|
||||
object. If the reference is from a weak dictionary,
|
||||
the dictionary entry is cleared first. Then, any associated
|
||||
callback is called with the object passed as a parameter. Once
|
||||
all callbacks have been called, the object is finalized and
|
|
@ -12,9 +12,9 @@ Post-History:
|
|||
Abstract
|
||||
========
|
||||
|
||||
This PEP proposes a redesign and re-implementation of the multi-
|
||||
dimensional array module, Numeric, to make it easier to add new
|
||||
features and functionality to the module. Aspects of Numeric 2
|
||||
This PEP proposes a redesign and re-implementation of the
|
||||
multi-dimensional array module, Numeric, to make it easier to add
|
||||
new features and functionality to the module. Aspects of Numeric 2
|
||||
that will receive special attention are efficient access to arrays
|
||||
exceeding a gigabyte in size and composed of inhomogeneous data
|
||||
structures or records. The proposed design uses four Python
|
||||
|
@ -128,8 +128,8 @@ Some planned features are:
|
|||
automatically handle alignment and representational issues
|
||||
when data is accessed or operated on. There are two
|
||||
approaches to implementing records; as either a derived array
|
||||
class or a special array type, depending on your point-of-
|
||||
view. We defer this discussion to the Open Issues section.
|
||||
class or a special array type, depending on your point-of-view.
|
||||
We defer this discussion to the Open Issues section.
|
||||
|
||||
|
||||
2. Additional array types
|
||||
|
@ -265,8 +265,8 @@ The design of Numeric 2 has four primary classes:
|
|||
_ufunc.compute(slice, data, func, swap, conv)
|
||||
|
||||
The 'func' argument is a CFuncObject, while the 'swap' and 'conv'
|
||||
arguments are lists of CFuncObjects for those arrays needing pre-
|
||||
or post-processing, otherwise None is used. The data argument is
|
||||
arguments are lists of CFuncObjects for those arrays needing pre- or
|
||||
post-processing, otherwise None is used. The data argument is
|
||||
a list of buffer objects, and the slice argument gives the number
|
||||
of iterations for each dimension along with the buffer offset and
|
||||
step size for each array and each dimension.
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue