diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 000000000..de60a5f44 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,2 @@ +# Notify @EWDurbin for all opened Issues and Pull Requests +* @EWDurbin @JacobCoffee diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 000000000..fa82b4297 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,19 @@ + +#### Description + +- + + +#### Closes + +- + diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7d0becf27..ed08f4b7f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,7 +5,7 @@ jobs: runs-on: ubuntu-latest services: postgres: - image: postgres:10.1 + image: postgres:15.3 env: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres @@ -31,7 +31,7 @@ jobs: sudo dpkg -i pandoc-2.17.1.1-1-amd64.deb - uses: actions/setup-python@v5 with: - python-version: 3.9.16 + python-version-file: '.python-version' - name: Cache Python dependencies uses: actions/cache@v4 env: diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml new file mode 100644 index 000000000..3207b964e --- /dev/null +++ b/.github/workflows/static.yml @@ -0,0 +1,29 @@ +name: Check collectstatic +on: [push, pull_request] +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version-file: '.python-version' + - name: Cache Python dependencies + uses: actions/cache@v4 + env: + cache-name: pythondotorg-cache-pip + with: + path: ~/.cache/pip + key: ${{ runner.os }}-${{ github.job }}-${{ env.cache-name }}-${{ hashFiles('requirements.txt', '*-requirements.txt') }} + restore-keys: | + ${{ runner.os }}-${{ github.job }}-${{ env.cache-name }}- + ${{ runner.os }}-${{ github.job }}- + ${{ runner.os }}- + - name: Install Python dependencies + run: | + pip install -U pip setuptools wheel + pip install -r requirements.txt -r prod-requirements.txt + - name: Run Tests + run: | + DJANGO_SETTINGS_MODULE=pydotorg.settings.static python manage.py collectstatic --noinput diff --git a/.python-version b/.python-version index 9f3d4c178..35f236d6e 100644 --- a/.python-version +++ b/.python-version @@ -1 +1 @@ -3.9.16 +3.12.6 diff --git a/Dockerfile b/Dockerfile index a3c351f5e..c701cd76c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.9-bookworm +FROM python:3.12.6-bookworm ENV PYTHONUNBUFFERED=1 ENV PYTHONDONTWRITEBYTECODE=1 diff --git a/Dockerfile.cabotage b/Dockerfile.cabotage index d96e002a7..9bc9d27ad 100644 --- a/Dockerfile.cabotage +++ b/Dockerfile.cabotage @@ -1,4 +1,4 @@ -FROM python:3.9-bullseye +FROM python:3.12.6-bookworm COPY --from=ewdurbin/nginx-static:1.25.x /usr/bin/nginx /usr/bin/nginx ENV PYTHONUNBUFFERED=1 ENV PYTHONDONTWRITEBYTECODE=1 diff --git a/Makefile b/Makefile index dc296feb4..bd4291bbe 100644 --- a/Makefile +++ b/Makefile @@ -4,13 +4,13 @@ default: @$(MAKE) -pRrq -f $(lastword $(MAKEFILE_LIST)) : 2>/dev/null\ | awk -v RS= -F: '/^# File/,/^# Finished Make data base/ {if ($$1 !~ "^[#.]") {print $$1}}'\ | sort\ - | egrep -v -e '^[^[:alnum:]]' -e '^$@$$' + | grep -E -v -e '^[^[:alnum:]]' -e '^$@$$' @echo @exit 1 .state/docker-build-web: Dockerfile dev-requirements.txt base-requirements.txt # Build web container for this project - docker-compose build --force-rm web + docker compose build --force-rm web # Mark the state so we don't rebuild this needlessly. mkdir -p .state && touch .state/docker-build-web @@ -24,35 +24,35 @@ default: .state/db-initialized: .state/docker-build-web .state/db-migrated # Load all fixtures - docker-compose run --rm web ./manage.py loaddata fixtures/*.json + docker compose run --rm web ./manage.py loaddata fixtures/*.json # Mark the state so we don't rebuild this needlessly. mkdir -p .state && touch .state/db-initialized serve: .state/db-initialized - docker-compose up --remove-orphans + docker compose up --remove-orphans migrations: .state/db-initialized # Run Django makemigrations - docker-compose run --rm web ./manage.py makemigrations + docker compose run --rm web ./manage.py makemigrations migrate: .state/docker-build-web # Run Django migrate - docker-compose run --rm web ./manage.py migrate + docker compose run --rm web ./manage.py migrate manage: .state/db-initialized # Run Django manage to accept arbitrary arguments - docker-compose run --rm web ./manage.py $(filter-out $@,$(MAKECMDGOALS)) + docker compose run --rm web ./manage.py $(filter-out $@,$(MAKECMDGOALS)) shell: .state/db-initialized - docker-compose run --rm web ./manage.py shell + docker compose run --rm web ./manage.py shell clean: - docker-compose down -v + docker compose down -v rm -f .state/docker-build-web .state/db-initialized .state/db-migrated test: .state/db-initialized - docker-compose run --rm web ./manage.py test + docker compose run --rm web ./manage.py test docker_shell: .state/db-initialized - docker-compose run --rm web /bin/bash + docker compose run --rm web /bin/bash diff --git a/README.md b/README.md index fc59b7cdf..caa261e07 100644 --- a/README.md +++ b/README.md @@ -5,19 +5,20 @@ ### General information -This is the repository and issue tracker for [python.org](https://www.python.org). -The repository for CPython itself is at https://github.com/python/cpython, and the -issue tracker is at https://github.com/python/cpython/issues/. +This is the repository and issue tracker for [python.org](https://www.python.org). -Issues related to [Python's documentation](https://docs.python.org) can be filed in -https://github.com/python/cpython/issues/. +> [!NOTE] +> The repository for CPython itself is at https://github.com/python/cpython, and the +> issue tracker is at https://github.com/python/cpython/issues/. +> +> Similarly, issues related to [Python's documentation](https://docs.python.org) can be filed in +> https://github.com/python/cpython/issues/. ### Contributing * Source code: https://github.com/python/pythondotorg * Issue tracker: https://github.com/python/pythondotorg/issues -* Documentation: https://pythondotorg.readthedocs.org/ +* Documentation: https://pythondotorg.readthedocs.io/ * Mailing list: [pydotorg-www](https://mail.python.org/mailman/listinfo/pydotorg-www) * IRC: `#pydotorg` on Freenode -* Staging site: https://staging.python.org/ (`main` branch) * License: Apache License diff --git a/banners/__init__.py b/banners/__init__.py index 010b54570..e69de29bb 100644 --- a/banners/__init__.py +++ b/banners/__init__.py @@ -1 +0,0 @@ -default_app_config = 'banners.apps.BannersAppConfig' diff --git a/base-requirements.txt b/base-requirements.txt index 07f0f5a3f..4f9c0aa39 100644 --- a/base-requirements.txt +++ b/base-requirements.txt @@ -1,53 +1,52 @@ dj-database-url==0.5.0 -django-pipeline==2.0.6 -django-sitetree==1.18.0 +django-pipeline==3.0.0 # 3.0.0 is first version that supports Django 4.2 +django-sitetree==1.18.0 # >=1.17.1 is (?) first version that supports Django 4.2 django-apptemplates==1.5 django-admin-interface==0.24.2 django-translation-aliases==0.1.0 -Django==2.2.28 -docutils==0.12 -Markdown==3.3.4 +Django==4.2.16 +docutils==0.21.2 +Markdown==3.7 cmarkgfm==0.6.0 -Pillow==9.4.0 -psycopg2-binary==2.8.6 +Pillow==10.4.0 +psycopg2-binary==2.9.9 python3-openid==3.2.0 python-decouple==3.4 # lxml used by BeautifulSoup. lxml==5.2.2 cssselect==1.1.0 -feedparser==6.0.8 -beautifulsoup4==4.11.2 +feedparser==6.0.11 +beautifulsoup4==4.12.3 icalendar==4.0.7 chardet==4.0.0 celery[redis]==5.3.6 django-celery-beat==2.5.0 # TODO: We may drop 'django-imagekit' completely. -django-imagekit==4.0.2 +django-imagekit==5.0 # 5.0 is first version that supports Django 4.2 django-haystack==3.2.1 elasticsearch>=7,<8 # TODO: 0.14.0 only supports Django 1.8 and 1.11. -django-tastypie==0.14.3 +django-tastypie==0.14.6 # 0.14.6 is first version that supports Django 4.2 pytz==2021.1 python-dateutil==2.8.2 requests[security]>=2.26.0 -django-honeypot==1.0.1 -django-markupfield==2.0.0 -django-markupfield-helpers==0.1.1 +django-honeypot==1.0.4 # 1.0.4 is first version that supports Django 4.2 +django-markupfield==2.0.1 -django-allauth==0.50.0 +django-allauth==64.2.1 django-waffle==2.2.1 -djangorestframework==3.12.2 +djangorestframework==3.14.0 # 3.14.0 is first version that supports Django 4.1, 4.2 support hasnt been "released" django-filter==2.4.0 django-ordered-model==3.4.3 -django-widget-tweaks==1.4.8 +django-widget-tweaks==1.5.0 django-countries==7.2.1 num2words==0.5.10 -django-polymorphic==3.0.0 +django-polymorphic==3.1.0 # 3.1.0 is first version that supports Django 4.0, unsure if it fully supports 4.2 sorl-thumbnail==12.7.0 django-extensions==3.1.4 django-import-export==2.7.1 diff --git a/blogs/__init__.py b/blogs/__init__.py index 620291c46..e69de29bb 100644 --- a/blogs/__init__.py +++ b/blogs/__init__.py @@ -1 +0,0 @@ -default_app_config = 'blogs.apps.BlogsAppConfig' diff --git a/blogs/admin.py b/blogs/admin.py index 055431ae9..e5fea1cfb 100644 --- a/blogs/admin.py +++ b/blogs/admin.py @@ -10,11 +10,13 @@ class BlogEntryAdmin(admin.ModelAdmin): date_hierarchy = 'pub_date' actions = ['sync_new_entries'] + @admin.action( + description="Sync new blog entries" + ) def sync_new_entries(self, request, queryset): call_command('update_blogs') self.message_user(request, "Blog entries updated.") - sync_new_entries.short_description = "Sync new blog entries" @admin.register(FeedAggregate) diff --git a/blogs/migrations/0003_alter_relatedblog_creator_and_more.py b/blogs/migrations/0003_alter_relatedblog_creator_and_more.py new file mode 100644 index 000000000..9e71084a8 --- /dev/null +++ b/blogs/migrations/0003_alter_relatedblog_creator_and_more.py @@ -0,0 +1,26 @@ +# Generated by Django 4.2.11 on 2024-09-05 17:10 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('blogs', '0002_remove_translations_and_contributors'), + ] + + operations = [ + migrations.AlterField( + model_name='relatedblog', + name='creator', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s_creator', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='relatedblog', + name='last_modified_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s_modified', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/blogs/parser.py b/blogs/parser.py index 8ac8dc684..fd5e4b54d 100644 --- a/blogs/parser.py +++ b/blogs/parser.py @@ -3,7 +3,7 @@ from django.conf import settings from django.template.loader import render_to_string -from django.utils.timezone import make_aware, utc +from django.utils.timezone import make_aware from boxes.models import Box from .models import BlogEntry, Feed @@ -16,7 +16,7 @@ def get_all_entries(feed_url): for e in d['entries']: published = make_aware( - datetime.datetime(*e['published_parsed'][:7]), timezone=utc + datetime.datetime(*e['published_parsed'][:7]), timezone=datetime.timezone.utc ) entry = { diff --git a/boxes/__init__.py b/boxes/__init__.py index 401a83d2e..e69de29bb 100644 --- a/boxes/__init__.py +++ b/boxes/__init__.py @@ -1 +0,0 @@ -default_app_config = 'boxes.apps.BoxesAppConfig' diff --git a/boxes/migrations/0004_alter_box_creator_alter_box_last_modified_by.py b/boxes/migrations/0004_alter_box_creator_alter_box_last_modified_by.py new file mode 100644 index 000000000..3829382ec --- /dev/null +++ b/boxes/migrations/0004_alter_box_creator_alter_box_last_modified_by.py @@ -0,0 +1,26 @@ +# Generated by Django 4.2.11 on 2024-09-05 17:10 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('boxes', '0003_auto_20171101_2138'), + ] + + operations = [ + migrations.AlterField( + model_name='box', + name='creator', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s_creator', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='box', + name='last_modified_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s_modified', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/cms/__init__.py b/cms/__init__.py index 92d29195c..e69de29bb 100644 --- a/cms/__init__.py +++ b/cms/__init__.py @@ -1 +0,0 @@ -default_app_config = 'cms.apps.CmsAppConfig' diff --git a/codesamples/__init__.py b/codesamples/__init__.py index f51a992fa..e69de29bb 100644 --- a/codesamples/__init__.py +++ b/codesamples/__init__.py @@ -1 +0,0 @@ -default_app_config = 'codesamples.apps.CodesamplesAppConfig' diff --git a/codesamples/migrations/0004_alter_codesample_creator_and_more.py b/codesamples/migrations/0004_alter_codesample_creator_and_more.py new file mode 100644 index 000000000..0b29294ad --- /dev/null +++ b/codesamples/migrations/0004_alter_codesample_creator_and_more.py @@ -0,0 +1,26 @@ +# Generated by Django 4.2.11 on 2024-09-05 17:10 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('codesamples', '0003_auto_20170821_2000'), + ] + + operations = [ + migrations.AlterField( + model_name='codesample', + name='creator', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s_creator', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='codesample', + name='last_modified_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s_modified', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/codesamples/tests.py b/codesamples/tests.py index 73c85c164..7ddf51119 100644 --- a/codesamples/tests.py +++ b/codesamples/tests.py @@ -16,9 +16,7 @@ def setUp(self): is_published=False) def test_published(self): - self.assertQuerysetEqual(CodeSample.objects.published(), - ['']) + self.assertQuerySetEqual(CodeSample.objects.published(),[''], transform=repr) def test_draft(self): - self.assertQuerysetEqual(CodeSample.objects.draft(), - ['']) + self.assertQuerySetEqual(CodeSample.objects.draft(),[''], transform=repr) diff --git a/community/__init__.py b/community/__init__.py index bc11cfaf6..e69de29bb 100644 --- a/community/__init__.py +++ b/community/__init__.py @@ -1 +0,0 @@ -default_app_config = 'community.apps.CommunityAppConfig' diff --git a/community/migrations/0005_alter_link_creator_alter_link_last_modified_by_and_more.py b/community/migrations/0005_alter_link_creator_alter_link_last_modified_by_and_more.py new file mode 100644 index 000000000..9372dbf0e --- /dev/null +++ b/community/migrations/0005_alter_link_creator_alter_link_last_modified_by_and_more.py @@ -0,0 +1,76 @@ +# Generated by Django 4.2.11 on 2024-09-05 17:10 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('community', '0001_squashed_0004_auto_20170831_0541'), + ] + + operations = [ + migrations.AlterField( + model_name='link', + name='creator', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s_creator', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='link', + name='last_modified_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s_modified', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='link', + name='post', + field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='related_%(class)s', to='community.post'), + ), + migrations.AlterField( + model_name='photo', + name='creator', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s_creator', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='photo', + name='last_modified_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s_modified', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='photo', + name='post', + field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='related_%(class)s', to='community.post'), + ), + migrations.AlterField( + model_name='post', + name='creator', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s_creator', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='post', + name='last_modified_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s_modified', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='post', + name='meta', + field=models.JSONField(blank=True, default=dict), + ), + migrations.AlterField( + model_name='video', + name='creator', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s_creator', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='video', + name='last_modified_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s_modified', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='video', + name='post', + field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='related_%(class)s', to='community.post'), + ), + ] diff --git a/community/models.py b/community/models.py index 1e199b590..75ee94cd8 100644 --- a/community/models.py +++ b/community/models.py @@ -1,4 +1,4 @@ -from django.contrib.postgres.fields import JSONField +from django.db.models import JSONField from django.urls import reverse from django.db import models from django.utils.translation import gettext_lazy as _ diff --git a/community/tests/test_managers.py b/community/tests/test_managers.py index 004e5ee2e..8e91e5523 100644 --- a/community/tests/test_managers.py +++ b/community/tests/test_managers.py @@ -16,6 +16,6 @@ def test_post_manager(self): status=Post.STATUS_PUBLIC ) - self.assertQuerysetEqual(Post.objects.all(), [public_post, private_post], lambda x: x) - self.assertQuerysetEqual(Post.objects.public(), [public_post], lambda x: x) - self.assertQuerysetEqual(Post.objects.private(), [private_post], lambda x: x) + self.assertQuerySetEqual(Post.objects.all(), [public_post, private_post], lambda x: x) + self.assertQuerySetEqual(Post.objects.public(), [public_post], lambda x: x) + self.assertQuerySetEqual(Post.objects.private(), [private_post], lambda x: x) diff --git a/companies/__init__.py b/companies/__init__.py index 1a15cc943..e69de29bb 100644 --- a/companies/__init__.py +++ b/companies/__init__.py @@ -1 +0,0 @@ -default_app_config = 'companies.apps.CompaniesAppConfig' diff --git a/custom_storages/storages.py b/custom_storages/storages.py index c735fd530..567685603 100644 --- a/custom_storages/storages.py +++ b/custom_storages/storages.py @@ -23,6 +23,21 @@ class PipelineManifestStorage(PipelineMixin, ManifestFilesMixin, StaticFilesStor imports in comments. Ref: https://code.djangoproject.com/ticket/21080 """ + # Skip map files + # https://code.djangoproject.com/ticket/33353#comment:13 + patterns = ( + ( + "*.css", + ( + "(?Purl\\(['\"]{0,1}\\s*(?P.*?)[\"']{0,1}\\))", + ( + "(?P@import\\s*[\"']\\s*(?P.*?)[\"'])", + '@import url("%(url)s")', + ), + ), + ), + ) + def get_comment_blocks(self, content): """ Return a list of (start, end) tuples for each comment block. @@ -32,73 +47,85 @@ def get_comment_blocks(self, content): for match in re.finditer(r'\/\*.*?\*\/', content, flags=re.DOTALL) ] - def url_converter(self, name, hashed_files, template=None, comment_blocks=None): + + def is_in_comment(self, pos, comments): + for start, end in comments: + if start < pos and pos < end: + return True + if pos < start: + return False + return False + + + def url_converter(self, name, hashed_files, template=None, comment_blocks=[]): """ Return the custom URL converter for the given file name. """ - if comment_blocks is None: - comment_blocks = [] - if template is None: template = self.default_template def converter(matchobj): """ Convert the matched URL to a normalized and hashed URL. + This requires figuring out which files the matched URL resolves to and calling the url() method of the storage. """ - matched, url = matchobj.groups() + matches = matchobj.groupdict() + matched = matches["matched"] + url = matches["url"] # Ignore URLs in comments. if self.is_in_comment(matchobj.start(), comment_blocks): return matched # Ignore absolute/protocol-relative and data-uri URLs. - if re.match(r'^[a-z]+:', url): + if re.match(r"^[a-z]+:", url): return matched # Ignore absolute URLs that don't point to a static file (dynamic # CSS / JS?). Note that STATIC_URL cannot be empty. - if url.startswith('/') and not url.startswith(settings.STATIC_URL): + if url.startswith("/") and not url.startswith(settings.STATIC_URL): return matched # Strip off the fragment so a path-like fragment won't interfere. url_path, fragment = urldefrag(url) - if url_path.startswith('/'): + # Ignore URLs without a path + if not url_path: + return matched + + if url_path.startswith("/"): # Otherwise the condition above would have returned prematurely. assert url_path.startswith(settings.STATIC_URL) - target_name = url_path[len(settings.STATIC_URL):] + target_name = url_path[len(settings.STATIC_URL) :] else: # We're using the posixpath module to mix paths and URLs conveniently. - source_name = name if os.sep == '/' else name.replace(os.sep, '/') + source_name = name if os.sep == "/" else name.replace(os.sep, "/") target_name = posixpath.join(posixpath.dirname(source_name), url_path) # Determine the hashed name of the target file with the storage backend. hashed_url = self._url( - self._stored_name, unquote(target_name), - force=True, hashed_files=hashed_files, + self._stored_name, + unquote(target_name), + force=True, + hashed_files=hashed_files, ) - transformed_url = '/'.join(url_path.split('/')[:-1] + hashed_url.split('/')[-1:]) + transformed_url = "/".join( + url_path.split("/")[:-1] + hashed_url.split("/")[-1:] + ) # Restore the fragment that was stripped off earlier. if fragment: - transformed_url += ('?#' if '?#' in url else '#') + fragment + transformed_url += ("?#" if "?#" in url else "#") + fragment # Return the hashed version to the file - return template % unquote(transformed_url) + matches["url"] = unquote(transformed_url) + return template % matches return converter - def is_in_comment(self, pos, comments): - for start, end in comments: - if start < pos and pos < end: - return True - if pos < start: - return False - return False def _post_process(self, paths, adjustable_paths, hashed_files): # Sort the files by directory level @@ -122,7 +149,7 @@ def path_level(name): hashed_name = hashed_files[hash_key] # then get the original's file content.. - if hasattr(original_file, 'seek'): + if hasattr(original_file, "seek"): original_file.seek(0) hashed_file_exists = self.exists(hashed_name) @@ -131,12 +158,14 @@ def path_level(name): # ..to apply each replacement pattern to the content if name in adjustable_paths: old_hashed_name = hashed_name - content = original_file.read().decode(settings.FILE_CHARSET) + content = original_file.read().decode("utf-8") for extension, patterns in self._patterns.items(): if matches_patterns(path, (extension,)): comment_blocks = self.get_comment_blocks(content) for pattern, template in patterns: - converter = self.url_converter(name, hashed_files, template, comment_blocks) + converter = self.url_converter( + name, hashed_files, template, comment_blocks + ) try: content = pattern.sub(converter, content) except ValueError as exc: @@ -145,8 +174,9 @@ def path_level(name): self.delete(hashed_name) # then save the processed result content_file = ContentFile(content.encode()) - # Save intermediate file for reference - saved_name = self._save(hashed_name, content_file) + if self.keep_intermediate_files: + # Save intermediate file for reference + self._save(hashed_name, content_file) hashed_name = self.hashed_name(name, content_file) if self.exists(hashed_name): diff --git a/docker-compose.yml b/docker-compose.yml index 2e5f8bf16..4406c0ff5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,7 +2,7 @@ version: "3.9" services: postgres: - image: postgres:10-bullseye + image: postgres:15.3-bullseye ports: - "5433:5432" environment: diff --git a/docs/source/administration.rst b/docs/source/administration.rst index 872222055..6ba820fd8 100644 --- a/docs/source/administration.rst +++ b/docs/source/administration.rst @@ -46,7 +46,7 @@ Pages are individual entire pages of markup content. They are require ``Title`` :Is Published: Controls whether or not the page is visible on the site. :Template Name: By default Pages use the template ``templates/pages/default.html`` to use a different template enter the template path here. -.. note:: Pages are automatically purge from Fastly.com upon save. +.. note:: Pages are automatically purged from Fastly.com upon save. .. _boxes: @@ -82,7 +82,7 @@ Release Files have a checkbox named 'Download button' that determines which bina Jobs ---- -The jobs application is using to display Python jobs on the site. The data items should be fairly self explanatory. There are a couple of things to keep in mind. Logged in users of the site can submit jobs for review. +The jobs application is used to display Python jobs on the site. The data items should be fairly self explanatory. There are a couple of things to keep in mind. Logged in users of the site can submit jobs for review. :Status: Jobs enter the system in 'review' status after the submitter has entered them. Only jobs in the 'approved' state are displayed on the site. :Featured: Featured jobs are displayed more prominently on the landing page. diff --git a/downloads/__init__.py b/downloads/__init__.py index 0f460f952..e69de29bb 100644 --- a/downloads/__init__.py +++ b/downloads/__init__.py @@ -1 +0,0 @@ -default_app_config = 'downloads.apps.DownloadsAppConfig' diff --git a/downloads/migrations/0011_alter_os_creator_alter_os_last_modified_by_and_more.py b/downloads/migrations/0011_alter_os_creator_alter_os_last_modified_by_and_more.py new file mode 100644 index 000000000..368d575c2 --- /dev/null +++ b/downloads/migrations/0011_alter_os_creator_alter_os_last_modified_by_and_more.py @@ -0,0 +1,46 @@ +# Generated by Django 4.2.11 on 2024-09-05 17:10 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('downloads', '0010_releasefile_sbom_spdx2_file'), + ] + + operations = [ + migrations.AlterField( + model_name='os', + name='creator', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s_creator', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='os', + name='last_modified_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s_modified', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='release', + name='creator', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s_creator', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='release', + name='last_modified_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s_modified', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='releasefile', + name='creator', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s_creator', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='releasefile', + name='last_modified_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s_modified', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/downloads/tests/test_models.py b/downloads/tests/test_models.py index f27e9517d..d31afae5c 100644 --- a/downloads/tests/test_models.py +++ b/downloads/tests/test_models.py @@ -82,8 +82,8 @@ def test_is_version_at_least(self): release_38 = Release.objects.create(name='Python 3.8.0') self.assertFalse(release_38.is_version_at_least_3_9) - self.assert_(release_38.is_version_at_least_3_5) + self.assertTrue(release_38.is_version_at_least_3_5) release_310 = Release.objects.create(name='Python 3.10.0') - self.assert_(release_310.is_version_at_least_3_9) - self.assert_(release_310.is_version_at_least_3_5) + self.assertTrue(release_310.is_version_at_least_3_9) + self.assertTrue(release_310.is_version_at_least_3_5) diff --git a/downloads/tests/test_views.py b/downloads/tests/test_views.py index 50270c556..c585fe05c 100644 --- a/downloads/tests/test_views.py +++ b/downloads/tests/test_views.py @@ -5,11 +5,10 @@ from django.urls import reverse from django.test import TestCase, override_settings -from rest_framework.authtoken.models import Token from rest_framework.test import APITestCase from .base import BaseDownloadTests, DownloadMixin -from ..models import OS, Release +from ..models import Release from pages.factories import PageFactory from pydotorg.drf import BaseAPITestCase from users.factories import UserFactory @@ -41,7 +40,7 @@ def test_download_release_detail(self): self.assertEqual(response.status_code, 200) with self.subTest("Release file sizes should be human-readable"): - self.assertInHTML("11.8 MB", response.content.decode()) + self.assertInHTML("11.8 MB", response.content.decode()) url = reverse('download:download_release_detail', kwargs={'release_slug': 'fake_slug'}) response = self.client.get(url) @@ -122,7 +121,7 @@ def test_invalid_token(self): self.assertEqual(response.status_code, 401) url = self.create_url('os') - response = self.client.get(url, HTTP_AUTHORIZATION=self.Authorization_invalid) + response = self.client.get(url, headers={"authorization": self.Authorization_invalid}) # TODO: API v1 returns 200 for a GET request even if token is invalid. # 'StaffAuthorization.read_list` returns 'object_list' unconditionally, # and 'StaffAuthorization.read_detail` returns 'True'. @@ -222,7 +221,7 @@ def test_get_release(self): self.assertEqual(len(content), 4) # Login to get all releases. - response = self.client.get(url, HTTP_AUTHORIZATION=self.Authorization) + response = self.client.get(url, headers={"authorization": self.Authorization}) self.assertEqual(response.status_code, 200) content = self.get_json(response) self.assertEqual(len(content), 5) @@ -258,7 +257,7 @@ def test_post_release(self): response = self.client.get(new_url) # TODO: API v1 returns 401; and API v2 returns 404. self.assertIn(response.status_code, [401, 404]) - response = self.client.get(new_url, HTTP_AUTHORIZATION=self.Authorization) + response = self.client.get(new_url, headers={"authorization": self.Authorization}) self.assertEqual(response.status_code, 200) content = self.get_json(response) self.assertEqual(content['name'], data['name']) @@ -490,15 +489,15 @@ def test_throttling_anon(self): ) def test_throttling_user(self): url = self.create_url('os') - response = self.client.get(url, HTTP_AUTHORIZATION=self.Authorization) + response = self.client.get(url, headers={"authorization": self.Authorization}) self.assertEqual(response.status_code, 200) # Second request should be okay for a user. - response = self.client.get(url, HTTP_AUTHORIZATION=self.Authorization) + response = self.client.get(url, headers={"authorization": self.Authorization}) self.assertEqual(response.status_code, 200) # Third request should return '429 TOO MANY REQUESTS'. - response = self.client.get(url, HTTP_AUTHORIZATION=self.Authorization) + response = self.client.get(url, headers={"authorization": self.Authorization}) self.assertEqual(response.status_code, 429) def test_filter_release_file_delete_by_release(self): @@ -552,6 +551,6 @@ def test_filter_release_file_delete_by_release(self): 'release_file/delete_by_release', filters={'release': self.release_275.pk}, ), - HTTP_AUTHORIZATION=self.Authorization, + headers={"authorization": self.Authorization} ) self.assertEqual(response.status_code, 405) diff --git a/events/__init__.py b/events/__init__.py index 28291bff9..e69de29bb 100644 --- a/events/__init__.py +++ b/events/__init__.py @@ -1 +0,0 @@ -default_app_config = 'events.apps.EventsAppConfig' diff --git a/events/importer.py b/events/importer.py index fa303184b..fe04d35f5 100644 --- a/events/importer.py +++ b/events/importer.py @@ -22,7 +22,13 @@ def import_occurrence(self, event, event_data): # but won't add any timezone information. We will convert them to # aware datetime objects manually. dt_start = extract_date_or_datetime(event_data['DTSTART'].dt) - dt_end = extract_date_or_datetime(event_data['DTEND'].dt) + if 'DTEND' in event_data: + # DTEND is not always set on events, in particular it seems that + # events which have the same start and end time, don't provide + # DTEND. See #2021. + dt_end = extract_date_or_datetime(event_data['DTEND'].dt) + else: + dt_end = dt_start # Let's mark those occurrences as 'all-day'. all_day = ( @@ -48,12 +54,13 @@ def import_event(self, event_data): ) defaults = { 'title': title, - 'description': description, - 'description_markup_type': 'html', 'venue': location, 'calendar': self.calendar, } event, _ = Event.objects.update_or_create(uid=uid, defaults=defaults) + event.description.raw = description + event.description.markup_type = "html" + event.save() self.import_occurrence(event, event_data) def fetch(self, url): diff --git a/events/migrations/0008_alter_alarm_creator_alter_alarm_last_modified_by_and_more.py b/events/migrations/0008_alter_alarm_creator_alter_alarm_last_modified_by_and_more.py new file mode 100644 index 000000000..371ae3aae --- /dev/null +++ b/events/migrations/0008_alter_alarm_creator_alter_alarm_last_modified_by_and_more.py @@ -0,0 +1,46 @@ +# Generated by Django 4.2.11 on 2024-09-05 17:10 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('events', '0007_auto_20180705_0352'), + ] + + operations = [ + migrations.AlterField( + model_name='alarm', + name='creator', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s_creator', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='alarm', + name='last_modified_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s_modified', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='calendar', + name='creator', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s_creator', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='calendar', + name='last_modified_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s_modified', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='event', + name='creator', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s_creator', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='event', + name='last_modified_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s_modified', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/events/models.py b/events/models.py index 3334ca326..b41d92b22 100644 --- a/events/models.py +++ b/events/models.py @@ -237,7 +237,7 @@ class OccurringRule(RuleMixin, models.Model): def __str__(self): strftime = settings.SHORT_DATETIME_FORMAT - return f'{self.event.title} {date(self.dt_start.strftime, strftime)} - {date(self.dt_end.strftime, strftime)}' + return f'{self.event.title} {date(self.dt_start, strftime)} - {date(self.dt_end, strftime)}' @property def begin(self): @@ -283,8 +283,8 @@ class RecurringRule(RuleMixin, models.Model): all_day = models.BooleanField(default=False) def __str__(self): - strftime = settings.SHORT_DATETIME_FORMAT - return f'{self.event.title} every {timedelta_nice_repr(self.interval)} since {date(self.dt_start.strftime, strftime)}' + return (f'{self.event.title} every {timedelta_nice_repr(self.freq_interval_as_timedelta)} since ' + f'{date(self.dt_start, settings.SHORT_DATETIME_FORMAT)}') def to_rrule(self): return rrule( diff --git a/fixtures/boxes.json b/fixtures/boxes.json index bc3816cc7..df66827b5 100644 --- a/fixtures/boxes.json +++ b/fixtures/boxes.json @@ -318,9 +318,9 @@ "created": "2014-02-13T17:37:50.862Z", "updated": "2014-02-16T23:01:04.762Z", "label": "widget-weneedyou", - "content": "

>>> Python Needs You

\r\n

Open source software is made better when users can easily contribute code and documentation to fix bugs and add features. Python strongly encourages community involvement in improving the software. Learn more about how to make Python better for everyone.

\r\n

\r\n Contribute to Python\r\n Bug Tracker\r\n

", + "content": "

>>> Python Needs You

\r\n

Open source software is made better when users can easily contribute code and documentation to fix bugs and add features. Python strongly encourages community involvement in improving the software. Learn more about how to make Python better for everyone.

\r\n

\r\n Contribute to Python\r\n Bug Tracker\r\n

", "content_markup_type": "html", - "_content_rendered": "

>>> Python Needs You

\r\n

Open source software is made better when users can easily contribute code and documentation to fix bugs and add features. Python strongly encourages community involvement in improving the software. Learn more about how to make Python better for everyone.

\r\n

\r\n Contribute to Python\r\n Bug Tracker\r\n

" + "_content_rendered": "

>>> Python Needs You

\r\n

Open source software is made better when users can easily contribute code and documentation to fix bugs and add features. Python strongly encourages community involvement in improving the software. Learn more about how to make Python better for everyone.

\r\n

\r\n Contribute to Python\r\n Bug Tracker\r\n

" } }, { diff --git a/jobs/__init__.py b/jobs/__init__.py index 3716a978d..e69de29bb 100644 --- a/jobs/__init__.py +++ b/jobs/__init__.py @@ -1 +0,0 @@ -default_app_config = 'jobs.apps.JobsAppConfig' diff --git a/jobs/migrations/0021_alter_job_creator_alter_job_last_modified_by_and_more.py b/jobs/migrations/0021_alter_job_creator_alter_job_last_modified_by_and_more.py new file mode 100644 index 000000000..a82f65ac9 --- /dev/null +++ b/jobs/migrations/0021_alter_job_creator_alter_job_last_modified_by_and_more.py @@ -0,0 +1,36 @@ +# Generated by Django 4.2.11 on 2024-09-05 17:10 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('jobs', '0020_auto_20191101_1601'), + ] + + operations = [ + migrations.AlterField( + model_name='job', + name='creator', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s_creator', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='job', + name='last_modified_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s_modified', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='jobreviewcomment', + name='creator', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s_creator', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='jobreviewcomment', + name='last_modified_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s_modified', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/jobs/migrations/0022_alter_jobtype_options_alter_job_job_types_and_more.py b/jobs/migrations/0022_alter_jobtype_options_alter_job_job_types_and_more.py new file mode 100644 index 000000000..4013f2376 --- /dev/null +++ b/jobs/migrations/0022_alter_jobtype_options_alter_job_job_types_and_more.py @@ -0,0 +1,27 @@ +# Generated by Django 4.2.16 on 2024-09-13 17:30 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('jobs', '0021_alter_job_creator_alter_job_last_modified_by_and_more'), + ] + + operations = [ + migrations.AlterModelOptions( + name='jobtype', + options={'ordering': ('name',), 'verbose_name': 'job types', 'verbose_name_plural': 'job types'}, + ), + migrations.AlterField( + model_name='job', + name='job_types', + field=models.ManyToManyField(blank=True, limit_choices_to={'active': True}, related_name='jobs', to='jobs.jobtype', verbose_name='Job types'), + ), + migrations.AlterField( + model_name='job', + name='other_job_type', + field=models.CharField(blank=True, max_length=100, verbose_name='Other job types'), + ), + ] diff --git a/jobs/models.py b/jobs/models.py index 54722873d..8b232fb93 100644 --- a/jobs/models.py +++ b/jobs/models.py @@ -30,8 +30,8 @@ class JobType(NameSlugModel): objects = JobTypeQuerySet.as_manager() class Meta: - verbose_name = 'job technologies' - verbose_name_plural = 'job technologies' + verbose_name = 'job types' + verbose_name_plural = 'job types' ordering = ('name', ) @@ -59,11 +59,11 @@ class Job(ContentManageable): JobType, related_name='jobs', blank=True, - verbose_name='Job technologies', + verbose_name='Job types', limit_choices_to={'active': True}, ) other_job_type = models.CharField( - verbose_name='Other job technologies', + verbose_name='Other job types', max_length=100, blank=True, ) diff --git a/jobs/signals.py b/jobs/signals.py index 9c470d83a..0317ff716 100644 --- a/jobs/signals.py +++ b/jobs/signals.py @@ -1,10 +1,10 @@ from django.dispatch import Signal # Sent after job offer was submitted for review -job_was_submitted = Signal(providing_args=['job']) +job_was_submitted = Signal() # Sent after job offer was approved -job_was_approved = Signal(providing_args=['approving_user', 'job']) +job_was_approved = Signal() # Sent after job offer was rejected -job_was_rejected = Signal(providing_args=['rejecting_user', 'job']) +job_was_rejected = Signal() # Sent after comment was posted -comment_was_posted = Signal(providing_args=['comment']) +comment_was_posted = Signal() diff --git a/jobs/tests/test_models.py b/jobs/tests/test_models.py index 310659165..5a9c5eb8d 100644 --- a/jobs/tests/test_models.py +++ b/jobs/tests/test_models.py @@ -76,7 +76,7 @@ def test_visible_manager(self): j3 = factories.ApprovedJobFactory(expires=past) visible = Job.objects.visible() - self.assertTrue(len(visible), 1) + self.assertEqual(len(visible), 1) self.assertIn(j1, visible) self.assertNotIn(j2, visible) self.assertNotIn(j3, visible) diff --git a/mailing/tests/__init__.py b/mailing/tests/__init__.py index d85108ab5..8d8b4f5c7 100644 --- a/mailing/tests/__init__.py +++ b/mailing/tests/__init__.py @@ -1 +1 @@ -# Create your tests here +"""Tests for the mailing app.""" diff --git a/mailing/tests/forms.py b/mailing/tests/forms.py new file mode 100644 index 000000000..b433adea6 --- /dev/null +++ b/mailing/tests/forms.py @@ -0,0 +1,13 @@ +"""Forms to be used in mailing tests.""" + +from mailing.forms import BaseEmailTemplateForm +from mailing.tests.models import MockEmailTemplate + + +class TestBaseEmailTemplateForm(BaseEmailTemplateForm): + """Base email template form for testing.""" + + class Meta: + """Metaclass for the form.""" + model = MockEmailTemplate + fields = "__all__" diff --git a/mailing/tests/models.py b/mailing/tests/models.py new file mode 100644 index 000000000..917e8dfb9 --- /dev/null +++ b/mailing/tests/models.py @@ -0,0 +1,12 @@ +"""Models to be used in mailing tests.""" + +from mailing.models import BaseEmailTemplate + + +class MockEmailTemplate(BaseEmailTemplate): + """Mock model for BaseEmailTemplate to use in tests.""" + + class Meta: + """Metaclass for MockEmailTemplate to avoid creating a table in the database.""" + app_label = 'mailing' + managed = False diff --git a/mailing/tests/test_forms.py b/mailing/tests/test_forms.py index 1e9165a0c..f7a0c6890 100644 --- a/mailing/tests/test_forms.py +++ b/mailing/tests/test_forms.py @@ -1,6 +1,7 @@ +"""Tests for mailing app forms.""" from django.test import TestCase -from mailing.forms import BaseEmailTemplateForm +from mailing.tests.forms import TestBaseEmailTemplateForm class BaseEmailTemplateFormTests(TestCase): @@ -14,16 +15,16 @@ def setUp(self): def test_validate_required_fields(self): required = set(self.data) - form = BaseEmailTemplateForm(data={}) + form = TestBaseEmailTemplateForm(data={}) self.assertFalse(form.is_valid()) self.assertEqual(required, set(form.errors)) def test_validate_with_correct_data(self): - form = BaseEmailTemplateForm(data=self.data) + form = TestBaseEmailTemplateForm(data=self.data) self.assertTrue(form.is_valid()) def test_invalid_form_if_broken_template_syntax(self): self.data["content"] = "Invalid syntax {% invalid %}" - form = BaseEmailTemplateForm(data=self.data) + form = TestBaseEmailTemplateForm(data=self.data) self.assertFalse(form.is_valid()) self.assertIn("content", form.errors, form.errors) diff --git a/membership/__init__.py b/membership/__init__.py index 2e00144a5..e69de29bb 100644 --- a/membership/__init__.py +++ b/membership/__init__.py @@ -1 +0,0 @@ -default_app_config = 'membership.apps.MembershipAppConfig' diff --git a/minutes/__init__.py b/minutes/__init__.py index 862de63c6..e69de29bb 100644 --- a/minutes/__init__.py +++ b/minutes/__init__.py @@ -1 +0,0 @@ -default_app_config = 'minutes.apps.MinutesAppConfig' diff --git a/minutes/migrations/0003_alter_minutes_creator_alter_minutes_last_modified_by.py b/minutes/migrations/0003_alter_minutes_creator_alter_minutes_last_modified_by.py new file mode 100644 index 000000000..07e512874 --- /dev/null +++ b/minutes/migrations/0003_alter_minutes_creator_alter_minutes_last_modified_by.py @@ -0,0 +1,26 @@ +# Generated by Django 4.2.11 on 2024-09-05 17:10 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('minutes', '0002_auto_20150416_1853'), + ] + + operations = [ + migrations.AlterField( + model_name='minutes', + name='creator', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s_creator', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='minutes', + name='last_modified_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s_modified', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/minutes/tests/test_models.py b/minutes/tests/test_models.py index fcd9bf571..4b5603641 100644 --- a/minutes/tests/test_models.py +++ b/minutes/tests/test_models.py @@ -21,16 +21,10 @@ def setUp(self): ) def test_draft(self): - self.assertQuerysetEqual( - Minutes.objects.draft(), - [''] - ) + self.assertQuerySetEqual(Minutes.objects.draft(), [''], transform=repr) def test_published(self): - self.assertQuerysetEqual( - Minutes.objects.published(), - [''] - ) + self.assertQuerySetEqual(Minutes.objects.published(), [''], transform=repr) def test_date_methods(self): self.assertEqual(self.m1.get_date_year(), '2012') diff --git a/nominations/__init__.py b/nominations/__init__.py index 368a604a5..e69de29bb 100644 --- a/nominations/__init__.py +++ b/nominations/__init__.py @@ -1 +0,0 @@ -default_app_config = 'nominations.apps.NominationsAppConfig' diff --git a/pages/__init__.py b/pages/__init__.py index 6f1e657eb..e69de29bb 100644 --- a/pages/__init__.py +++ b/pages/__init__.py @@ -1 +0,0 @@ -default_app_config = 'pages.apps.PagesAppConfig' diff --git a/pages/migrations/0004_alter_page_creator_alter_page_last_modified_by.py b/pages/migrations/0004_alter_page_creator_alter_page_last_modified_by.py new file mode 100644 index 000000000..19c5a6082 --- /dev/null +++ b/pages/migrations/0004_alter_page_creator_alter_page_last_modified_by.py @@ -0,0 +1,26 @@ +# Generated by Django 4.2.11 on 2024-09-05 17:10 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('pages', '0003_auto_20230214_2113'), + ] + + operations = [ + migrations.AlterField( + model_name='page', + name='creator', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s_creator', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='page', + name='last_modified_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s_modified', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/pages/tests/test_api.py b/pages/tests/test_api.py index 4026bac75..1c4cc6184 100644 --- a/pages/tests/test_api.py +++ b/pages/tests/test_api.py @@ -30,7 +30,7 @@ def test_get_published_pages(self): def test_get_all_pages(self): # Login to get all pages. url = self.create_url('page') - response = self.client.get(url, HTTP_AUTHORIZATION=self.Authorization) + response = self.client.get(url, headers={"authorization": self.Authorization}) self.assertEqual(response.status_code, 200) self.assertEqual(len(response.data), 3) @@ -41,7 +41,7 @@ def test_filter_page(self): self.assertEqual(len(response.data), 1) # Login to filter all pages. - response = self.client.get(url, HTTP_AUTHORIZATION=self.Authorization) + response = self.client.get(url, headers={"authorization": self.Authorization}) self.assertEqual(response.status_code, 200) self.assertEqual(len(response.data), 2) @@ -53,7 +53,7 @@ def test_filter_page(self): self.assertEqual(len(response.data), 0) # This should return only unpublished pages. - response = self.client.get(url, HTTP_AUTHORIZATION=self.Authorization) + response = self.client.get(url, headers={"authorization": self.Authorization}) self.assertEqual(response.status_code, 200) self.assertEqual(len(response.data), 1) diff --git a/pages/tests/test_models.py b/pages/tests/test_models.py index 2215cc812..eac62f102 100644 --- a/pages/tests/test_models.py +++ b/pages/tests/test_models.py @@ -8,11 +8,11 @@ class PageModelTests(BasePageTests): - def test_published(self): - self.assertQuerysetEqual(Page.objects.published(), ['']) - def test_draft(self): - self.assertQuerysetEqual(Page.objects.draft(), ['']) + self.assertQuerySetEqual(Page.objects.draft(), [''], transform=repr) + + def test_published(self): + self.assertQuerySetEqual(Page.objects.published(), [''], transform=repr) def test_get_title(self): one = Page.objects.get(path='one') diff --git a/peps/__init__.py b/peps/__init__.py deleted file mode 100644 index c71df1925..000000000 --- a/peps/__init__.py +++ /dev/null @@ -1 +0,0 @@ -default_app_config = 'peps.apps.PepsAppConfig' diff --git a/peps/apps.py b/peps/apps.py deleted file mode 100644 index a59996f2a..000000000 --- a/peps/apps.py +++ /dev/null @@ -1,6 +0,0 @@ -from django.apps import AppConfig - - -class PepsAppConfig(AppConfig): - - name = 'peps' diff --git a/peps/converters.py b/peps/converters.py deleted file mode 100644 index 1d63d7438..000000000 --- a/peps/converters.py +++ /dev/null @@ -1,262 +0,0 @@ -import functools -import datetime -import re -import os - -from bs4 import BeautifulSoup - -from django.conf import settings -from django.core.exceptions import ImproperlyConfigured -from django.core.files import File -from django.db.models import Max - -from pages.models import Page, Image - -PEP_TEMPLATE = 'pages/pep-page.html' -pep_url = lambda num: f'dev/peps/pep-{num}/' - - -def get_peps_last_updated(): - last_update = Page.objects.filter( - path__startswith='dev/peps', - ).aggregate(Max('updated')).get('updated__max') - if last_update is None: - return datetime.datetime( - 1970, 1, 1, tzinfo=datetime.timezone( - datetime.timedelta(0) - ) - ) - return last_update - - -def convert_pep0(artifact_path): - """ - Take existing generated pep-0000.html and convert to something suitable - for a Python.org Page returns the core body HTML necessary only - """ - pep0_path = os.path.join(artifact_path, 'pep-0000.html') - pep0_content = open(pep0_path).read() - data = convert_pep_page(0, pep0_content) - if data is None: - return - return data['content'] - - -def get_pep0_page(artifact_path, commit=True): - """ - Using convert_pep0 above, create a CMS ready pep0 page and return it - - pep0 is used as the directory index, but it's also an actual pep, so we - return both Page objects. - """ - pep0_content = convert_pep0(artifact_path) - if pep0_content is None: - return None, None - pep0_page, _ = Page.objects.get_or_create(path='dev/peps/') - pep0000_page, _ = Page.objects.get_or_create(path='dev/peps/pep-0000/') - for page in [pep0_page, pep0000_page]: - page.content = pep0_content - page.content_markup_type = 'html' - page.title = "PEP 0 -- Index of Python Enhancement Proposals (PEPs)" - page.template_name = PEP_TEMPLATE - - if commit: - page.save() - - return pep0_page, pep0000_page - - -def fix_headers(soup, data): - """ Remove empty or unwanted headers and find our title """ - header_rows = soup.find_all('th') - for t in header_rows: - if 'Version:' in t.text: - if t.next_sibling.text == '$Revision$': - t.parent.extract() - if t.next_sibling.text == '': - t.parent.extract() - if 'Last-Modified:' in t.text: - if '$Date$'in t.next_sibling.text: - t.parent.extract() - if t.next_sibling.text == '': - t.parent.extract() - if t.text == 'Title:': - data['title'] = t.next_sibling.text - if t.text == 'Content-Type:': - t.parent.extract() - if 'Version:' in t.text and 'N/A' in t.next_sibling.text: - t.parent.extract() - - return soup, data - - -def convert_pep_page(pep_number, content): - """ - Handle different formats that pep2html.py outputs - """ - data = { - 'title': None, - } - # Remove leading zeros from PEP number for display purposes - pep_number_humanize = re.sub(r'^0+', '', str(pep_number)) - - if '' in content: - soup = BeautifulSoup(content, 'lxml') - data['title'] = soup.title.text - - if not re.search(r'PEP \d+', data['title']): - data['title'] = 'PEP {} -- {}'.format( - pep_number_humanize, - soup.title.text, - ) - - header = soup.body.find('div', class_="header") - header, data = fix_headers(header, data) - data['header'] = str(header) - - main_content = soup.body.find('div', class_="content") - - data['main_content'] = str(main_content) - data['content'] = ''.join([ - data['header'], - data['main_content'] - ]) - - else: - soup = BeautifulSoup(content, 'lxml') - - soup, data = fix_headers(soup, data) - if not data['title']: - data['title'] = f"PEP {pep_number_humanize} -- " - else: - if not re.search(r'PEP \d+', data['title']): - data['title'] = "PEP {} -- {}".format( - pep_number_humanize, - data['title'], - ) - - data['content'] = str(soup) - - # Fix PEP links - pep_content = BeautifulSoup(data['content'], 'lxml') - body_links = pep_content.find_all("a") - - pep_href_re = re.compile(r'pep-(\d+)\.html') - - for b in body_links: - m = pep_href_re.search(b.attrs['href']) - - # Skip anything not matching 'pep-XXXX.html' - if not m: - continue - - b.attrs['href'] = f'/dev/peps/pep-{m.group(1)}/' - - # Return early if 'html' or 'body' return None. - if pep_content.html is None or pep_content.body is None: - return - - # Strip and tags. - pep_content.html.unwrap() - pep_content.body.unwrap() - - data['content'] = str(pep_content) - return data - - -def get_pep_page(artifact_path, pep_number, commit=True): - """ - Given a pep_number retrieve original PEP source text, rst, or html. - Get or create the associated Page and return it - """ - pep_path = os.path.join(artifact_path, f'pep-{pep_number}.html') - if not os.path.exists(pep_path): - print(f"PEP Path '{pep_path}' does not exist, skipping") - return - - pep_content = convert_pep_page(pep_number, open(pep_path).read()) - if pep_content is None: - return None - pep_rst_source = os.path.join( - artifact_path, f'pep-{pep_number}.rst', - ) - pep_ext = '.rst' if os.path.exists(pep_rst_source) else '.txt' - source_link = 'https://github.com/python/peps/blob/master/pep-{}{}'.format( - pep_number, pep_ext) - pep_content['content'] += """Source: {0}""".format(source_link) - - pep_page, _ = Page.objects.get_or_create(path=pep_url(pep_number)) - - pep_page.title = pep_content['title'] - - pep_page.content = pep_content['content'] - pep_page.content_markup_type = 'html' - pep_page.template_name = PEP_TEMPLATE - - if commit: - pep_page.save() - - return pep_page - - -def add_pep_image(artifact_path, pep_number, path): - image_path = os.path.join(artifact_path, path) - if not os.path.exists(image_path): - print(f"Image Path '{image_path}' does not exist, skipping") - return - - try: - page = Page.objects.get(path=pep_url(pep_number)) - except Page.DoesNotExist: - print(f"Could not find backing PEP {pep_number}") - return - - # Find existing images, we have to loop here as we can't use the ORM - # to query against image__path - existing_images = Image.objects.filter(page=page) - - FOUND = False - for image in existing_images: - if image.image.name.endswith(path): - FOUND = True - break - - if not FOUND: - image = Image(page=page) - - with open(image_path, 'rb') as image_obj: - image.image.save(path, File(image_obj)) - image.save() - - # Old images used to live alongside html, but now they're in different - # places, so update the page accordingly. - soup = BeautifulSoup(page.content.raw, 'lxml') - for img_tag in soup.findAll('img'): - if img_tag['src'] == path: - img_tag['src'] = image.image.url - - page.content.raw = str(soup) - page.save() - - return image - - -def get_peps_rss(artifact_path): - rss_feed = os.path.join(artifact_path, 'peps.rss') - if not os.path.exists(rss_feed): - return - - page, _ = Page.objects.get_or_create( - path="dev/peps/peps.rss", - template_name="pages/raw.html", - ) - - with open(rss_feed) as rss_content: - content = rss_content.read() - - page.content = content - page.is_published = True - page.content_type = "application/rss+xml" - page.save() - - return page diff --git a/peps/management/__init__.py b/peps/management/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/peps/management/commands/__init__.py b/peps/management/commands/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/peps/management/commands/dump_pep_pages.py b/peps/management/commands/dump_pep_pages.py deleted file mode 100644 index 549b8faa4..000000000 --- a/peps/management/commands/dump_pep_pages.py +++ /dev/null @@ -1,21 +0,0 @@ -from django.core import serializers -from django.core.management import BaseCommand - -from pages.models import Page - - -class Command(BaseCommand): - """ - Dump PEP related Pages as indented JSON - """ - help = "Dump PEP related Pages as indented JSON" - - def handle(self, **options): - qs = Page.objects.filter(path__startswith='dev/peps/') - - serializers.serialize( - format='json', - queryset=qs, - indent=4, - stream=self.stdout, - ) diff --git a/peps/management/commands/generate_pep_pages.py b/peps/management/commands/generate_pep_pages.py deleted file mode 100644 index 9f9010584..000000000 --- a/peps/management/commands/generate_pep_pages.py +++ /dev/null @@ -1,143 +0,0 @@ -import re -import os - -from contextlib import ExitStack -from tarfile import TarFile -from tempfile import TemporaryDirectory, TemporaryFile - -import requests - -from django.core.management import BaseCommand -from django.conf import settings - -from dateutil.parser import parse as parsedate - -from peps.converters import ( - get_pep0_page, get_pep_page, add_pep_image, get_peps_rss, get_peps_last_updated -) - -pep_number_re = re.compile(r'pep-(\d+)') - - -class Command(BaseCommand): - """ - Generate CMS Pages from flat file PEP data. - - Run this command AFTER normal RST -> HTML PEP transformation from the PEP - repository has happened. This works on the HTML files created during that - process. - - For verbose output run this with: - - ./manage.py generate_pep_pages --verbosity=2 - """ - help = "Generate PEP Page objects from rendered HTML" - - def is_pep_page(self, path): - return path.startswith('pep-') and path.endswith('.html') - - def is_image(self, path): - # All images are pngs - return path.endswith('.png') - - def handle(self, **options): - verbosity = int(options['verbosity']) - - def verbose(msg): - """ Output wrapper """ - if verbosity > 1: - print(msg) - - verbose("== Starting PEP page generation") - - with ExitStack() as stack: - if settings.PEP_REPO_PATH is not None: - artifacts_path = settings.PEP_REPO_PATH - else: - verbose(f"== Fetching PEP artifact from {settings.PEP_ARTIFACT_URL}") - temp_file = self.get_artifact_tarball(stack) - if not temp_file: - verbose("== No update to artifacts, we're done here!") - return - temp_dir = stack.enter_context(TemporaryDirectory()) - tar_ball = stack.enter_context(TarFile.open(fileobj=temp_file, mode='r:gz')) - tar_ball.extractall(path=temp_dir, numeric_owner=False) - - artifacts_path = os.path.join(temp_dir, 'peps') - - verbose("Generating RSS Feed") - peps_rss = get_peps_rss(artifacts_path) - if not peps_rss: - verbose("Could not find generated RSS feed. Skipping.") - - verbose("Generating PEP0 index page") - pep0_page, _ = get_pep0_page(artifacts_path) - if pep0_page is None: - verbose("HTML version of PEP 0 cannot be generated.") - return - - image_paths = set() - - # Find pep pages - for f in os.listdir(artifacts_path): - - if self.is_image(f): - verbose(f"- Deferring import of image '{f}'") - image_paths.add(f) - continue - - # Skip files we aren't looking for - if not self.is_pep_page(f): - verbose(f"- Skipping non-PEP file '{f}'") - continue - - if 'pep-0000.html' in f: - verbose("- Skipping duplicate PEP0 index") - continue - - verbose(f"Generating PEP Page from '{f}'") - pep_match = pep_number_re.match(f) - if pep_match: - pep_number = pep_match.groups(1)[0] - p = get_pep_page(artifacts_path, pep_number) - if p is None: - verbose( - "- HTML version PEP {!r} cannot be generated.".format( - pep_number - ) - ) - verbose(f"====== Title: '{p.title}'") - else: - verbose(f"- Skipping invalid '{f}'") - - # Find pep images. This needs to happen afterwards, because we need - for img in image_paths: - pep_match = pep_number_re.match(img) - if pep_match: - pep_number = pep_match.groups(1)[0] - verbose("Generating image for PEP {} at '{}'".format( - pep_number, img)) - add_pep_image(artifacts_path, pep_number, img) - else: - verbose(f"- Skipping non-PEP related image '{img}'") - - verbose("== Finished") - - def get_artifact_tarball(self, stack): - artifact_url = settings.PEP_ARTIFACT_URL - if not artifact_url.startswith(('http://', 'https://')): - return stack.enter_context(open(artifact_url, 'rb')) - - peps_last_updated = get_peps_last_updated() - with requests.get(artifact_url, stream=True) as r: - artifact_last_modified = parsedate(r.headers['last-modified']) - if peps_last_updated > artifact_last_modified: - return - - temp_file = stack.enter_context(TemporaryFile()) - for chunk in r.iter_content(chunk_size=8192): - if chunk: - temp_file.write(chunk) - - temp_file.seek(0) - return temp_file diff --git a/peps/models.py b/peps/models.py deleted file mode 100644 index e45cd9cd1..000000000 --- a/peps/models.py +++ /dev/null @@ -1 +0,0 @@ -# Intentially left blank diff --git a/peps/templatetags/__init__.py b/peps/templatetags/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/peps/templatetags/peps.py b/peps/templatetags/peps.py deleted file mode 100644 index 9d90afe24..000000000 --- a/peps/templatetags/peps.py +++ /dev/null @@ -1,16 +0,0 @@ -from django import template - -from pages.models import Page - -register = template.Library() - - -@register.simple_tag -def get_newest_pep_pages(limit=5): - """ Retrieve the most recently added PEPs """ - latest_peps = Page.objects.filter( - path__startswith='dev/peps/', - is_published=True, - ).order_by('-created')[:limit] - - return latest_peps diff --git a/peps/tests/__init__.py b/peps/tests/__init__.py deleted file mode 100644 index 944cc90aa..000000000 --- a/peps/tests/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -import os - -from django.conf import settings - -FAKE_PEP_REPO = os.path.join(settings.BASE, 'peps/tests/peps/') -FAKE_PEP_ARTIFACT = os.path.join(settings.BASE, 'peps/tests/peps.tar.gz') diff --git a/peps/tests/peps.tar.gz b/peps/tests/peps.tar.gz deleted file mode 100644 index edaa86b79..000000000 Binary files a/peps/tests/peps.tar.gz and /dev/null differ diff --git a/peps/tests/peps/pep-0000.html b/peps/tests/peps/pep-0000.html deleted file mode 100644 index c11c027c9..000000000 --- a/peps/tests/peps/pep-0000.html +++ /dev/null @@ -1,1030 +0,0 @@ - - - - - PEP 0 -- Index of Python Enhancement Proposals (PEPs) - - - - - - -
- - - - - - - - - -
PEP: 0
Title: Index of Python Enhancement Proposals (PEPs)
Version: N/A
Last-Modified: 2014-09-26
Author: David Goodger <goodger at python.org>, Barry Warsaw <barry at python.org>
Status: Active
Type: Informational
Created: 13-Jul-2000
-
-
-
-

Introduction

-
-    This PEP contains the index of all Python Enhancement Proposals,
-    known as PEPs.  PEP numbers are assigned by the PEP editors, and
-    once assigned are never changed[1].  The Mercurial history[2] of
-    the PEP texts represent their historical record.
-
-
-
-

Index by Category

-
-     num  title                                                   owner
-     ---  -----                                                   -----
-
- Meta-PEPs (PEPs about PEPs or Processes)
-
- P     1  PEP Purpose and Guidelines                              Warsaw, Hylton, Goodger, Coghlan
- P     4  Deprecation of Standard Modules                         von Löwis
- P     5  Guidelines for Language Evolution                       Prescod
- P     6  Bug Fix Releases                                        Aahz, Baxter
- P     7  Style Guide for C Code                                  GvR
- P     8  Style Guide for Python Code                             GvR, Warsaw, Coghlan
- P     9  Sample Plaintext PEP Template                           Warsaw
- P    10  Voting Guidelines                                       Warsaw
- P    11  Removing support for little used platforms              von Löwis
- P    12  Sample reStructuredText PEP Template                    Goodger, Warsaw
-
- Other Informational PEPs
-
- I    20  The Zen of Python                                       Peters
- I   101  Doing Python Releases 101                               Warsaw, GvR
- IF  247  API for Cryptographic Hash Functions                    Kuchling
- IF  248  Python Database API Specification v1.0                  Lemburg
- IF  249  Python Database API Specification v2.0                  Lemburg
- I   257  Docstring Conventions                                   Goodger, GvR
- IF  272  API for Block Encryption Algorithms v1.0                Kuchling
- I   287  reStructuredText Docstring Format                       Goodger
- I   290  Code Migration and Modernization                        Hettinger
- IF  291  Backward Compatibility for the Python 2 Standard ...    Norwitz
- IF  333  Python Web Server Gateway Interface v1.0                Eby
- I   373  Python 2.7 Release Schedule                             Peterson
- I   392  Python 3.2 Release Schedule                             Brandl
- I   394  The "python" Command on Unix-Like Systems               Staley, Coghlan
- I   398  Python 3.3 Release Schedule                             Brandl
- IF  399  Pure Python/C Accelerator Module Compatibility ...      Cannon
- IF  404  Python 2.8 Un-release Schedule                          Warsaw
- IA  411  Provisional packages in the Python standard library     Coghlan, Bendersky
- I   429  Python 3.4 Release Schedule                             Hastings
- IF  430  Migrating to Python 3 as the default online ...         Coghlan
- I   434  IDLE Enhancement Exception for All Branches             Rovito, Reedy
- IA  440  Version Identification and Dependency Specification     Coghlan, Stufft
- I   478  Python 3.5 Release Schedule                             Hastings
- IF 3333  Python Web Server Gateway Interface v1.0.1              Eby
-
- Accepted PEPs (accepted; may not be implemented yet)
-
- SA  345  Metadata for Python Software Packages 1.2               Jones
- SA  376  Database of Installed Python Distributions              Ziadé
- SA  425  Compatibility Tags for Built Distributions              Holth
- SA  427  The Wheel Binary Package Format 1.0                     Holth
- SA  461  Adding % formatting to bytes and bytearray              Furman
- SA  471  os.scandir() function -- a better and faster ...        Hoyt
- SA  477  Backport ensurepip (PEP 453) to Python 2.7              Stufft, Coghlan
- SA 3121  Extension Module Initialization and Finalization        von Löwis
-
- Open PEPs (under consideration)
-
- S   381  Mirroring infrastructure for PyPI                       Ziadé, v. Löwis
- P   387  Backwards Compatibility Policy                          Peterson
- S   426  Metadata for Python Software Packages 2.0               Coghlan, Holth, Stufft
- S   431  Time zone support improvements                          Regebro
- S   432  Simplifying the CPython startup sequence                Coghlan
- S   436  The Argument Clinic DSL                                 Hastings
- S   441  Improving Python ZIP Application Support                Holth
- S   447  Add __getdescriptor__ method to metaclass               Oussoren
- S   448  Additional Unpacking Generalizations                    Landau
- I   452  API for Cryptographic Hash Functions v2.0               Kuchling, Heimes
- S   455  Adding a key-transforming dictionary to collections     Pitrou
- I   457  Syntax For Positional-Only Parameters                   Hastings
- S   458  Surviving a Compromise of PyPI                          Kuppusamy, Stufft, Cappos
- S   459  Standard Metadata Extensions for Python Software ...    Coghlan
- S   463  Exception-catching expressions                          Angelico
- S   467  Minor API improvements for binary sequences             Coghlan
- S   468  Preserving the order of \*\*kwargs in a function.       Snow
- P   470  Using Multi Index Support for External to PyPI ...      Stufft
- S   472  Support for indexing with keyword arguments             Borini, Martinot-Lagarde
- S   473  Adding structured data to built-in exceptions           Kreft
- S   475  Retry system calls failing with EINTR                   Natali, Stinner
- S   476  Enabling certificate verification by default for ...    Gaynor
-
- Finished PEPs (done, implemented in code repository)
-
- SF  100  Python Unicode Integration                              Lemburg
- SF  201  Lockstep Iteration                                      Warsaw
- SF  202  List Comprehensions                                     Warsaw
- SF  203  Augmented Assignments                                   Wouters
- SF  205  Weak References                                         Drake
- SF  207  Rich Comparisons                                        GvR, Ascher
- SF  208  Reworking the Coercion Model                            Schemenauer, Lemburg
- SF  214  Extended Print Statement                                Warsaw
- SF  217  Display Hook for Interactive Use                        Zadka
- SF  218  Adding a Built-In Set Object Type                       Wilson, Hettinger
- SF  221  Import As                                               Wouters
- SF  223  Change the Meaning of \x Escapes                        Peters
- SF  227  Statically Nested Scopes                                Hylton
- SF  229  Using Distutils to Build Python                         Kuchling
- SF  230  Warning Framework                                       GvR
- SF  232  Function Attributes                                     Warsaw
- SF  234  Iterators                                               Yee, GvR
- SF  235  Import on Case-Insensitive Platforms                    Peters
- SF  236  Back to the __future__                                  Peters
- SF  237  Unifying Long Integers and Integers                     Zadka, GvR
- SF  238  Changing the Division Operator                          Zadka, GvR
- SF  241  Metadata for Python Software Packages                   Kuchling
- SF  250  Using site-packages on Windows                          Moore
- SF  252  Making Types Look More Like Classes                     GvR
- SF  253  Subtyping Built-in Types                                GvR
- SF  255  Simple Generators                                       Schemenauer, Peters, Hetland
- SF  260  Simplify xrange()                                       GvR
- SF  261  Support for "wide" Unicode characters                   Prescod
- SF  263  Defining Python Source Code Encodings                   Lemburg, von Löwis
- SF  264  Future statements in simulated shells                   Hudson
- SF  273  Import Modules from Zip Archives                        Ahlstrom
- SF  274  Dict Comprehensions                                     Warsaw
- SF  277  Unicode file name support for Windows NT                Hodgson
- SF  278  Universal Newline Support                               Jansen
- SF  279  The enumerate() built-in function                       Hettinger
- SF  282  A Logging System                                        Sajip, Mick
- SF  285  Adding a bool type                                      GvR
- SF  289  Generator Expressions                                   Hettinger
- SF  292  Simpler String Substitutions                            Warsaw
- SF  293  Codec Error Handling Callbacks                          Dörwald
- SF  301  Package Index and Metadata for Distutils                Jones
- SF  302  New Import Hooks                                        JvR, Moore
- SF  305  CSV File API                                            Altis, Cole, McNamara, Montanaro, Wells
- SF  307  Extensions to the pickle protocol                       GvR, Peters
- SF  308  Conditional Expressions                                 GvR, Hettinger
- SF  309  Partial Function Application                            Harris
- SF  311  Simplified Global Interpreter Lock Acquisition for ...  Hammond
- SF  314  Metadata for Python Software Packages v1.1              Kuchling, Jones
- SF  318  Decorators for Functions and Methods                    Smith
- SF  322  Reverse Iteration                                       Hettinger
- SF  324  subprocess - New process module                         Astrand
- SF  327  Decimal Data Type                                       Batista
- SF  328  Imports: Multi-Line and Absolute/Relative               Aahz
- SF  331  Locale-Independent Float/String Conversions             Reis
- SF  338  Executing modules as scripts                            Coghlan
- SF  341  Unifying try-except and try-finally                     Brandl
- SF  342  Coroutines via Enhanced Generators                      GvR, Eby
- SF  343  The "with" Statement                                    GvR, Coghlan
- SF  352  Required Superclass for Exceptions                      Cannon, GvR
- SF  353  Using ssize_t as the index type                         von Löwis
- SF  357  Allowing Any Object to be Used for Slicing              Oliphant
- SF  358  The "bytes" Object                                      Schemenauer, GvR
- SF  362  Function Signature Object                               Cannon, Seo, Selivanov, Hastings
- SF  366  Main module explicit relative imports                   Coghlan
- SF  370  Per user site-packages directory                        Heimes
- SF  371  Addition of the multiprocessing package to the ...      Noller, Oudkerk
- SF  372  Adding an ordered dictionary to collections             Ronacher, Hettinger
- SF  378  Format Specifier for Thousands Separator                Hettinger
- SF  380  Syntax for Delegating to a Subgenerator                 Ewing
- SF  383  Non-decodable Bytes in System Character Interfaces      v. Löwis
- SF  384  Defining a Stable ABI                                   v. Löwis
- SF  389  argparse - New Command Line Parsing Module              Bethard
- SF  391  Dictionary-Based Configuration For Logging              Sajip
- SF  393  Flexible String Representation                          v. Löwis
- SF  397  Python launcher for Windows                             Hammond, v. Löwis
- SF  405  Python Virtual Environments                             Meyer
- SF  409  Suppressing exception context                           Furman
- SF  412  Key-Sharing Dictionary                                  Shannon
- SF  414  Explicit Unicode Literal for Python 3.3                 Ronacher, Coghlan
- SF  415  Implement context suppression with exception attributes Peterson
- SF  417  Including mock in the Standard Library                  Foord
- SF  418  Add monotonic time, performance counter, and ...        Simpson, Jewett, Turnbull, Stinner
- SF  420  Implicit Namespace Packages                             Smith
- SF  421  Adding sys.implementation                               Snow
- SF  424  A method for exposing a length hint                     Gaynor
- SF  428  The pathlib module -- object-oriented filesystem paths  Pitrou
- SF  435  Adding an Enum type to the Python standard library      Warsaw, Bendersky, Furman
- SF  442  Safe object finalization                                Pitrou
- SF  443  Single-dispatch generic functions                       Langa
- SF  445  Add new APIs to customize Python memory allocators      Stinner
- SF  446  Make newly created file descriptors non-inheritable     Stinner
- SF  450  Adding A Statistics Module To The Standard Library      D'Aprano
- SF  451  A ModuleSpec Type for the Import System                 Snow
- SF  453  Explicit bootstrapping of pip in Python installations   Stufft, Coghlan
- SF  454  Add a new tracemalloc module to trace Python memory ... Stinner
- SF  456  Secure and interchangeable hash algorithm               Heimes
- SF  465  A dedicated infix operator for matrix multiplication    Smith
- SF  466  Network Security Enhancements for Python 2.7.x          Coghlan
- SF 3101  Advanced String Formatting                              Talin
- SF 3102  Keyword-Only Arguments                                  Talin
- SF 3104  Access to Names in Outer Scopes                         Yee
- SF 3105  Make print a function                                   Brandl
- SF 3106  Revamping dict.keys(), .values() and .items()           GvR
- SF 3107  Function Annotations                                    Winter, Lownds
- SF 3108  Standard Library Reorganization                         Cannon
- SF 3109  Raising Exceptions in Python 3000                       Winter
- SF 3110  Catching Exceptions in Python 3000                      Winter
- SF 3111  Simple input built-in in Python 3000                    Roberge
- SF 3112  Bytes literals in Python 3000                           Orendorff
- SF 3113  Removal of Tuple Parameter Unpacking                    Cannon
- SF 3114  Renaming iterator.next() to iterator.__next__()         Yee
- SF 3115  Metaclasses in Python 3000                              Talin
- SF 3116  New I/O                                                 Stutzbach, GvR, Verdone
- SF 3118  Revising the buffer protocol                            Oliphant, Banks
- SF 3119  Introducing Abstract Base Classes                       GvR, Talin
- SF 3120  Using UTF-8 as the default source encoding              von Löwis
- SF 3123  Making PyObject_HEAD conform to standard C              von Löwis
- SF 3127  Integer Literal Support and Syntax                      Maupin
- SF 3129  Class Decorators                                        Winter
- SF 3131  Supporting Non-ASCII Identifiers                        von Löwis
- SF 3132  Extended Iterable Unpacking                             Brandl
- SF 3134  Exception Chaining and Embedded Tracebacks              Yee
- SF 3135  New Super                                               Spealman, Delaney, Ryan
- SF 3137  Immutable Bytes and Mutable Buffer                      GvR
- SF 3138  String representation in Python 3000                    Ishimoto
- SF 3141  A Type Hierarchy for Numbers                            Yasskin
- SF 3144  IP Address Manipulation Library for the Python ...      Moody
- SF 3147  PYC Repository Directories                              Warsaw
- SF 3148  futures - execute computations asynchronously           Quinlan
- SF 3149  ABI version tagged .so files                            Warsaw
- SF 3151  Reworking the OS and IO exception hierarchy             Pitrou
- SF 3154  Pickle protocol version 4                               Pitrou
- SF 3155  Qualified name for classes and functions                Pitrou
- SF 3156  Asynchronous IO Support Rebooted: the "asyncio" Module  GvR
-
- Historical Meta-PEPs and Informational PEPs
-
- PF    2  Procedure for Adding New Modules                        Faassen
- PF   42  Feature Requests                                        Hylton
- IF  160  Python 1.6 Release Schedule                             Drake
- IF  200  Python 2.0 Release Schedule                             Hylton
- IF  226  Python 2.1 Release Schedule                             Hylton
- IF  251  Python 2.2 Release Schedule                             Warsaw, GvR
- IF  283  Python 2.3 Release Schedule                             GvR
- IF  320  Python 2.4 Release Schedule                             Warsaw, Hettinger, Baxter
- PF  347  Migrating the Python CVS to Subversion                  von Löwis
- IF  356  Python 2.5 Release Schedule                             Norwitz, GvR, Baxter
- PF  360  Externally Maintained Packages                          Cannon
- IF  361  Python 2.6 and 3.0 Release Schedule                     Norwitz, Warsaw
- PF  374  Choosing a distributed VCS for the Python project       Cannon, Turnbull, Vassalotti, Warsaw, Ochtman
- IF  375  Python 3.1 Release Schedule                             Peterson
- PF  385  Migrating from Subversion to Mercurial                  Ochtman, Pitrou, Brandl
- PA  438  Transitioning to release-file hosting on PyPI           Krekel, Meyer
- PA  449  Removal of the PyPI Mirror Auto Discovery and ...       Stufft
- PA  464  Removal of the PyPI Mirror Authenticity API             Stufft
- PF 3000  Python 3000                                             GvR
- PF 3002  Procedure for Backwards-Incompatible Changes            Bethard
- PF 3003  Python Language Moratorium                              Cannon, Noller, GvR
- PF 3099  Things that will Not Change in Python 3000              Brandl
- PF 3100  Miscellaneous Python 3.0 Plans                          Cannon
-
- Deferred PEPs
-
- SD  211  Adding A New Outer Product Operator                     Wilson
- SD  212  Loop Counter Iteration                                  Schneider-Kamp
- SD  213  Attribute Access Handlers                               Prescod
- SD  219  Stackless Python                                        McMillan
- SD  222  Web Library Enhancements                                Kuchling
- SD  225  Elementwise/Objectwise Operators                        Zhu, Lielens
- SD  233  Python Online Help                                      Prescod
- SD  262  A Database of Installed Python Packages                 Kuchling
- SD  267  Optimized Access to Module Namespaces                   Hylton
- SD  269  Pgen Module for Python                                  Riehl
- SD  280  Optimizing access to globals                            GvR
- SD  286  Enhanced Argument Tuples                                von Löwis
- SD  312  Simple Implicit Lambda                                  Suzi, Martelli
- SD  316  Programming by Contract for Python                      Way
- SD  323  Copyable Iterators                                      Martelli
- SD  337  Logging Usage in the Standard Library                   Dubner
- SD  349  Allow str() to return unicode strings                   Schemenauer
- SD  368  Standard image protocol and class                       Mastrodomenico
- ID  396  Module Version Numbers                                  Warsaw
- SD  400  Deprecate codecs.StreamReader and codecs.StreamWriter   Stinner
- SD  403  General purpose decorator clause (aka "@in" clause)     Coghlan
- PD  407  New release cycle and introducing long-term support ... Pitrou, Brandl, Warsaw
- SD  419  Protecting cleanup statements from interruptions        Colomiets
- SD  422  Simpler customisation of class creation                 Coghlan, Urban
- ID  423  Naming conventions and recipes related to packaging     Bryon
- ID  444  Python Web3 Interface                                   McDonough, Ronacher
- PD  462  Core development workflow automation for CPython        Coghlan
- PD  474  Creating forge.python.org                               Coghlan
- SD  628  Add ``math.tau``                                        Coghlan
- SD 3124  Overloading, Generic Functions, Interfaces, and ...     Eby
- SD 3143  Standard daemon process library                         Finney
- SD 3150  Statement local namespaces (aka "given" clause)         Coghlan
- SD 3152  Cofunctions                                             Ewing
-
- Abandoned, Withdrawn, and Rejected PEPs
-
- PW    3  Guidelines for Handling Bug Reports                     Hylton
- IS  102  Doing Python Micro Releases                             Baxter, Warsaw, GvR
- SR  204  Range Literals                                          Wouters
- IW  206  Python Advanced Library                                 Kuchling
- SW  209  Multi-dimensional Arrays                                Barrett, Oliphant
- SR  210  Decoupling the Interpreter Loop                         Ascher
- SS  215  String Interpolation                                    Yee
- IR  216  Docstring Format                                        Zadka
- IR  220  Coroutines, Generators, Continuations                   McMillan
- SR  224  Attribute Docstrings                                    Lemburg
- SW  228  Reworking Python's Numeric Model                        Zadka, GvR
- SR  231  __findattr__()                                          Warsaw
- SR  239  Adding a Rational Type to Python                        Craig, Zadka
- SR  240  Adding a Rational Literal to Python                     Craig, Zadka
- SR  242  Numeric Kinds                                           Dubois
- SW  243  Module Repository Upload Mechanism                      Reifschneider
- SR  244  The `directive' statement                               von Löwis
- SR  245  Python Interface Syntax                                 Pelletier
- SR  246  Object Adaptation                                       Martelli, Evans
- SR  254  Making Classes Look More Like Types                     GvR
- SR  256  Docstring Processing System Framework                   Goodger
- SR  258  Docutils Design Specification                           Goodger
- SR  259  Omit printing newline after newline                     GvR
- SR  265  Sorting Dictionaries by Value                           Griffin
- SW  266  Optimizing Global Variable/Attribute Access             Montanaro
- SR  268  Extended HTTP functionality and WebDAV                  Stein
- SR  270  uniq method for list objects                            Petrone
- SR  271  Prefixing sys.path by command line option               Giacometti
- SR  275  Switching on Multiple Values                            Lemburg
- SR  276  Simple Iterator for ints                                Althoff
- SR  281  Loop Counter Iteration with range and xrange            Hetland
- SR  284  Integer for-loops                                       Eppstein, Ewing
- SW  288  Generators Attributes and Exceptions                    Hettinger
- SR  294  Type Names in the types Module                          Tirosh
- SR  295  Interpretation of multiline string constants            Koltsov
- SW  296  Adding a bytes Object Type                              Gilbert
- SR  297  Support for System Upgrades                             Lemburg
- SW  298  The Locked Buffer Interface                             Heller
- SR  299  Special __main__() function in modules                  Epler
- SR  303  Extend divmod() for Multiple Divisors                   Bellman
- SW  304  Controlling Generation of Bytecode Files                Montanaro
- IW  306  How to Change Python's Grammar                          Hudson, Diederich, Coghlan, Peterson
- SR  310  Reliable Acquisition/Release Pairs                      Hudson, Moore
- SR  313  Adding Roman Numeral Literals to Python                 Meyer
- SR  315  Enhanced While Loop                                     Hettinger, Carroll
- SR  317  Eliminate Implicit Exception Instantiation              Taschuk
- SR  319  Python Synchronize/Asynchronize Block                   Pelletier
- SW  321  Date/Time Parsing and Formatting                        Kuchling
- SR  325  Resource-Release Support for Generators                 Pedroni
- SR  326  A Case for Top and Bottom Values                        Carlson, Reedy
- SR  329  Treating Builtins as Constants in the Standard Library  Hettinger
- SR  330  Python Bytecode Verification                            Pelletier
- SR  332  Byte vectors and String/Unicode Unification             Montanaro
- SW  334  Simple Coroutines via SuspendIteration                  Evans
- SR  335  Overloadable Boolean Operators                          Ewing
- SR  336  Make None Callable                                      McClelland
- IW  339  Design of the CPython Compiler                          Cannon
- SR  340  Anonymous Block Statements                              GvR
- SS  344  Exception Chaining and Embedded Tracebacks              Yee
- SW  346  User Defined ("``with``") Statements                    Coghlan
- SR  348  Exception Reorganization for Python 3.0                 Cannon
- IR  350  Codetags                                                Elliott
- SR  351  The freeze protocol                                     Warsaw
- SS  354  Enumerations in Python                                  Finney
- SR  355  Path - Object oriented filesystem paths                 Lindqvist
- SW  359  The "make" Statement                                    Bethard
- SR  363  Syntax For Dynamic Attribute Access                     North
- SW  364  Transitioning to the Py3K Standard Library              Warsaw
- SR  365  Adding the pkg_resources module                         Eby
- SS  367  New Super                                               Spealman, Delaney
- SW  369  Post import hooks                                       Heimes
- SR  377  Allow __enter__() methods to skip the statement body    Coghlan
- SW  379  Adding an Assignment Expression                         Whitley
- SR  382  Namespace Packages                                      v. Löwis
- SS  386  Changing the version comparison module in Distutils     Ziadé
- SR  390  Static metadata for Distutils                           Ziadé
- SW  395  Qualified Names for Modules                             Coghlan
- PR  401  BDFL Retirement                                         Warsaw, Cannon
- SR  402  Simplified Package Layout and Partitioning              Eby
- SW  406  Improved Encapsulation of Import State                  Coghlan, Slodkowicz
- SR  408  Standard library __preview__ package                    Coghlan, Bendersky
- SR  410  Use decimal.Decimal type for timestamps                 Stinner
- PW  413  Faster evolution of the Python Standard Library         Coghlan
- SR  416  Add a frozendict builtin type                           Stinner
- SS  433  Easier suppression of file descriptor inheritance       Stinner
- SR  437  A DSL for specifying signatures, annotations and ...    Krah
- SR  439  Inclusion of implicit pip bootstrap in Python ...       Jones
- SW  460  Add binary interpolation and formatting                 Pitrou
- SW  469  Migration of dict iteration code to Python 3            Coghlan
- SR  666  Reject Foolish Indentation                              Creighton
- SR  754  IEEE 754 Floating Point Special Values                  Warnes
- PW 3001  Procedure for reviewing and improving standard ...      Brandl
- SR 3103  A Switch/Case Statement                                 GvR
- SR 3117  Postfix type declarations                               Brandl
- SR 3122  Delineation of the main module                          Cannon
- SR 3125  Remove Backslash Continuation                           Jewett
- SR 3126  Remove Implicit String Concatenation                    Jewett, Hettinger
- SR 3128  BList: A Faster List-like Type                          Stutzbach
- SR 3130  Access to Current Module/Class/Function                 Jewett
- SR 3133  Introducing Roles                                       Winter
- SR 3136  Labeled break and continue                              Chisholm
- SR 3139  Cleaning out sys and the "interpreter" module           Peterson
- SR 3140  str(container) should call str(item), not repr(item)    Broytmann, Jewett
- SR 3142  Add a "while" clause to generator expressions           Britton
- SW 3145  Asynchronous I/O For subprocess.Popen                   Pruitt, McCreary, Carlson
- SW 3146  Merging Unladen Swallow into CPython                    Winter, Yasskin, Kleckner
- SS 3153  Asynchronous IO support                                 Houtven
-
-
-
-

Numerical Index

-
-     num  title                                                   owner
-     ---  -----                                                   -----
- P     1  PEP Purpose and Guidelines                              Warsaw, Hylton, Goodger, Coghlan
- PF    2  Procedure for Adding New Modules                        Faassen
- PW    3  Guidelines for Handling Bug Reports                     Hylton
- P     4  Deprecation of Standard Modules                         von Löwis
- P     5  Guidelines for Language Evolution                       Prescod
- P     6  Bug Fix Releases                                        Aahz, Baxter
- P     7  Style Guide for C Code                                  GvR
- P     8  Style Guide for Python Code                             GvR, Warsaw, Coghlan
- P     9  Sample Plaintext PEP Template                           Warsaw
- P    10  Voting Guidelines                                       Warsaw
- P    11  Removing support for little used platforms              von Löwis
- P    12  Sample reStructuredText PEP Template                    Goodger, Warsaw
-
- I    20  The Zen of Python                                       Peters
-
- PF   42  Feature Requests                                        Hylton
-
- SF  100  Python Unicode Integration                              Lemburg
- I   101  Doing Python Releases 101                               Warsaw, GvR
- IS  102  Doing Python Micro Releases                             Baxter, Warsaw, GvR
-
- IF  160  Python 1.6 Release Schedule                             Drake
-
- IF  200  Python 2.0 Release Schedule                             Hylton
- SF  201  Lockstep Iteration                                      Warsaw
- SF  202  List Comprehensions                                     Warsaw
- SF  203  Augmented Assignments                                   Wouters
- SR  204  Range Literals                                          Wouters
- SF  205  Weak References                                         Drake
- IW  206  Python Advanced Library                                 Kuchling
- SF  207  Rich Comparisons                                        GvR, Ascher
- SF  208  Reworking the Coercion Model                            Schemenauer, Lemburg
- SW  209  Multi-dimensional Arrays                                Barrett, Oliphant
- SR  210  Decoupling the Interpreter Loop                         Ascher
- SD  211  Adding A New Outer Product Operator                     Wilson
- SD  212  Loop Counter Iteration                                  Schneider-Kamp
- SD  213  Attribute Access Handlers                               Prescod
- SF  214  Extended Print Statement                                Warsaw
- SS  215  String Interpolation                                    Yee
- IR  216  Docstring Format                                        Zadka
- SF  217  Display Hook for Interactive Use                        Zadka
- SF  218  Adding a Built-In Set Object Type                       Wilson, Hettinger
- SD  219  Stackless Python                                        McMillan
- IR  220  Coroutines, Generators, Continuations                   McMillan
- SF  221  Import As                                               Wouters
- SD  222  Web Library Enhancements                                Kuchling
- SF  223  Change the Meaning of \x Escapes                        Peters
- SR  224  Attribute Docstrings                                    Lemburg
- SD  225  Elementwise/Objectwise Operators                        Zhu, Lielens
- IF  226  Python 2.1 Release Schedule                             Hylton
- SF  227  Statically Nested Scopes                                Hylton
- SW  228  Reworking Python's Numeric Model                        Zadka, GvR
- SF  229  Using Distutils to Build Python                         Kuchling
- SF  230  Warning Framework                                       GvR
- SR  231  __findattr__()                                          Warsaw
- SF  232  Function Attributes                                     Warsaw
- SD  233  Python Online Help                                      Prescod
- SF  234  Iterators                                               Yee, GvR
- SF  235  Import on Case-Insensitive Platforms                    Peters
- SF  236  Back to the __future__                                  Peters
- SF  237  Unifying Long Integers and Integers                     Zadka, GvR
- SF  238  Changing the Division Operator                          Zadka, GvR
- SR  239  Adding a Rational Type to Python                        Craig, Zadka
- SR  240  Adding a Rational Literal to Python                     Craig, Zadka
- SF  241  Metadata for Python Software Packages                   Kuchling
- SR  242  Numeric Kinds                                           Dubois
- SW  243  Module Repository Upload Mechanism                      Reifschneider
- SR  244  The `directive' statement                               von Löwis
- SR  245  Python Interface Syntax                                 Pelletier
- SR  246  Object Adaptation                                       Martelli, Evans
- IF  247  API for Cryptographic Hash Functions                    Kuchling
- IF  248  Python Database API Specification v1.0                  Lemburg
- IF  249  Python Database API Specification v2.0                  Lemburg
- SF  250  Using site-packages on Windows                          Moore
- IF  251  Python 2.2 Release Schedule                             Warsaw, GvR
- SF  252  Making Types Look More Like Classes                     GvR
- SF  253  Subtyping Built-in Types                                GvR
- SR  254  Making Classes Look More Like Types                     GvR
- SF  255  Simple Generators                                       Schemenauer, Peters, Hetland
- SR  256  Docstring Processing System Framework                   Goodger
- I   257  Docstring Conventions                                   Goodger, GvR
- SR  258  Docutils Design Specification                           Goodger
- SR  259  Omit printing newline after newline                     GvR
- SF  260  Simplify xrange()                                       GvR
- SF  261  Support for "wide" Unicode characters                   Prescod
- SD  262  A Database of Installed Python Packages                 Kuchling
- SF  263  Defining Python Source Code Encodings                   Lemburg, von Löwis
- SF  264  Future statements in simulated shells                   Hudson
- SR  265  Sorting Dictionaries by Value                           Griffin
- SW  266  Optimizing Global Variable/Attribute Access             Montanaro
- SD  267  Optimized Access to Module Namespaces                   Hylton
- SR  268  Extended HTTP functionality and WebDAV                  Stein
- SD  269  Pgen Module for Python                                  Riehl
- SR  270  uniq method for list objects                            Petrone
- SR  271  Prefixing sys.path by command line option               Giacometti
- IF  272  API for Block Encryption Algorithms v1.0                Kuchling
- SF  273  Import Modules from Zip Archives                        Ahlstrom
- SF  274  Dict Comprehensions                                     Warsaw
- SR  275  Switching on Multiple Values                            Lemburg
- SR  276  Simple Iterator for ints                                Althoff
- SF  277  Unicode file name support for Windows NT                Hodgson
- SF  278  Universal Newline Support                               Jansen
- SF  279  The enumerate() built-in function                       Hettinger
- SD  280  Optimizing access to globals                            GvR
- SR  281  Loop Counter Iteration with range and xrange            Hetland
- SF  282  A Logging System                                        Sajip, Mick
- IF  283  Python 2.3 Release Schedule                             GvR
- SR  284  Integer for-loops                                       Eppstein, Ewing
- SF  285  Adding a bool type                                      GvR
- SD  286  Enhanced Argument Tuples                                von Löwis
- I   287  reStructuredText Docstring Format                       Goodger
- SW  288  Generators Attributes and Exceptions                    Hettinger
- SF  289  Generator Expressions                                   Hettinger
- I   290  Code Migration and Modernization                        Hettinger
- IF  291  Backward Compatibility for the Python 2 Standard ...    Norwitz
- SF  292  Simpler String Substitutions                            Warsaw
- SF  293  Codec Error Handling Callbacks                          Dörwald
- SR  294  Type Names in the types Module                          Tirosh
- SR  295  Interpretation of multiline string constants            Koltsov
- SW  296  Adding a bytes Object Type                              Gilbert
- SR  297  Support for System Upgrades                             Lemburg
- SW  298  The Locked Buffer Interface                             Heller
- SR  299  Special __main__() function in modules                  Epler
-
- SF  301  Package Index and Metadata for Distutils                Jones
- SF  302  New Import Hooks                                        JvR, Moore
- SR  303  Extend divmod() for Multiple Divisors                   Bellman
- SW  304  Controlling Generation of Bytecode Files                Montanaro
- SF  305  CSV File API                                            Altis, Cole, McNamara, Montanaro, Wells
- IW  306  How to Change Python's Grammar                          Hudson, Diederich, Coghlan, Peterson
- SF  307  Extensions to the pickle protocol                       GvR, Peters
- SF  308  Conditional Expressions                                 GvR, Hettinger
- SF  309  Partial Function Application                            Harris
- SR  310  Reliable Acquisition/Release Pairs                      Hudson, Moore
- SF  311  Simplified Global Interpreter Lock Acquisition for ...  Hammond
- SD  312  Simple Implicit Lambda                                  Suzi, Martelli
- SR  313  Adding Roman Numeral Literals to Python                 Meyer
- SF  314  Metadata for Python Software Packages v1.1              Kuchling, Jones
- SR  315  Enhanced While Loop                                     Hettinger, Carroll
- SD  316  Programming by Contract for Python                      Way
- SR  317  Eliminate Implicit Exception Instantiation              Taschuk
- SF  318  Decorators for Functions and Methods                    Smith
- SR  319  Python Synchronize/Asynchronize Block                   Pelletier
- IF  320  Python 2.4 Release Schedule                             Warsaw, Hettinger, Baxter
- SW  321  Date/Time Parsing and Formatting                        Kuchling
- SF  322  Reverse Iteration                                       Hettinger
- SD  323  Copyable Iterators                                      Martelli
- SF  324  subprocess - New process module                         Astrand
- SR  325  Resource-Release Support for Generators                 Pedroni
- SR  326  A Case for Top and Bottom Values                        Carlson, Reedy
- SF  327  Decimal Data Type                                       Batista
- SF  328  Imports: Multi-Line and Absolute/Relative               Aahz
- SR  329  Treating Builtins as Constants in the Standard Library  Hettinger
- SR  330  Python Bytecode Verification                            Pelletier
- SF  331  Locale-Independent Float/String Conversions             Reis
- SR  332  Byte vectors and String/Unicode Unification             Montanaro
- IF  333  Python Web Server Gateway Interface v1.0                Eby
- SW  334  Simple Coroutines via SuspendIteration                  Evans
- SR  335  Overloadable Boolean Operators                          Ewing
- SR  336  Make None Callable                                      McClelland
- SD  337  Logging Usage in the Standard Library                   Dubner
- SF  338  Executing modules as scripts                            Coghlan
- IW  339  Design of the CPython Compiler                          Cannon
- SR  340  Anonymous Block Statements                              GvR
- SF  341  Unifying try-except and try-finally                     Brandl
- SF  342  Coroutines via Enhanced Generators                      GvR, Eby
- SF  343  The "with" Statement                                    GvR, Coghlan
- SS  344  Exception Chaining and Embedded Tracebacks              Yee
- SA  345  Metadata for Python Software Packages 1.2               Jones
- SW  346  User Defined ("``with``") Statements                    Coghlan
- PF  347  Migrating the Python CVS to Subversion                  von Löwis
- SR  348  Exception Reorganization for Python 3.0                 Cannon
- SD  349  Allow str() to return unicode strings                   Schemenauer
- IR  350  Codetags                                                Elliott
- SR  351  The freeze protocol                                     Warsaw
- SF  352  Required Superclass for Exceptions                      Cannon, GvR
- SF  353  Using ssize_t as the index type                         von Löwis
- SS  354  Enumerations in Python                                  Finney
- SR  355  Path - Object oriented filesystem paths                 Lindqvist
- IF  356  Python 2.5 Release Schedule                             Norwitz, GvR, Baxter
- SF  357  Allowing Any Object to be Used for Slicing              Oliphant
- SF  358  The "bytes" Object                                      Schemenauer, GvR
- SW  359  The "make" Statement                                    Bethard
- PF  360  Externally Maintained Packages                          Cannon
- IF  361  Python 2.6 and 3.0 Release Schedule                     Norwitz, Warsaw
- SF  362  Function Signature Object                               Cannon, Seo, Selivanov, Hastings
- SR  363  Syntax For Dynamic Attribute Access                     North
- SW  364  Transitioning to the Py3K Standard Library              Warsaw
- SR  365  Adding the pkg_resources module                         Eby
- SF  366  Main module explicit relative imports                   Coghlan
- SS  367  New Super                                               Spealman, Delaney
- SD  368  Standard image protocol and class                       Mastrodomenico
- SW  369  Post import hooks                                       Heimes
- SF  370  Per user site-packages directory                        Heimes
- SF  371  Addition of the multiprocessing package to the ...      Noller, Oudkerk
- SF  372  Adding an ordered dictionary to collections             Ronacher, Hettinger
- I   373  Python 2.7 Release Schedule                             Peterson
- PF  374  Choosing a distributed VCS for the Python project       Cannon, Turnbull, Vassalotti, Warsaw, Ochtman
- IF  375  Python 3.1 Release Schedule                             Peterson
- SA  376  Database of Installed Python Distributions              Ziadé
- SR  377  Allow __enter__() methods to skip the statement body    Coghlan
- SF  378  Format Specifier for Thousands Separator                Hettinger
- SW  379  Adding an Assignment Expression                         Whitley
- SF  380  Syntax for Delegating to a Subgenerator                 Ewing
- S   381  Mirroring infrastructure for PyPI                       Ziadé, v. Löwis
- SR  382  Namespace Packages                                      v. Löwis
- SF  383  Non-decodable Bytes in System Character Interfaces      v. Löwis
- SF  384  Defining a Stable ABI                                   v. Löwis
- PF  385  Migrating from Subversion to Mercurial                  Ochtman, Pitrou, Brandl
- SS  386  Changing the version comparison module in Distutils     Ziadé
- P   387  Backwards Compatibility Policy                          Peterson
-
- SF  389  argparse - New Command Line Parsing Module              Bethard
- SR  390  Static metadata for Distutils                           Ziadé
- SF  391  Dictionary-Based Configuration For Logging              Sajip
- I   392  Python 3.2 Release Schedule                             Brandl
- SF  393  Flexible String Representation                          v. Löwis
- I   394  The "python" Command on Unix-Like Systems               Staley, Coghlan
- SW  395  Qualified Names for Modules                             Coghlan
- ID  396  Module Version Numbers                                  Warsaw
- SF  397  Python launcher for Windows                             Hammond, v. Löwis
- I   398  Python 3.3 Release Schedule                             Brandl
- IF  399  Pure Python/C Accelerator Module Compatibility ...      Cannon
- SD  400  Deprecate codecs.StreamReader and codecs.StreamWriter   Stinner
- PR  401  BDFL Retirement                                         Warsaw, Cannon
- SR  402  Simplified Package Layout and Partitioning              Eby
- SD  403  General purpose decorator clause (aka "@in" clause)     Coghlan
- IF  404  Python 2.8 Un-release Schedule                          Warsaw
- SF  405  Python Virtual Environments                             Meyer
- SW  406  Improved Encapsulation of Import State                  Coghlan, Slodkowicz
- PD  407  New release cycle and introducing long-term support ... Pitrou, Brandl, Warsaw
- SR  408  Standard library __preview__ package                    Coghlan, Bendersky
- SF  409  Suppressing exception context                           Furman
- SR  410  Use decimal.Decimal type for timestamps                 Stinner
- IA  411  Provisional packages in the Python standard library     Coghlan, Bendersky
- SF  412  Key-Sharing Dictionary                                  Shannon
- PW  413  Faster evolution of the Python Standard Library         Coghlan
- SF  414  Explicit Unicode Literal for Python 3.3                 Ronacher, Coghlan
- SF  415  Implement context suppression with exception attributes Peterson
- SR  416  Add a frozendict builtin type                           Stinner
- SF  417  Including mock in the Standard Library                  Foord
- SF  418  Add monotonic time, performance counter, and ...        Simpson, Jewett, Turnbull, Stinner
- SD  419  Protecting cleanup statements from interruptions        Colomiets
- SF  420  Implicit Namespace Packages                             Smith
- SF  421  Adding sys.implementation                               Snow
- SD  422  Simpler customisation of class creation                 Coghlan, Urban
- ID  423  Naming conventions and recipes related to packaging     Bryon
- SF  424  A method for exposing a length hint                     Gaynor
- SA  425  Compatibility Tags for Built Distributions              Holth
- S   426  Metadata for Python Software Packages 2.0               Coghlan, Holth, Stufft
- SA  427  The Wheel Binary Package Format 1.0                     Holth
- SF  428  The pathlib module -- object-oriented filesystem paths  Pitrou
- I   429  Python 3.4 Release Schedule                             Hastings
- IF  430  Migrating to Python 3 as the default online ...         Coghlan
- S   431  Time zone support improvements                          Regebro
- S   432  Simplifying the CPython startup sequence                Coghlan
- SS  433  Easier suppression of file descriptor inheritance       Stinner
- I   434  IDLE Enhancement Exception for All Branches             Rovito, Reedy
- SF  435  Adding an Enum type to the Python standard library      Warsaw, Bendersky, Furman
- S   436  The Argument Clinic DSL                                 Hastings
- SR  437  A DSL for specifying signatures, annotations and ...    Krah
- PA  438  Transitioning to release-file hosting on PyPI           Krekel, Meyer
- SR  439  Inclusion of implicit pip bootstrap in Python ...       Jones
- IA  440  Version Identification and Dependency Specification     Coghlan, Stufft
- S   441  Improving Python ZIP Application Support                Holth
- SF  442  Safe object finalization                                Pitrou
- SF  443  Single-dispatch generic functions                       Langa
- ID  444  Python Web3 Interface                                   McDonough, Ronacher
- SF  445  Add new APIs to customize Python memory allocators      Stinner
- SF  446  Make newly created file descriptors non-inheritable     Stinner
- S   447  Add __getdescriptor__ method to metaclass               Oussoren
- S   448  Additional Unpacking Generalizations                    Landau
- PA  449  Removal of the PyPI Mirror Auto Discovery and ...       Stufft
- SF  450  Adding A Statistics Module To The Standard Library      D'Aprano
- SF  451  A ModuleSpec Type for the Import System                 Snow
- I   452  API for Cryptographic Hash Functions v2.0               Kuchling, Heimes
- SF  453  Explicit bootstrapping of pip in Python installations   Stufft, Coghlan
- SF  454  Add a new tracemalloc module to trace Python memory ... Stinner
- S   455  Adding a key-transforming dictionary to collections     Pitrou
- SF  456  Secure and interchangeable hash algorithm               Heimes
- I   457  Syntax For Positional-Only Parameters                   Hastings
- S   458  Surviving a Compromise of PyPI                          Kuppusamy, Stufft, Cappos
- S   459  Standard Metadata Extensions for Python Software ...    Coghlan
- SW  460  Add binary interpolation and formatting                 Pitrou
- SA  461  Adding % formatting to bytes and bytearray              Furman
- PD  462  Core development workflow automation for CPython        Coghlan
- S   463  Exception-catching expressions                          Angelico
- PA  464  Removal of the PyPI Mirror Authenticity API             Stufft
- SF  465  A dedicated infix operator for matrix multiplication    Smith
- SF  466  Network Security Enhancements for Python 2.7.x          Coghlan
- S   467  Minor API improvements for binary sequences             Coghlan
- S   468  Preserving the order of \*\*kwargs in a function.       Snow
- SW  469  Migration of dict iteration code to Python 3            Coghlan
- P   470  Using Multi Index Support for External to PyPI ...      Stufft
- SA  471  os.scandir() function -- a better and faster ...        Hoyt
- S   472  Support for indexing with keyword arguments             Borini, Martinot-Lagarde
- S   473  Adding structured data to built-in exceptions           Kreft
- PD  474  Creating forge.python.org                               Coghlan
- S   475  Retry system calls failing with EINTR                   Natali, Stinner
- S   476  Enabling certificate verification by default for ...    Gaynor
- SA  477  Backport ensurepip (PEP 453) to Python 2.7              Stufft, Coghlan
- I   478  Python 3.5 Release Schedule                             Hastings
-
- SD  628  Add ``math.tau``                                        Coghlan
-
- SR  666  Reject Foolish Indentation                              Creighton
-
- SR  754  IEEE 754 Floating Point Special Values                  Warnes
-
- PF 3000  Python 3000                                             GvR
- PW 3001  Procedure for reviewing and improving standard ...      Brandl
- PF 3002  Procedure for Backwards-Incompatible Changes            Bethard
- PF 3003  Python Language Moratorium                              Cannon, Noller, GvR
-
- PF 3099  Things that will Not Change in Python 3000              Brandl
- PF 3100  Miscellaneous Python 3.0 Plans                          Cannon
- SF 3101  Advanced String Formatting                              Talin
- SF 3102  Keyword-Only Arguments                                  Talin
- SR 3103  A Switch/Case Statement                                 GvR
- SF 3104  Access to Names in Outer Scopes                         Yee
- SF 3105  Make print a function                                   Brandl
- SF 3106  Revamping dict.keys(), .values() and .items()           GvR
- SF 3107  Function Annotations                                    Winter, Lownds
- SF 3108  Standard Library Reorganization                         Cannon
- SF 3109  Raising Exceptions in Python 3000                       Winter
- SF 3110  Catching Exceptions in Python 3000                      Winter
- SF 3111  Simple input built-in in Python 3000                    Roberge
- SF 3112  Bytes literals in Python 3000                           Orendorff
- SF 3113  Removal of Tuple Parameter Unpacking                    Cannon
- SF 3114  Renaming iterator.next() to iterator.__next__()         Yee
- SF 3115  Metaclasses in Python 3000                              Talin
- SF 3116  New I/O                                                 Stutzbach, GvR, Verdone
- SR 3117  Postfix type declarations                               Brandl
- SF 3118  Revising the buffer protocol                            Oliphant, Banks
- SF 3119  Introducing Abstract Base Classes                       GvR, Talin
- SF 3120  Using UTF-8 as the default source encoding              von Löwis
- SA 3121  Extension Module Initialization and Finalization        von Löwis
- SR 3122  Delineation of the main module                          Cannon
- SF 3123  Making PyObject_HEAD conform to standard C              von Löwis
- SD 3124  Overloading, Generic Functions, Interfaces, and ...     Eby
- SR 3125  Remove Backslash Continuation                           Jewett
- SR 3126  Remove Implicit String Concatenation                    Jewett, Hettinger
- SF 3127  Integer Literal Support and Syntax                      Maupin
- SR 3128  BList: A Faster List-like Type                          Stutzbach
- SF 3129  Class Decorators                                        Winter
- SR 3130  Access to Current Module/Class/Function                 Jewett
- SF 3131  Supporting Non-ASCII Identifiers                        von Löwis
- SF 3132  Extended Iterable Unpacking                             Brandl
- SR 3133  Introducing Roles                                       Winter
- SF 3134  Exception Chaining and Embedded Tracebacks              Yee
- SF 3135  New Super                                               Spealman, Delaney, Ryan
- SR 3136  Labeled break and continue                              Chisholm
- SF 3137  Immutable Bytes and Mutable Buffer                      GvR
- SF 3138  String representation in Python 3000                    Ishimoto
- SR 3139  Cleaning out sys and the "interpreter" module           Peterson
- SR 3140  str(container) should call str(item), not repr(item)    Broytmann, Jewett
- SF 3141  A Type Hierarchy for Numbers                            Yasskin
- SR 3142  Add a "while" clause to generator expressions           Britton
- SD 3143  Standard daemon process library                         Finney
- SF 3144  IP Address Manipulation Library for the Python ...      Moody
- SW 3145  Asynchronous I/O For subprocess.Popen                   Pruitt, McCreary, Carlson
- SW 3146  Merging Unladen Swallow into CPython                    Winter, Yasskin, Kleckner
- SF 3147  PYC Repository Directories                              Warsaw
- SF 3148  futures - execute computations asynchronously           Quinlan
- SF 3149  ABI version tagged .so files                            Warsaw
- SD 3150  Statement local namespaces (aka "given" clause)         Coghlan
- SF 3151  Reworking the OS and IO exception hierarchy             Pitrou
- SD 3152  Cofunctions                                             Ewing
- SS 3153  Asynchronous IO support                                 Houtven
- SF 3154  Pickle protocol version 4                               Pitrou
- SF 3155  Qualified name for classes and functions                Pitrou
- SF 3156  Asynchronous IO Support Rebooted: the "asyncio" Module  GvR
-
- IF 3333  Python Web Server Gateway Interface v1.0.1              Eby
-
-
-
-

Reserved PEP Numbers

-
-     num  title                                                   owner
-     ---  -----                                                   -----
-     801  RESERVED                                                Warsaw
-
-
-
-

Key

-
-    S - Standards Track PEP
-    I - Informational PEP
-    P - Process PEP
-
-    A - Accepted proposal
-    R - Rejected proposal
-    W - Withdrawn proposal
-    D - Deferred proposal
-    F - Final proposal
-    A - Active proposal
-    D - Draft proposal
-    S - Superseded proposal
-
-
-
-

Owners

-
-    name                         email address
-    ----                         -------------
-    Aahz                         aahz at pythoncraft.com
-    Ahlstrom, James C.           jim at interet.com
-    Althoff, Jim                 james_althoff at i2.com
-    Altis, Kevin                 altis at semi-retired.com
-    Angelico, Chris              rosuav at gmail.com
-    Ascher, David                davida at activestate.com
-    Astrand, Peter               astrand at lysator.liu.se
-    Banks, Carl                  pythondev at aerojockey.com
-    Barrett, Paul                barrett at stsci.edu
-    Batista, Facundo             facundo at taniquetil.com.ar
-    Baxter, Anthony              anthony at interlink.com.au
-    Bellman, Thomas              bellman+pep-divmod@lysator.liu.se
-    Bendersky, Eli               eliben at gmail.com
-    Bethard, Steven              steven.bethard at gmail.com
-    Borini, Stefano              
-    Brandl, Georg                georg at python.org
-    Britton, Gerald              gerald.britton at gmail.com
-    Broytmann, Oleg              phd at phd.pp.ru
-    Bryon, Benoit                benoit at marmelune.net
-    Cannon, Brett                brett at python.org
-    Cappos, Justin               jcappos at poly.edu
-    Carlson, Josiah              jcarlson at uci.edu
-    Carroll,         W Isaac     icarroll at pobox.com
-    Chisholm, Matt               matt-python at theory.org
-    Coghlan, Nick                ncoghlan at gmail.com
-    Cole, Dave                   djc at object-craft.com.au
-    Colomiets, Paul              paul at colomiets.name
-    Craig, Christopher A.        python-pep at ccraig.org
-    Creighton, Laura             lac at strakt.com
-    D'Aprano, Steven             steve at pearwood.info
-    Delaney, Tim                 timothy.c.delaney at gmail.com
-    Diederich, Jack              jackdied at gmail.com
-    Dörwald, Walter              walter at livinglogic.de
-    Drake, Fred L., Jr.          fdrake at acm.org
-    Dubner, Michael P.           dubnerm at mindless.com
-    Dubois, Paul F.              paul at pfdubois.com
-    Eby, P.J.                    pje at telecommunity.com
-    Eby, Phillip J.              pje at telecommunity.com
-    Elliott, Micah               mde at tracos.org
-    Epler, Jeff                  jepler at unpythonic.net
-    Eppstein, David              eppstein at ics.uci.edu
-    Evans, Clark C.              cce at clarkevans.com
-    Ewing, Gregory               greg.ewing at canterbury.ac.nz
-    Ewing, Greg                  greg.ewing at canterbury.ac.nz
-    Faassen, Martijn             faassen at infrae.com
-    Finney, Ben                  ben+python@benfinney.id.au
-    Foord, Michael               michael at python.org
-    Furman, Ethan                ethan at stoneleaf.us
-    Gaynor, Alex                 alex.gaynor at gmail.com
-    Giacometti, Frédéric B.      fred at arakne.com
-    Gilbert, Scott               xscottg at yahoo.com
-    Goodger, David               goodger at python.org
-    Griffin, Grant               g2 at iowegian.com
-    Hammond, Mark                mhammond at skippinet.com.au
-    Harris, Peter                scav at blueyonder.co.uk
-    Hastings, Larry              larry at hastings.org
-    Heimes, Christian            christian at python.org
-    Heller, Thomas               theller at python.net
-    Hetland, Magnus Lie          magnus at hetland.org
-    Hettinger, Raymond           python at rcn.com
-    Hodgson, Neil                neilh at scintilla.org
-    Holth, Daniel                dholth at gmail.com
-    Houtven, Laurens Van         _ at lvh.cc
-    Hoyt, Ben                    benhoyt at gmail.com
-    Hudson, Michael              mwh at python.net
-    Hylton, Jeremy               jeremy at alum.mit.edu
-    Ishimoto, Atsuo              ishimoto--at--gembook.org
-    Jansen, Jack                 jack at cwi.nl
-    Jewett, Jim J.               jimjjewett at gmail.com
-    Jewett, Jim                  jimjjewett at gmail.com
-    Jones, Richard               richard at python.org
-    Kleckner, Reid               rnk at mit.edu
-    Koltsov, Stepan              yozh at mx1.ru
-    Krah, Stefan                 skrah at bytereef.org
-    Kreft, Sebastian             skreft at deezer.com
-    Krekel, Holger               holger at merlinux.eu
-    Kuchling, A.M.               amk at amk.ca
-    Kuppusamy, Trishank Karthik  tk47 at students.poly.edu
-    Landau, Joshua               joshua at landau.ws
-    Langa, Łukasz                lukasz at langa.pl
-    Lemburg, Marc-André          mal at lemburg.com
-    Lielens, Gregory             gregory.lielens at fft.be
-    Lindqvist, Björn             bjourne at gmail.com
-    von Löwis, Martin            martin at v.loewis.de
-    v. Löwis, Martin             martin at v.loewis.de
-    Lownds, Tony                 tony at lownds.com
-    Martelli, Alex               aleaxit at gmail.com
-    Martinot-Lagarde, Joseph     
-    Mastrodomenico, Lino         l.mastrodomenico at gmail.com
-    Maupin, Patrick              pmaupin at gmail.com
-    McClelland, Andrew           eternalsquire at comcast.net
-    McCreary, Charles R.         
-    McDonough, Chris             chrism at plope.com
-    McMillan, Gordon             gmcm at hypernet.com
-    McNamara, Andrew             andrewm at object-craft.com.au
-    Meyer, Mike                  mwm at mired.org
-    Meyer, Carl                  carl at oddbird.net
-    Mick, Trent                  trentm at activestate.com
-    Montanaro, Skip              skip at pobox.com
-    Moody, Peter                 pmoody at google.com
-    Moore, Paul                  gustav at morpheus.demon.co.uk
-    Natali, Charles-François     cf.natali at gmail.com
-    Noller, Jesse                jnoller at gmail.com
-    North, Ben                   ben at redfrontdoor.org
-    Norwitz, Neal                nnorwitz at gmail.com
-    Ochtman, Dirkjan             dirkjan at ochtman.nl
-    Oliphant, Travis             oliphant at ee.byu.edu
-    Orendorff, Jason             jason.orendorff at gmail.com
-    Oudkerk, Richard             r.m.oudkerk at googlemail.com
-    Oussoren, Ronald             ronaldoussoren at mac.com
-    Pedroni, Samuele             pedronis at python.org
-    Pelletier, Michel            michel at users.sourceforge.net
-    Peters, Tim                  tim at zope.com
-    Peterson, Benjamin           benjamin at python.org
-    Petrone, Jason               jp at demonseed.net
-    Pitrou, Antoine              solipsis at pitrou.net
-    Prescod, Paul                paul at prescod.net
-    Pruitt, (James) Eric         
-    Quinlan, Brian               brian at sweetapp.com
-    Reedy, Terry                 tjreedy at udel.edu
-    Regebro, Lennart             regebro at gmail.com
-    Reifschneider, Sean          jafo-pep at tummy.com
-    Reis, Christian R.           kiko at async.com.br
-    Riehl, Jonathan              jriehl at spaceship.com
-    Roberge, Andre               andre.roberge at gmail.com 
-    Ronacher, Armin              armin.ronacher at active-4.com
-    van Rossum, Guido (GvR)      guido at python.org
-    van Rossum, Just (JvR)       just at letterror.com
-    Rovito, Todd                 rovitotv at gmail.com
-    Ryan, Lie                    lie.1296 at gmail.com
-    Sajip, Vinay                 vinay_sajip at red-dove.com
-    Schemenauer, Neil            nas at arctrix.com
-    Schneider-Kamp, Peter        nowonder at nowonder.de
-    Selivanov, Yury              yselivanov at sprymix.com
-    Seo, Jiwon                   seojiwon at gmail.com
-    Shannon, Mark                mark at hotpy.org
-    Simpson, Cameron             cs at zip.com.au
-    Slodkowicz, Greg             jergosh at gmail.com
-    Smith, Nathaniel J.          njs at pobox.com
-    Smith, Kevin D.              kevin.smith at themorgue.org
-    Smith, Eric V.               eric at trueblade.com
-    Snow, Eric                   ericsnowcurrently at gmail.com
-    Spealman, Calvin             ironfroggy at gmail.com
-    Staley, Kerrick              mail at kerrickstaley.com
-    Stein, Greg                  gstein at lyra.org
-    Stinner, Victor              victor.stinner at gmail.com
-    Stufft, Donald               donald at stufft.io
-    Stutzbach, Daniel            daniel at stutzbachenterprises.com
-    Suzi, Roman                  rnd at onego.ru
-    Talin                        talin at acm.org
-    Taschuk, Steven              staschuk at telusplanet.net
-    Tirosh, Oren                 oren at hishome.net
-    Turnbull, Stephen J.         stephen at xemacs.org
-    Urban, Daniel                urban.dani+py@gmail.com
-    Vassalotti, Alexandre        alexandre at peadrop.com
-    Verdone, Mike                mike.verdone at gmail.com
-    Warnes, Gregory R.           gregory_r_warnes at groton.pfizer.com
-    Warsaw, Barry                barry at python.org
-    Way, Terence                 terry at wayforward.net
-    Wells, Cliff                 logiplexsoftware at earthlink.net
-    Whitley, Jervis              jervisau at gmail.com
-    Wilson, Greg                 gvwilson at ddj.com
-    Winter, Collin               collinwinter at google.com
-    Wouters, Thomas              thomas at python.org
-    Yasskin, Jeffrey             jyasskin at google.com
-    Yee, Ka-Ping                 ping at zesty.ca
-    Zadka, Moshe                 moshez at zadka.site.co.il
-    Zhu, Huaiyu                  hzhu at users.sourceforge.net
-    Ziadé, Tarek                 tarek at ziade.org
-
-
-
-

References

-
-    [1] PEP 1: PEP Purpose and Guidelines
-    [2] View PEP history online
-        http://hg.python.org/peps/
-
-
-
- - diff --git a/peps/tests/peps/pep-0012.html b/peps/tests/peps/pep-0012.html deleted file mode 100644 index e341e82f5..000000000 --- a/peps/tests/peps/pep-0012.html +++ /dev/null @@ -1,53 +0,0 @@ - - --- - - - - - - - - - - - - - - - - - -
PEP:12
Title:Sample reStructuredText PEP Template
Author:David Goodger <goodger at python.org>, -Barry Warsaw <barry at python.org>
Status:Active
Type:Process
Content-Type:text/x-rst
Created:05-Aug-2002
Post-History:30-Aug-2002
-
-
-

Contents

- -
-
-

Abstract

-

This PEP provides a boilerplate or sample template for creating your -own reStructuredText PEPs.

-
- - diff --git a/peps/tests/peps/pep-0012.rst b/peps/tests/peps/pep-0012.rst deleted file mode 100644 index 92a90835e..000000000 --- a/peps/tests/peps/pep-0012.rst +++ /dev/null @@ -1,33 +0,0 @@ -PEP: 12 -Title: Sample reStructuredText PEP Template -Author: David Goodger , - Barry Warsaw -Status: Active -Type: Process -Content-Type: text/x-rst -Created: 05-Aug-2002 -Post-History: 30-Aug-2002 - - -Abstract -======== - -This PEP provides a boilerplate or sample template for creating your -own reStructuredText PEPs. - - -Copyright -========= - -This document has been placed in the public domain. - - - -.. - Local Variables: - mode: indented-text - indent-tabs-mode: nil - sentence-end-double-space: t - fill-column: 70 - coding: utf-8 - End: diff --git a/peps/tests/peps/pep-0525.html b/peps/tests/peps/pep-0525.html deleted file mode 100644 index 55a756e0d..000000000 --- a/peps/tests/peps/pep-0525.html +++ /dev/null @@ -1,595 +0,0 @@ - - --- - - - - - - - - - - - - - - - - - - - - - - - - - -
PEP:525
Title:Asynchronous Generators
Version:$Revision$
Last-Modified:$Date$
Author:Yury Selivanov <yury at magic.io>
Discussions-To:<python-dev at python.org>
Status:Draft
Type:Standards Track
Content-Type:text/x-rst
Created:28-Jul-2016
Python-Version:3.6
Post-History:02-Aug-2016
-
- -
-

Abstract

-

PEP 492 introduced support for native coroutines and async/await -syntax to Python 3.5. It is proposed here to extend Python's -asynchronous capabilities by adding support for -asynchronous generators.

-
-
-

Rationale and Goals

-

Regular generators (introduced in PEP 255) enabled an elegant way of -writing complex data producers and have them behave like an iterator.

-

However, currently there is no equivalent concept for the asynchronous -iteration protocol (async for). This makes writing asynchronous -data producers unnecessarily complex, as one must define a class that -implements __aiter__ and __anext__ to be able to use it in -an async for statement.

-

Essentially, the goals and rationale for PEP 255, applied to the -asynchronous execution case, hold true for this proposal as well.

-

Performance is an additional point for this proposal: in our testing of -the reference implementation, asynchronous generators are 2x faster -than an equivalent implemented as an asynchronous iterator.

-

As an illustration of the code quality improvement, consider the -following class that prints numbers with a given delay once iterated:

-
-class Ticker:
-    """Yield numbers from 0 to `to` every `delay` seconds."""
-
-    def __init__(self, delay, to):
-        self.delay = delay
-        self.i = 0
-        self.to = to
-
-    def __aiter__(self):
-        return self
-
-    async def __anext__(self):
-        i = self.i
-        if i >= self.to:
-            raise StopAsyncIteration
-        self.i += 1
-        if i:
-            await asyncio.sleep(self.delay)
-        return i
-
-

The same can be implemented as a much simpler asynchronous generator:

-
-async def ticker(delay, to):
-    """Yield numbers from 0 to `to` every `delay` seconds."""
-    for i in range(to):
-        yield i
-        await asyncio.sleep(delay)
-
-
-
-

Specification

-

This proposal introduces the concept of asynchronous generators to -Python.

-

This specification presumes knowledge of the implementation of -generators and coroutines in Python (PEP 342, PEP 380 and PEP 492).

-
-

Asynchronous Generators

-

A Python generator is any function containing one or more yield -expressions:

-
-def func():            # a function
-    return
-
-def genfunc():         # a generator function
-    yield
-
-

We propose to use the same approach to define -asynchronous generators:

-
-async def coro():      # a coroutine function
-    await smth()
-
-async def asyncgen():  # an asynchronous generator function
-    await smth()
-    yield 42
-
-

The result of calling an asynchronous generator function is -an asynchronous generator object, which implements the asynchronous -iteration protocol defined in PEP 492.

-

It is a SyntaxError to have a non-empty return statement in an -asynchronous generator.

-
-
-

Support for Asynchronous Iteration Protocol

-

The protocol requires two special methods to be implemented:

-
    -
  1. An __aiter__ method returning an asynchronous iterator.
  2. -
  3. An __anext__ method returning an awaitable object, which uses -StopIteration exception to "yield" values, and -StopAsyncIteration exception to signal the end of the iteration.
  4. -
-

Asynchronous generators define both of these methods. Let's manually -iterate over a simple asynchronous generator:

-
-async def genfunc():
-    yield 1
-    yield 2
-
-gen = genfunc()
-
-assert gen.__aiter__() is gen
-
-assert await gen.__anext__() == 1
-assert await gen.__anext__() == 2
-
-await gen.__anext__()  # This line will raise StopAsyncIteration.
-
-
-
-

Finalization

-

PEP 492 requires an event loop or a scheduler to run coroutines. -Because asynchronous generators are meant to be used from coroutines, -they also require an event loop to run and finalize them.

-

Asynchronous generators can have try..finally blocks, as well as -async with. It is important to provide a guarantee that, even -when partially iterated, and then garbage collected, generators can -be safely finalized. For example:

-
-async def square_series(con, to):
-    async with con.transaction():
-        cursor = con.cursor(
-            'SELECT generate_series(0, $1) AS i', to)
-        async for row in cursor:
-            yield row['i'] ** 2
-
-async for i in square_series(con, 1000):
-    if i == 100:
-        break
-
-

The above code defines an asynchronous generator that uses -async with to iterate over a database cursor in a transaction. -The generator is then iterated over with async for, which interrupts -the iteration at some point.

-

The square_series() generator will then be garbage collected, -and without a mechanism to asynchronously close the generator, Python -interpreter would not be able to do anything.

-

To solve this problem we propose to do the following:

-
    -
  1. Implement an aclose method on asynchronous generators -returning a special awaitable. When awaited it -throws a GeneratorExit into the suspended generator and -iterates over it until either a GeneratorExit or -a StopAsyncIteration occur.

    -

    This is very similar to what the close() method does to regular -Python generators, except that an event loop is required to execute -aclose().

    -
  2. -
  3. Raise a RuntimeError, when an asynchronous generator executes -a yield expression in its finally block (using await -is fine, though):

    -
    -async def gen():
    -    try:
    -        yield
    -    finally:
    -        await asyncio.sleep(1)   # Can use 'await'.
    -
    -        yield                    # Cannot use 'yield',
    -                                 # this line will trigger a
    -                                 # RuntimeError.
    -
    -
  4. -
  5. Add two new methods to the sys module: -set_asyncgen_finalizer() and get_asyncgen_finalizer().

    -
  6. -
-

The idea behind sys.set_asyncgen_finalizer() is to allow event -loops to handle generators finalization, so that the end user -does not need to care about the finalization problem, and it just -works.

-

When an asynchronous generator is iterated for the first time, -it stores a reference to the current finalizer. If there is none, -a RuntimeError is raised. This provides a strong guarantee that -every asynchronous generator object will always have a finalizer -installed by the correct event loop.

-

When an asynchronous generator is about to be garbage collected, -it calls its cached finalizer. The assumption is that the finalizer -will schedule an aclose() call with the loop that was active -when the iteration started.

-

For instance, here is how asyncio is modified to allow safe -finalization of asynchronous generators:

-
-# asyncio/base_events.py
-
-class BaseEventLoop:
-
-    def run_forever(self):
-        ...
-        old_finalizer = sys.get_asyncgen_finalizer()
-        sys.set_asyncgen_finalizer(self._finalize_asyncgen)
-        try:
-            ...
-        finally:
-            sys.set_asyncgen_finalizer(old_finalizer)
-            ...
-
-    def _finalize_asyncgen(self, gen):
-        self.create_task(gen.aclose())
-
-

sys.set_asyncgen_finalizer() is thread-specific, so several event -loops running in parallel threads can use it safely.

-
-
-

Asynchronous Generator Object

-

The object is modeled after the standard Python generator object. -Essentially, the behaviour of asynchronous generators is designed -to replicate the behaviour of synchronous generators, with the only -difference in that the API is asynchronous.

-

The following methods and properties are defined:

-
    -
  1. agen.__aiter__(): Returns agen.

    -
  2. -
  3. agen.__anext__(): Returns an awaitable, that performs one -asynchronous generator iteration when awaited.

    -
  4. -
  5. agen.asend(val): Returns an awaitable, that pushes the -val object in the agen generator. When the agen has -not yet been iterated, val must be None.

    -

    Example:

    -
    -async def gen():
    -    await asyncio.sleep(0.1)
    -    v = yield 42
    -    print(v)
    -    await asyncio.sleep(0.2)
    -
    -g = gen()
    -
    -await g.asend(None)      # Will return 42 after sleeping
    -                         # for 0.1 seconds.
    -
    -await g.asend('hello')   # Will print 'hello' and
    -                         # raise StopAsyncIteration
    -                         # (after sleeping for 0.2 seconds.)
    -
    -
  6. -
  7. agen.athrow(typ, [val, [tb]]): Returns an awaitable, that -throws an exception into the agen generator.

    -

    Example:

    -
    -async def gen():
    -    try:
    -        await asyncio.sleep(0.1)
    -        yield 'hello'
    -    except ZeroDivisionError:
    -        await asyncio.sleep(0.2)
    -        yield 'world'
    -
    -g = gen()
    -v = await g.asend(None)
    -print(v)                # Will print 'hello' after
    -                        # sleeping for 0.1 seconds.
    -
    -v = await g.athrow(ZeroDivisionError)
    -print(v)                # Will print 'world' after
    -                        $ sleeping 0.2 seconds.
    -
    -
  8. -
  9. agen.aclose(): Returns an awaitable, that throws a -GeneratorExit exception into the generator. The awaitable can -either return a yielded value, if agen handled the exception, -or agen will be closed and the exception will propagate back -to the caller.

    -
  10. -
  11. agen.__name__ and agen.__qualname__: readable and writable -name and qualified name attributes.

    -
  12. -
  13. agen.ag_await: The object that agen is currently awaiting -on, or None. This is similar to the currently available -gi_yieldfrom for generators and cr_await for coroutines.

    -
  14. -
  15. agen.ag_frame, agen.ag_running, and agen.ag_code: -defined in the same way as similar attributes of standard generators.

    -
  16. -
-

StopIteration and StopAsyncIteration are not propagated out of -asynchronous generators, and are replaced with a RuntimeError.

-
-
-

Implementation Details

-

Asynchronous generator object (PyAsyncGenObject) shares the -struct layout with PyGenObject. In addition to that, the -reference implementation introduces three new objects:

-
    -
  1. PyAsyncGenASend: the awaitable object that implements -__anext__ and asend() methods.
  2. -
  3. PyAsyncGenAThrow: the awaitable object that implements -athrow() and aclose() methods.
  4. -
  5. _PyAsyncGenWrappedValue: every directly yielded object from an -asynchronous generator is implicitly boxed into this structure. This -is how the generator implementation can separate objects that are -yielded using regular iteration protocol from objects that are -yielded using asynchronous iteration protocol.
  6. -
-

PyAsyncGenASend and PyAsyncGenAThrow are awaitables (they have -__await__ methods returning self) and are coroutine-like objects -(implementing __iter__, __next__, send() and throw() -methods). Essentially, they control how asynchronous generators are -iterated:

-pep-0525-1.png -
-

PyAsyncGenASend and PyAsyncGenAThrow

-

PyAsyncGenASend is a coroutine-like object that drives __anext__ -and asend() methods and implements the asynchronous iteration -protocol.

-

agen.asend(val) and agen.__anext__() return instances of -PyAsyncGenASend (which hold references back to the parent -agen object.)

-

The data flow is defined as follows:

-
    -
  1. When PyAsyncGenASend.send(val) is called for the first time, -val is pushed to the parent agen object (using existing -facilities of PyGenObject.)

    -

    Subsequent iterations over the PyAsyncGenASend objects, push -None to agen.

    -

    When a _PyAsyncGenWrappedValue object is yielded, it -is unboxed, and a StopIteration exception is raised with the -unwrapped value as an argument.

    -
  2. -
  3. When PyAsyncGenASend.throw(*exc) is called for the first time, -*exc is throwed into the parent agen object.

    -

    Subsequent iterations over the PyAsyncGenASend objects, push -None to agen.

    -

    When a _PyAsyncGenWrappedValue object is yielded, it -is unboxed, and a StopIteration exception is raised with the -unwrapped value as an argument.

    -
  4. -
  5. return statements in asynchronous generators raise -StopAsyncIteration exception, which is propagated through -PyAsyncGenASend.send() and PyAsyncGenASend.throw() methods.

    -
  6. -
-

PyAsyncGenAThrow is very similar to PyAsyncGenASend. The only -difference is that PyAsyncGenAThrow.send(), when called first time, -throws an exception into the parent agen object (instead of pushing -a value into it.)

-
-
-
-

New Standard Library Functions and Types

-
    -
  1. types.AsyncGeneratorType -- type of asynchronous generator -object.
  2. -
  3. sys.set_asyncgen_finalizer() and sys.get_asyncgen_finalizer() -methods to set up asynchronous generators finalizers in event loops.
  4. -
  5. inspect.isasyncgen() and inspect.isasyncgenfunction() -introspection functions.
  6. -
-
-
-

Backwards Compatibility

-

The proposal is fully backwards compatible.

-

In Python 3.5 it is a SyntaxError to define an async def -function with a yield expression inside, therefore it's safe to -introduce asynchronous generators in 3.6.

-
-
-
-

Performance

-
-

Regular Generators

-

There is no performance degradation for regular generators. -The following micro benchmark runs at the same speed on CPython with -and without asynchronous generators:

-
-def gen():
-    i = 0
-    while i < 100000000:
-        yield i
-        i += 1
-
-list(gen())
-
-
-
-

Improvements over asynchronous iterators

-

The following micro-benchmark shows that asynchronous generators -are about 2.3x faster than asynchronous iterators implemented in -pure Python:

-
-N = 10 ** 7
-
-async def agen():
-    for i in range(N):
-        yield i
-
-class AIter:
-    def __init__(self):
-        self.i = 0
-
-    def __aiter__(self):
-        return self
-
-    async def __anext__(self):
-        i = self.i
-        if i >= N:
-            raise StopAsyncIteration
-        self.i += 1
-        return i
-
-
-
-
-

Design Considerations

-
-

aiter() and anext() builtins

-

Originally, PEP 492 defined __aiter__ as a method that should -return an awaitable object, resulting in an asynchronous iterator.

-

However, in CPython 3.5.2, __aiter__ was redefined to return -asynchronous iterators directly. To avoid breaking backwards -compatibility, it was decided that Python 3.6 will support both -ways: __aiter__ can still return an awaitable with -a DeprecationWarning being issued.

-

Because of this dual nature of __aiter__ in Python 3.6, we cannot -add a synchronous implementation of aiter() built-in. Therefore, -it is proposed to wait until Python 3.7.

-
-
-

Asynchronous list/dict/set comprehensions

-

Syntax for asynchronous comprehensions is unrelated to the asynchronous -generators machinery, and should be considered in a separate PEP.

-
-
-

Asynchronous yield from

-

While it is theoretically possible to implement yield from support -for asynchronous generators, it would require a serious redesign of the -generators implementation.

-

yield from is also less critical for asynchronous generators, since -there is no need provide a mechanism of implementing another coroutines -protocol on top of coroutines. And to compose asynchronous generators a -simple async for loop can be used:

-
-async def g1():
-    yield 1
-    yield 2
-
-async def g2():
-    async for v in g1():
-        yield v
-
-
-
-

Why the asend() and athrow() methods are necessary

-

They make it possible to implement concepts similar to -contextlib.contextmanager using asynchronous generators. -For instance, with the proposed design, it is possible to implement -the following pattern:

-
-@async_context_manager
-async def ctx():
-    await open()
-    try:
-        yield
-    finally:
-        await close()
-
-async with ctx():
-    await ...
-
-

Another reason is that it is possible to push data and throw exceptions -into asynchronous generators using the object returned from -__anext__ object, but it is hard to do that correctly. Adding -explicit asend() and athrow() will pave a safe way to -accomplish that.

-

In terms of implementation, asend() is a slightly more generic -version of __anext__, and athrow() is very similar to -aclose(). Therefore having these methods defined for asynchronous -generators does not add any extra complexity.

-
-
-
-

Example

-

A working example with the current reference implementation (will -print numbers from 0 to 9 with one second delay):

-
-async def ticker(delay, to):
-    for i in range(to):
-        yield i
-        await asyncio.sleep(delay)
-
-
-async def run():
-    async for i in ticker(1, 10):
-        print(i)
-
-
-import asyncio
-loop = asyncio.get_event_loop()
-try:
-    loop.run_until_complete(run())
-finally:
-    loop.close()
-
-
-
-

Implementation

-

The complete reference implementation is available at [1].

-
- - - diff --git a/peps/tests/peps/pep-3001-1.png b/peps/tests/peps/pep-3001-1.png deleted file mode 100644 index 7f63aea50..000000000 Binary files a/peps/tests/peps/pep-3001-1.png and /dev/null differ diff --git a/peps/tests/peps/pep-3001.html b/peps/tests/peps/pep-3001.html deleted file mode 100644 index bd2da8dbe..000000000 --- a/peps/tests/peps/pep-3001.html +++ /dev/null @@ -1,140 +0,0 @@ - - --- - - - - - - - - - - - - - - - - - - - - - -
PEP:3001
Title:Procedure for reviewing and improving standard library modules
Version:$Revision$
Last-Modified:$Date$
Author:Georg Brandl <georg at python.org>
Status:Withdrawn
Type:Process
Content-Type:text/x-rst
Created:05-Apr-2006
Post-History:
-
- -
-

Abstract

-

This PEP describes a procedure for reviewing and improving standard -library modules, especially those written in Python, making them ready -for Python 3000. There can be different steps of refurbishing, each -of which is described in a section below. Of course, not every step -has to be performed for every module.

-
-
-

Removal of obsolete modules

-

All modules marked as deprecated in 2.x versions should be removed for -Python 3000. The same applies to modules which are seen as obsolete today, -but are too widely used to be deprecated or removed. Python 3000 is the -big occasion to get rid of them. pep-3001-1.png

-

There will have to be a document listing all removed modules, together -with information on possible substitutes or alternatives. This infor- -mation will also have to be provided by the python3warn.py porting -helper script mentioned in PEP XXX.

-
-
-

Renaming modules

-

There are proposals for a "great stdlib renaming" introducing a hierarchic -library namespace or a top-level package from which to import standard -modules. That possibility aside, some modules' names are known to have -been chosen unwisely, a mistake which could never be corrected in the 2.x -series. Examples are names like "StringIO" or "Cookie". For Python 3000, -there will be the possibility to give those modules less confusing and -more conforming names.

-

Of course, each rename will have to be stated in the documentation of -the respective module and perhaps in the global document of Step 1. -Additionally, the python3warn.py script will recognize the old module -names and notify the user accordingly.

-

If the name change is made in time for another release of the Python 2.x -series, it is worth considering to introduce the new name in the 2.x -branch to ease transition.

-
-
-

Code cleanup

-

As most library modules written in Python have not been touched except -for bug fixes, following the policy of never changing a running system, -many of them may contain code that is not up to the newest language -features and could be rewritten in a more concise, modern Python.

-

PyChecker should run cleanly over the library. With a carefully tuned -configuration file, PyLint should also emit as few warnings as possible.

-

As long as these changes don't change the module's interface and behavior, -no documentation updates are necessary.

-
-
-

Enhancement of test and documentation coverage

-

Code coverage by unit tests varies greatly between modules. Each test -suite should be checked for completeness, and the remaining classic tests -should be converted to PyUnit (or whatever new shiny testing framework -comes with Python 3000, perhaps py.test?).

-

It should also be verified that each publicly visible function has a -meaningful docstring which ideally contains several doctests.

-

No documentation changes are necessary for enhancing test coverage.

-
-
-

Unification of module metadata

-

This is a small and probably not very important step. There have been -various attempts at providing author, version and similar metadata in -modules (such as a "__version__" global). Those could be standardized -and used throughout the library.

-

No documentation changes are necessary for this step, too.

-
-
-

Backwards incompatible bug fixes

-

Over the years, many bug reports have been filed which complained about -bugs in standard library modules, but have subsequently been closed as -"Won't fix" since a fix would have introduced a major incompatibility -which was not acceptable in the Python 2.x series. In Python 3000, the -fix can be applied if the interface per se is still acceptable.

-

Each slight behavioral change caused by such fixes must be mentioned in -the documentation, perhaps in a "Changed in Version 3.0" paragraph.

-
-
-

Interface changes

-

The last and most disruptive change is the overhaul of a module's public -interface. If a module's interface is to be changed, a justification -should be made beforehand, or a PEP should be written.

-

The change must be fully documented as "New in Version 3.0", and the -python3warn.py script must know about it.

-
-
-

References

-

None yet.

-
- - diff --git a/peps/tests/test_commands.py b/peps/tests/test_commands.py deleted file mode 100644 index 2579a5f99..000000000 --- a/peps/tests/test_commands.py +++ /dev/null @@ -1,56 +0,0 @@ -import io - -from bs4 import BeautifulSoup - -from django.test import TestCase, override_settings -from django.conf import settings -from django.core import serializers -from django.core.management import call_command - -import responses - -from pages.models import Image - -from . import FAKE_PEP_ARTIFACT - - -PEP_ARTIFACT_URL = 'https://example.net/fake-peps.tar.gz' - - -@override_settings(PEP_ARTIFACT_URL=PEP_ARTIFACT_URL) -class PEPManagementCommandTests(TestCase): - - def setUp(self): - responses.add( - responses.GET, - PEP_ARTIFACT_URL, - headers={'Last-Modified': 'Sun, 24 Feb 2019 18:01:42 GMT'}, - stream=True, - content_type='application/x-tar', - status=200, - body=open(FAKE_PEP_ARTIFACT, 'rb'), - ) - - @responses.activate - def test_generate_pep_pages_real_with_remote_artifact(self): - call_command('generate_pep_pages') - - @override_settings(PEP_ARTIFACT_URL=FAKE_PEP_ARTIFACT) - def test_generate_pep_pages_real_with_local_artifact(self): - call_command('generate_pep_pages') - - @responses.activate - def test_image_generated(self): - call_command('generate_pep_pages') - img = Image.objects.get(page__path='dev/peps/pep-3001/') - soup = BeautifulSoup(img.page.content.raw, 'lxml') - self.assertIn(settings.MEDIA_URL, soup.find('img')['src']) - - @responses.activate - def test_dump_pep_pages(self): - call_command('generate_pep_pages') - stdout = io.StringIO() - call_command('dump_pep_pages', stdout=stdout) - output = stdout.getvalue() - result = list(serializers.deserialize('json', output)) - self.assertGreater(len(result), 0) diff --git a/peps/tests/test_converters.py b/peps/tests/test_converters.py deleted file mode 100644 index 833bf7c0e..000000000 --- a/peps/tests/test_converters.py +++ /dev/null @@ -1,64 +0,0 @@ -from django.test import TestCase, override_settings -from django.core.exceptions import ImproperlyConfigured -from django.test.utils import captured_stdout - -from peps.converters import get_pep0_page, get_pep_page, add_pep_image - -from . import FAKE_PEP_REPO - - -class PEPConverterTests(TestCase): - - def test_source_link(self): - pep = get_pep_page(FAKE_PEP_REPO, '0525') - self.assertEqual(pep.title, 'PEP 525 -- Asynchronous Generators') - self.assertIn( - 'Source: https://github.com/python/peps/blob/master/pep-0525.txt', - pep.content.rendered - ) - - def test_source_link_rst(self): - pep = get_pep_page(FAKE_PEP_REPO, '0012') - self.assertEqual(pep.title, 'PEP 12 -- Sample reStructuredText PEP Template') - self.assertIn( - 'Source: https://github.com/python/peps/blob/master/pep-0012.rst', - pep.content.rendered - ) - - def test_invalid_pep_number(self): - with captured_stdout() as stdout: - get_pep_page(FAKE_PEP_REPO, '9999999') - self.assertRegex( - stdout.getvalue(), - r"PEP Path '(.*)9999999(.*)' does not exist, skipping" - ) - - def test_add_image_not_found(self): - with captured_stdout() as stdout: - add_pep_image(FAKE_PEP_REPO, '0525', '/path/that/does/not/exist') - self.assertRegex( - stdout.getvalue(), - r"Image Path '(.*)/path/that/does/not/exist(.*)' does not exist, skipping" - ) - - def test_html_do_not_prettify(self): - pep = get_pep_page(FAKE_PEP_REPO, '3001') - self.assertEqual( - pep.title, - 'PEP 3001 -- Procedure for reviewing and improving standard library modules' - ) - self.assertIn( - 'Title:' - 'Procedure for reviewing and improving ' - 'standard library modules\n', - pep.content.rendered - ) - - def test_strip_html_and_body_tags(self): - pep = get_pep_page(FAKE_PEP_REPO, '0525') - self.assertNotIn('', pep.content.rendered) - self.assertNotIn('', pep.content.rendered) - self.assertNotIn('', pep.content.rendered) - self.assertNotIn('', pep.content.rendered) diff --git a/prod-requirements.txt b/prod-requirements.txt index a99aad083..cdc543952 100644 --- a/prod-requirements.txt +++ b/prod-requirements.txt @@ -3,6 +3,6 @@ gunicorn==22.0.0 raven==6.10.0 # Heroku -Whitenoise==6.0.0 # 6.0.0 is latest version that supports Django 2.2 -django-storages==1.12.3 # 1.12.3 is latest version that supports Django 2.2 +Whitenoise==6.6.0 # 6.4.0 is first version that supports Django 4.2 +django-storages==1.14.4 # 1.14.4 is first version that supports Django 4.2 boto3==1.26.165 diff --git a/pydotorg/settings/base.py b/pydotorg/settings/base.py index a602dff7e..2c392b355 100644 --- a/pydotorg/settings/base.py +++ b/pydotorg/settings/base.py @@ -31,6 +31,12 @@ ) } +DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' +"""The default primary key field type for Django models. + +Required during the Django 2.2 -> 4.2 migration. +""" + # celery settings _REDIS_URL = config("REDIS_URL", default="redis://redis:6379/0") @@ -53,7 +59,6 @@ TIME_ZONE = 'UTC' LANGUAGE_CODE = 'en-us' USE_I18N = True -USE_L10N = True USE_TZ = True DATE_FORMAT = 'Y-m-d' @@ -74,7 +79,14 @@ STATICFILES_DIRS = [ os.path.join(BASE, 'static'), ] -STATICFILES_STORAGE = 'pipeline.storage.PipelineStorage' +STORAGES = { + "default": { + "BACKEND": "django.core.files.storage.FileSystemStorage", + }, + "staticfiles": { + "BACKEND": 'pipeline.storage.PipelineStorage', + }, +} STATICFILES_FINDERS = ( 'django.contrib.staticfiles.finders.FileSystemFinder', 'django.contrib.staticfiles.finders.AppDirectoriesFinder', @@ -98,6 +110,8 @@ ACCOUNT_UNIQUE_EMAIL = True ACCOUNT_EMAIL_VERIFICATION = 'mandatory' ACCOUNT_AUTHENTICATION_METHOD = 'username_email' +# TODO: Enable enumeration prevention +ACCOUNT_PREVENT_ENUMERATION = False SOCIALACCOUNT_EMAIL_REQUIRED = True SOCIALACCOUNT_EMAIL_VERIFICATION = True SOCIALACCOUNT_QUERY_EMAIL = True @@ -157,6 +171,7 @@ 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'pages.middleware.PageFallbackMiddleware', 'django.contrib.redirects.middleware.RedirectFallbackMiddleware', + 'allauth.account.middleware.AccountMiddleware', ] AUTH_USER_MODEL = 'users.User' @@ -207,7 +222,6 @@ 'minutes', 'nominations', 'pages', - 'peps', 'sponsors', 'successstories', 'users', @@ -215,11 +229,6 @@ 'allauth', 'allauth.account', - 'allauth.socialaccount', - #'allauth.socialaccount.providers.facebook', - #'allauth.socialaccount.providers.github', - #'allauth.socialaccount.providers.openid', - #'allauth.socialaccount.providers.twitter', # Tastypie needs the `users` app to be already loaded. 'tastypie', @@ -275,10 +284,6 @@ ### Registration mailing lists MAILING_LIST_PSF_MEMBERS = "psf-members-announce-request@python.org" -### PEP Repo Location -PEP_REPO_PATH = None -PEP_ARTIFACT_URL = 'https://pythondotorg-assets-staging.s3.amazonaws.com/fake-peps.tar.gz' - ### Fastly ### FASTLY_API_KEY = False # Set to Fastly API key in production to allow pages to # be purged on save diff --git a/pydotorg/settings/cabotage.py b/pydotorg/settings/cabotage.py index d73beb83c..4661fbf66 100644 --- a/pydotorg/settings/cabotage.py +++ b/pydotorg/settings/cabotage.py @@ -44,8 +44,14 @@ ] + MIDDLEWARE MEDIAFILES_LOCATION = 'media' -DEFAULT_FILE_STORAGE = 'custom_storages.storages.MediaStorage' -STATICFILES_STORAGE = 'custom_storages.storages.PipelineManifestStorage' +STORAGES = { + "default": { + "BACKEND": 'custom_storages.storages.MediaStorage', + }, + "staticfiles": { + "BACKEND": 'custom_storages.storages.PipelineManifestStorage', + }, +} EMAIL_HOST = config('EMAIL_HOST') EMAIL_HOST_USER = config('EMAIL_HOST_USER') diff --git a/pydotorg/settings/static.py b/pydotorg/settings/static.py index 3d93e113e..5dcbf6f92 100644 --- a/pydotorg/settings/static.py +++ b/pydotorg/settings/static.py @@ -21,5 +21,11 @@ ] + MIDDLEWARE MEDIAFILES_LOCATION = 'media' -DEFAULT_FILE_STORAGE = 'custom_storages.storages.MediaStorage' -STATICFILES_STORAGE = 'custom_storages.storages.PipelineManifestStorage' +STORAGES = { + "default": { + "BACKEND": 'custom_storages.storages.MediaStorage', + }, + "staticfiles": { + "BACKEND": 'custom_storages.storages.PipelineManifestStorage', + }, +} diff --git a/pydotorg/urls.py b/pydotorg/urls.py index f6ee8001d..f87ab496b 100644 --- a/pydotorg/urls.py +++ b/pydotorg/urls.py @@ -1,7 +1,8 @@ -from django.conf.urls import handler404, include +from django.conf.urls import handler404 from django.contrib import admin from django.contrib.staticfiles.urls import staticfiles_urlpatterns from django.conf.urls.static import static +from django.urls import include from django.urls import path, re_path from django.views.generic.base import TemplateView from django.conf import settings diff --git a/pydotorg/urls_api.py b/pydotorg/urls_api.py index 0c27699b1..4afc7122e 100644 --- a/pydotorg/urls_api.py +++ b/pydotorg/urls_api.py @@ -1,4 +1,4 @@ -from django.conf.urls import url +from django.urls import re_path from rest_framework import routers from tastypie.api import Api @@ -22,6 +22,6 @@ router.register(r'downloads/release_file', ReleaseFileViewSet) urlpatterns = [ - url(r'sponsors/logo-placement/', LogoPlacementeAPIList.as_view(), name="logo_placement_list"), - url(r'sponsors/sponsorship-assets/', SponsorshipAssetsAPIList.as_view(), name="assets_list"), + re_path(r'sponsors/logo-placement/', LogoPlacementeAPIList.as_view(), name="logo_placement_list"), + re_path(r'sponsors/sponsorship-assets/', SponsorshipAssetsAPIList.as_view(), name="assets_list"), ] diff --git a/runtime.txt b/runtime.txt deleted file mode 100644 index c9cbcea6f..000000000 --- a/runtime.txt +++ /dev/null @@ -1 +0,0 @@ -python-3.9.16 diff --git a/sponsors/__init__.py b/sponsors/__init__.py index 016e79088..e69de29bb 100644 --- a/sponsors/__init__.py +++ b/sponsors/__init__.py @@ -1 +0,0 @@ -default_app_config = 'sponsors.apps.SponsorsAppConfig' diff --git a/sponsors/admin.py b/sponsors/admin.py index e16cffbc6..dc7278c08 100644 --- a/sponsors/admin.py +++ b/sponsors/admin.py @@ -38,18 +38,22 @@ class AssetsInline(GenericTabularInline): has_delete_permission = lambda self, request, obj: False readonly_fields = ["internal_name", "user_submitted_info", "value"] + @admin.display( + description="Submitted information" + ) def value(self, obj=None): if not obj or not obj.value: return "" return obj.value - value.short_description = "Submitted information" + @admin.display( + description="Fullfilled data?", + boolean=True, + ) def user_submitted_info(self, obj=None): return bool(self.value(obj)) - user_submitted_info.short_description = "Fullfilled data?" - user_submitted_info.boolean = True @admin.register(SponsorshipProgram) @@ -235,10 +239,12 @@ class SponsorshipsInline(admin.TabularInline): can_delete = False extra = 0 + @admin.display( + description="ID" + ) def link(self, obj): url = reverse("admin:sponsors_sponsorship_change", args=[obj.id]) return mark_safe(f"{obj.id}") - link.short_description = "ID" @admin.register(Sponsor) @@ -489,10 +495,12 @@ def get_queryset(self, *args, **kwargs): qs = super().get_queryset(*args, **kwargs) return qs.select_related("sponsor", "package", "submited_by") + @admin.action( + description='Send notifications to selected' + ) def send_notifications(self, request, queryset): return views_admin.send_sponsorship_notifications_action(self, request, queryset) - send_notifications.short_description = 'Send notifications to selected' def get_readonly_fields(self, request, obj): readonly_fields = [ @@ -528,11 +536,16 @@ def get_readonly_fields(self, request, obj): return readonly_fields + @admin.display( + description="Sponsor" + ) def sponsor_link(self, obj): url = reverse("admin:sponsors_sponsor_change", args=[obj.sponsor.id]) return mark_safe(f"{obj.sponsor.name}") - sponsor_link.short_description = "Sponsor" + @admin.display( + description="Estimated cost" + ) def get_estimated_cost(self, obj): cost = None html = "This sponsorship has not customizations so there's no estimated cost" @@ -542,8 +555,10 @@ def get_estimated_cost(self, obj): html = f"{cost} USD
Important: {msg}" return mark_safe(html) - get_estimated_cost.short_description = "Estimated cost" + @admin.display( + description="Contract" + ) def get_contract(self, obj): if not obj.contract: return "---" @@ -551,7 +566,6 @@ def get_contract(self, obj): html = f"{obj.contract}" return mark_safe(html) - get_contract.short_description = "Contract" def get_urls(self): urls = super().get_urls() @@ -597,21 +611,30 @@ def get_urls(self): ] return my_urls + urls + @admin.display( + description="Name" + ) def get_sponsor_name(self, obj): return obj.sponsor.name - get_sponsor_name.short_description = "Name" + @admin.display( + description="Description" + ) def get_sponsor_description(self, obj): return obj.sponsor.description - get_sponsor_description.short_description = "Description" + @admin.display( + description="Landing Page URL" + ) def get_sponsor_landing_page_url(self, obj): return obj.sponsor.landing_page_url - get_sponsor_landing_page_url.short_description = "Landing Page URL" + @admin.display( + description="Web Logo" + ) def get_sponsor_web_logo(self, obj): html = "{% load thumbnail %}{% thumbnail sponsor.web_logo '150x150' format='PNG' quality=100 as im %}{% endthumbnail %}" template = Template(html) @@ -619,8 +642,10 @@ def get_sponsor_web_logo(self, obj): html = template.render(context) return mark_safe(html) - get_sponsor_web_logo.short_description = "Web Logo" + @admin.display( + description="Print Logo" + ) def get_sponsor_print_logo(self, obj): img = obj.sponsor.print_logo html = "" @@ -631,13 +656,17 @@ def get_sponsor_print_logo(self, obj): html = template.render(context) return mark_safe(html) if html else "---" - get_sponsor_print_logo.short_description = "Print Logo" + @admin.display( + description="Primary Phone" + ) def get_sponsor_primary_phone(self, obj): return obj.sponsor.primary_phone - get_sponsor_primary_phone.short_description = "Primary Phone" + @admin.display( + description="Mailing/Billing Address" + ) def get_sponsor_mailing_address(self, obj): sponsor = obj.sponsor city_row = ( @@ -655,8 +684,10 @@ def get_sponsor_mailing_address(self, obj): html += f"

{sponsor.postal_code}

" return mark_safe(html) - get_sponsor_mailing_address.short_description = "Mailing/Billing Address" + @admin.display( + description="Contacts" + ) def get_sponsor_contacts(self, obj): html = "" contacts = obj.sponsor.contacts.all() @@ -676,8 +707,10 @@ def get_sponsor_contacts(self, obj): html += "" return mark_safe(html) - get_sponsor_contacts.short_description = "Contacts" + @admin.display( + description="Added by User" + ) def get_custom_benefits_added_by_user(self, obj): benefits = obj.user_customizations["added_by_user"] if not benefits: @@ -688,8 +721,10 @@ def get_custom_benefits_added_by_user(self, obj): ) return mark_safe(html) - get_custom_benefits_added_by_user.short_description = "Added by User" + @admin.display( + description="Removed by User" + ) def get_custom_benefits_removed_by_user(self, obj): benefits = obj.user_customizations["removed_by_user"] if not benefits: @@ -700,7 +735,6 @@ def get_custom_benefits_removed_by_user(self, obj): ) return mark_safe(html) - get_custom_benefits_removed_by_user.short_description = "Removed by User" def rollback_to_editing_view(self, request, pk): return views_admin.rollback_to_editing_view(self, request, pk) @@ -747,6 +781,9 @@ def get_urls(self): ] return my_urls + urls + @admin.display( + description="Links" + ) def links(self, obj): clone_form = CloneApplicationConfigForm() configured_years = clone_form.configured_years @@ -768,8 +805,10 @@ def links(self, obj): html += f"
  • {preview_label}" html += "" return mark_safe(html) - links.short_description = "Links" + @admin.display( + description="Other configured years" + ) def other_years(self, obj): clone_form = CloneApplicationConfigForm() configured_years = clone_form.configured_years @@ -800,7 +839,6 @@ def other_years(self, obj): html += "
  • " html += "" return mark_safe(html) - other_years.short_description = "Other configured years" def clone_application_config(self, request): return views_admin.clone_application_config(self, request) @@ -828,10 +866,12 @@ def get_queryset(self, *args, **kwargs): qs = super().get_queryset(*args, **kwargs) return qs.select_related("sponsorship__sponsor") + @admin.display( + description="Revision" + ) def get_revision(self, obj): return obj.revision if obj.is_draft else "Final" - get_revision.short_description = "Revision" fieldsets = [ ( @@ -899,6 +939,9 @@ def get_readonly_fields(self, request, obj): return readonly_fields + @admin.display( + description="Contract document" + ) def document_link(self, obj): html, url, msg = "---", "", "" @@ -916,8 +959,10 @@ def document_link(self, obj): html = f'{msg}' return mark_safe(html) - document_link.short_description = "Contract document" + @admin.display( + description="Sponsorship" + ) def get_sponsorship_url(self, obj): if not obj.sponsorship: return "---" @@ -925,7 +970,6 @@ def get_sponsorship_url(self, obj): html = f"{obj.sponsorship}" return mark_safe(html) - get_sponsorship_url.short_description = "Sponsorship" def get_urls(self): urls = super().get_urls() @@ -1090,14 +1134,19 @@ def all_sponsorships(self): qs = Sponsorship.objects.all().select_related("package", "sponsor") return {sp.id: sp for sp in qs} + @admin.display( + description="Value" + ) def get_value(self, obj): html = obj.value if obj.value and getattr(obj.value, "url", None): html = f"{obj.value}" return mark_safe(html) - get_value.short_description = "Value" + @admin.display( + description="Associated with" + ) def get_related_object(self, obj): """ Returns the content_object as an URL and performs better because @@ -1115,11 +1164,12 @@ def get_related_object(self, obj): html = f"{content_object}" return mark_safe(html) - get_related_object.short_description = "Associated with" + @admin.action( + description="Export selected" + ) def export_assets_as_zipfile(self, request, queryset): return views_admin.export_assets_as_zipfile(self, request, queryset) - export_assets_as_zipfile.short_description = "Export selected" class GenericAssetChildModelAdmin(PolymorphicChildModelAdmin): diff --git a/sponsors/migrations/0103_alter_benefitfeature_polymorphic_ctype_and_more.py b/sponsors/migrations/0103_alter_benefitfeature_polymorphic_ctype_and_more.py new file mode 100644 index 000000000..e9eb9e3a2 --- /dev/null +++ b/sponsors/migrations/0103_alter_benefitfeature_polymorphic_ctype_and_more.py @@ -0,0 +1,47 @@ +# Generated by Django 4.2.11 on 2024-09-05 17:10 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('contenttypes', '0002_remove_content_type_name'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('sponsors', '0102_auto_20240509_2037'), + ] + + operations = [ + migrations.AlterField( + model_name='benefitfeature', + name='polymorphic_ctype', + field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype'), + ), + migrations.AlterField( + model_name='benefitfeatureconfiguration', + name='polymorphic_ctype', + field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype'), + ), + migrations.AlterField( + model_name='genericasset', + name='polymorphic_ctype', + field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype'), + ), + migrations.AlterField( + model_name='sponsor', + name='creator', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s_creator', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='sponsor', + name='last_modified_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s_modified', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='sponsorshipbenefit', + name='conflicts', + field=models.ManyToManyField(blank=True, help_text='For benefits that conflict with one another,', to='sponsors.sponsorshipbenefit', verbose_name='Conflicts'), + ), + ] diff --git a/sponsors/tests/test_api.py b/sponsors/tests/test_api.py index caabd6aa1..3575e59e6 100644 --- a/sponsors/tests/test_api.py +++ b/sponsors/tests/test_api.py @@ -41,7 +41,7 @@ def tearDown(self): sponsor.print_logo.delete() def test_list_logo_placement_as_expected(self): - response = self.client.get(self.url, HTTP_AUTHORIZATION=self.authorization) + response = self.client.get(self.url, headers={"authorization": self.authorization}) data = response.json() self.assertEqual(200, response.status_code) @@ -71,7 +71,7 @@ def test_list_logo_placement_as_expected(self): def test_invalid_token(self): Token.objects.all().delete() - response = self.client.get(self.url, HTTP_AUTHORIZATION=self.authorization) + response = self.client.get(self.url, headers={"authorization": self.authorization}) self.assertEqual(401, response.status_code) def test_superuser_user_have_permission_by_default(self): @@ -79,19 +79,19 @@ def test_superuser_user_have_permission_by_default(self): self.user.is_superuser = True self.user.is_staff = True self.user.save() - response = self.client.get(self.url, HTTP_AUTHORIZATION=self.authorization) + response = self.client.get(self.url, headers={"authorization": self.authorization}) self.assertEqual(200, response.status_code) def test_staff_have_permission_by_default(self): self.user.user_permissions.remove(self.permission) self.user.is_staff = True self.user.save() - response = self.client.get(self.url, HTTP_AUTHORIZATION=self.authorization) + response = self.client.get(self.url, headers={"authorization": self.authorization}) self.assertEqual(200, response.status_code) def test_user_must_have_required_permission(self): self.user.user_permissions.remove(self.permission) - response = self.client.get(self.url, HTTP_AUTHORIZATION=self.authorization) + response = self.client.get(self.url, headers={"authorization": self.authorization}) self.assertEqual(403, response.status_code) def test_filter_sponsorship_by_publisher(self): @@ -99,7 +99,7 @@ def test_filter_sponsorship_by_publisher(self): "publisher": PublisherChoices.PYPI.value, }) url = f"{self.url}?{querystring}" - response = self.client.get(url, HTTP_AUTHORIZATION=self.authorization) + response = self.client.get(url, headers={"authorization": self.authorization}) data = response.json() self.assertEqual(200, response.status_code) @@ -111,7 +111,7 @@ def test_filter_sponsorship_by_flight(self): "flight": LogoPlacementChoices.SIDEBAR.value, }) url = f"{self.url}?{querystring}" - response = self.client.get(url, HTTP_AUTHORIZATION=self.authorization) + response = self.client.get(url, headers={"authorization": self.authorization}) data = response.json() self.assertEqual(200, response.status_code) @@ -125,7 +125,7 @@ def test_bad_request_for_invalid_filters(self): "publisher": "invalid-publisher" }) url = f"{self.url}?{querystring}" - response = self.client.get(url, HTTP_AUTHORIZATION=self.authorization) + response = self.client.get(url, headers={"authorization": self.authorization}) data = response.json() self.assertEqual(400, response.status_code) @@ -162,7 +162,7 @@ def tearDown(self): def test_invalid_token(self): Token.objects.all().delete() - response = self.client.get(self.url, HTTP_AUTHORIZATION=self.authorization) + response = self.client.get(self.url, headers={"authorization": self.authorization}) self.assertEqual(401, response.status_code) def test_superuser_user_have_permission_by_default(self): @@ -170,30 +170,30 @@ def test_superuser_user_have_permission_by_default(self): self.user.is_superuser = True self.user.is_staff = True self.user.save() - response = self.client.get(self.url, HTTP_AUTHORIZATION=self.authorization) + response = self.client.get(self.url, headers={"authorization": self.authorization}) self.assertEqual(200, response.status_code) def test_staff_have_permission_by_default(self): self.user.user_permissions.remove(self.permission) self.user.is_staff = True self.user.save() - response = self.client.get(self.url, HTTP_AUTHORIZATION=self.authorization) + response = self.client.get(self.url, headers={"authorization": self.authorization}) self.assertEqual(200, response.status_code) def test_user_must_have_required_permission(self): self.user.user_permissions.remove(self.permission) - response = self.client.get(self.url, HTTP_AUTHORIZATION=self.authorization) + response = self.client.get(self.url, headers={"authorization": self.authorization}) self.assertEqual(403, response.status_code) def test_bad_request_if_no_internal_name(self): url = reverse_lazy("assets_list") - response = self.client.get(url, HTTP_AUTHORIZATION=self.authorization) + response = self.client.get(url, headers={"authorization": self.authorization}) self.assertEqual(400, response.status_code) self.assertIn("internal_name", response.json()) def test_list_assets_by_internal_name(self): # by default exclude assets with no value - response = self.client.get(self.url, HTTP_AUTHORIZATION=self.authorization) + response = self.client.get(self.url, headers={"authorization": self.authorization}) data = response.json() self.assertEqual(200, response.status_code) self.assertEqual(0, len(data)) @@ -202,7 +202,7 @@ def test_list_assets_by_internal_name(self): self.txt_asset.value = "Text Content" self.txt_asset.save() - response = self.client.get(self.url, HTTP_AUTHORIZATION=self.authorization) + response = self.client.get(self.url, headers={"authorization": self.authorization}) data = response.json() self.assertEqual(1, len(data)) @@ -216,7 +216,7 @@ def test_list_assets_by_internal_name(self): def test_enable_to_filter_by_assets_with_no_value_via_querystring(self): self.url += "&list_empty=true" - response = self.client.get(self.url, HTTP_AUTHORIZATION=self.authorization) + response = self.client.get(self.url, headers={"authorization": self.authorization}) data = response.json() self.assertEqual(1, len(data)) @@ -230,7 +230,7 @@ def test_serialize_img_value_as_url_to_image(self): self.img_asset.save() url = reverse_lazy("assets_list") + f"?internal_name={self.img_asset.internal_name}" - response = self.client.get(url, HTTP_AUTHORIZATION=self.authorization) + response = self.client.get(url, headers={"authorization": self.authorization}) data = response.json() self.assertEqual(1, len(data)) diff --git a/static/js/plugins/IE7.js b/static/js/plugins/IE7.js old mode 100755 new mode 100644 index ba86e3ae0..2884c7d6b --- a/static/js/plugins/IE7.js +++ b/static/js/plugins/IE7.js @@ -12,7 +12,7 @@ Unknown W Brackets, Benjamin Westfarer, Rob Eberhardt, Bill Edney, Kevin Newman, James Crompton, Matthew Mastracci, Doug Wright, Richard York, Kenneth Kolano, MegaZone, - Thomas Verelst, Mark 'Tarquin' Wilton-Jones, Rainer hlfors, + Thomas Verelst, Mark 'Tarquin' Wilton-Jones, Rainer Åhlfors, David Zulaica, Ken Kolano, Kevin Newman, Sjoerd Visscher, Ingo Chao */ @@ -2406,3 +2406,4 @@ IE7.loaded = true; })(); })(this, document); + diff --git a/static/js/plugins/IE9.js b/static/js/plugins/IE9.js old mode 100755 new mode 100644 index 4d99fd69e..9a50014ed --- a/static/js/plugins/IE9.js +++ b/static/js/plugins/IE9.js @@ -14,7 +14,7 @@ Unknown W Brackets, Benjamin Westfarer, Rob Eberhardt, Bill Edney, Kevin Newman, James Crompton, Matthew Mastracci, Doug Wright, Richard York, Kenneth Kolano, MegaZone, - Thomas Verelst, Mark 'Tarquin' Wilton-Jones, Rainer hlfors, + Thomas Verelst, Mark 'Tarquin' Wilton-Jones, Rainer Åhlfors, David Zulaica, Ken Kolano, Kevin Newman, Sjoerd Visscher, Ingo Chao */ diff --git a/successstories/__init__.py b/successstories/__init__.py index e2c2c1446..e69de29bb 100644 --- a/successstories/__init__.py +++ b/successstories/__init__.py @@ -1 +0,0 @@ -default_app_config = 'successstories.apps.SuccessstoriesAppConfig' diff --git a/successstories/admin.py b/successstories/admin.py index fdde8878c..bc15d2d11 100644 --- a/successstories/admin.py +++ b/successstories/admin.py @@ -24,6 +24,8 @@ def get_list_display(self, request): fields = list(super().get_list_display(request)) return fields + ['show_link', 'is_published', 'featured'] + @admin.display( + description='View on site' + ) def show_link(self, obj): return format_html(f'\U0001F517') - show_link.short_description = 'View on site' diff --git a/successstories/migrations/0012_alter_story_creator_alter_story_last_modified_by.py b/successstories/migrations/0012_alter_story_creator_alter_story_last_modified_by.py new file mode 100644 index 000000000..dee246421 --- /dev/null +++ b/successstories/migrations/0012_alter_story_creator_alter_story_last_modified_by.py @@ -0,0 +1,26 @@ +# Generated by Django 4.2.11 on 2024-09-05 17:10 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('successstories', '0011_auto_20220127_1923'), + ] + + operations = [ + migrations.AlterField( + model_name='story', + name='creator', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s_creator', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='story', + name='last_modified_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s_modified', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/successstories/tests/test_models.py b/successstories/tests/test_models.py index de5c0d577..418d27062 100644 --- a/successstories/tests/test_models.py +++ b/successstories/tests/test_models.py @@ -15,12 +15,13 @@ def test_published(self): self.assertEqual(len(Story.objects.published()), 2) def test_draft(self): - self.assertQuerysetEqual(Story.objects.draft(), - [f'']) + draft_stories = Story.objects.draft() + self.assertTrue(all(story.name == 'Fraft Story' for story in draft_stories)) def test_featured(self): - self.assertQuerysetEqual(Story.objects.featured(), - [f'']) + featured_stories = Story.objects.featured() + expected_repr = [f''] + self.assertQuerysetEqual(featured_stories, expected_repr, transform=repr) def test_get_admin_url(self): self.assertEqual(self.story1.get_admin_url(), diff --git a/templates/base.html b/templates/base.html index b9f3df9c6..578dc1204 100644 --- a/templates/base.html +++ b/templates/base.html @@ -66,7 +66,7 @@ {# Tile icon for Win8 (144x144 + tile color) #} - + diff --git a/templates/components/pep-widget.html b/templates/components/pep-widget.html deleted file mode 100644 index bba29ea2d..000000000 --- a/templates/components/pep-widget.html +++ /dev/null @@ -1,19 +0,0 @@ -{% load peps %} -
    - -

    - >>> Python Enhancement Proposals (PEPs): The future of Python is discussed here. - -

    - - - {# This isn't that awesome, commenting out for now #} - {% comment %} - {% get_newest_pep_pages as peps %} - - {% endcomment %} -
    \ No newline at end of file diff --git a/templates/python/documentation.html b/templates/python/documentation.html index 7db3662d2..e301b0010 100644 --- a/templates/python/documentation.html +++ b/templates/python/documentation.html @@ -106,7 +106,4 @@

    P - - {% include 'components/pep-widget.html' %} - {% endblock content %} diff --git a/templates/python/index.html b/templates/python/index.html index ac8b191df..753a53407 100644 --- a/templates/python/index.html +++ b/templates/python/index.html @@ -85,8 +85,6 @@ - {% include 'components/pep-widget.html' %} - {% include 'components/psf-widget.html' %} {% endblock content %} diff --git a/users/__init__.py b/users/__init__.py index 1bf67ae9c..e69de29bb 100644 --- a/users/__init__.py +++ b/users/__init__.py @@ -1 +0,0 @@ -default_app_config = 'users.apps.UsersAppConfig' diff --git a/users/admin.py b/users/admin.py index 1c003655c..36d7e30f3 100644 --- a/users/admin.py +++ b/users/admin.py @@ -26,7 +26,7 @@ class ApiKeyInline(TastypieApiKeyInline): @admin.register(User) class UserAdmin(BaseUserAdmin): - inlines = BaseUserAdmin.inlines + [ApiKeyInline, MembershipInline] + inlines = BaseUserAdmin.inlines + (ApiKeyInline, MembershipInline,) fieldsets = ( (None, {'fields': ('username', 'password')}), (_('Personal info'), {'fields': ( @@ -44,9 +44,11 @@ class UserAdmin(BaseUserAdmin): def has_add_permission(self, request): return False + @admin.display( + description='Name' + ) def full_name(self, obj): return obj.get_full_name() - full_name.short_description = 'Name' @admin.register(Membership) diff --git a/users/migrations/0015_alter_user_first_name.py b/users/migrations/0015_alter_user_first_name.py new file mode 100644 index 000000000..ac7715204 --- /dev/null +++ b/users/migrations/0015_alter_user_first_name.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.11 on 2024-09-05 17:10 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0014_auto_20210801_2332'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='first_name', + field=models.CharField(blank=True, max_length=150, verbose_name='first name'), + ), + ] diff --git a/users/tests/test_forms.py b/users/tests/test_forms.py index 10ab95e32..897f41d6c 100644 --- a/users/tests/test_forms.py +++ b/users/tests/test_forms.py @@ -2,6 +2,7 @@ from django.test import TestCase from allauth.account.forms import SignupForm +from allauth.account.models import EmailAddress from users.forms import UserProfileForm, MembershipForm @@ -50,14 +51,16 @@ def test_duplicate_username(self): self.assertIn('username', form.errors) def test_duplicate_email(self): - User.objects.create_user('test1', 'test@example.com', 'testpass') + user = User.objects.create_user('test1', 'test@example.com', 'testpass') + EmailAddress.objects.create(user=user, email="test@example.com") - form = SignupForm({ + form = SignupForm(data={ 'username': 'username2', 'email': 'test@example.com', 'password1': 'password', - 'password2': 'password' + 'password2': 'password', }) + self.assertFalse(form.is_valid()) self.assertIn('email', form.errors) @@ -92,13 +95,8 @@ def test_non_ascii_username(self): 'password2': 'password', }) self.assertFalse(form.is_valid()) - self.assertEqual( - form.errors['username'], - [ - 'Enter a valid username. This value may contain only ' - 'English letters, numbers, and @/./+/-/_ characters.' - ] - ) + expected_error = 'Enter a valid username. This value may contain only unaccented lowercase a-z and uppercase A-Z letters, numbers, and @/./+/-/_ characters.' + self.assertIn(expected_error, form.errors['username']) def test_user_membership(self): form = MembershipForm({ diff --git a/users/tests/test_views.py b/users/tests/test_views.py index 13c226e5f..83b8330f9 100644 --- a/users/tests/test_views.py +++ b/users/tests/test_views.py @@ -2,7 +2,7 @@ from django.conf import settings from django.contrib.auth import get_user_model from django.urls import reverse -from django.test import TestCase +from django.test import TestCase, override_settings from sponsors.forms import SponsorUpdateForm, SponsorRequiredAssetsForm from sponsors.models import Sponsorship, RequiredTextAssetConfiguration, SponsorBenefit @@ -11,8 +11,6 @@ from users.factories import UserFactory from users.models import Membership -from ..factories import MembershipFactory - User = get_user_model() @@ -245,7 +243,7 @@ def test_user_duplicate_username_email(self): response, 'A user with that username already exists.' ) self.assertContains( - response, 'A user is already registered with this e-mail address.' + response, 'A user is already registered with this email address.' ) def test_usernames(self): diff --git a/work_groups/__init__.py b/work_groups/__init__.py index ef2fbcce9..e69de29bb 100644 --- a/work_groups/__init__.py +++ b/work_groups/__init__.py @@ -1 +0,0 @@ -default_app_config = 'work_groups.apps.WorkGroupsAppConfig' diff --git a/work_groups/migrations/0005_alter_workgroup_creator_and_more.py b/work_groups/migrations/0005_alter_workgroup_creator_and_more.py new file mode 100644 index 000000000..a316aa482 --- /dev/null +++ b/work_groups/migrations/0005_alter_workgroup_creator_and_more.py @@ -0,0 +1,26 @@ +# Generated by Django 4.2.11 on 2024-09-05 17:10 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('work_groups', '0004_auto_20180705_0352'), + ] + + operations = [ + migrations.AlterField( + model_name='workgroup', + name='creator', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s_creator', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='workgroup', + name='last_modified_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s_modified', to=settings.AUTH_USER_MODEL), + ), + ]