You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@allura.apache.org by ke...@apache.org on 2022/02/24 18:08:52 UTC

[allura] 03/04: [#8415] py2 removal - py37-plus pyupgrade run

This is an automated email from the ASF dual-hosted git repository.

kentontaylor pushed a commit to branch kt/8415
in repository https://gitbox.apache.org/repos/asf/allura.git

commit 9af4b1399b2da8bfc97b5cce2892355e1ac480ad
Author: Kenton Taylor <kt...@slashdotmedia.com>
AuthorDate: Thu Feb 24 16:09:53 2022 +0000

    [#8415] py2 removal - py37-plus pyupgrade run
---
 Allura/allura/__init__.py                          |   2 -
 Allura/allura/app.py                               |  25 ++-
 Allura/allura/command/base.py                      |   6 +-
 Allura/allura/command/create_neighborhood.py       |   2 +-
 Allura/allura/command/create_trove_categories.py   |   2 +-
 Allura/allura/command/script.py                    |   1 -
 Allura/allura/command/show_models.py               |  27 ++-
 Allura/allura/command/taskd.py                     |   8 +-
 Allura/allura/command/taskd_cleanup.py             |   3 +-
 Allura/allura/config/__init__.py                   |   2 -
 Allura/allura/config/app_cfg.py                    |  10 +-
 Allura/allura/config/environment.py                |   2 -
 Allura/allura/config/middleware.py                 |   4 +-
 Allura/allura/controllers/__init__.py              |   2 -
 Allura/allura/controllers/attachments.py           |   2 +-
 Allura/allura/controllers/auth.py                  |  19 +-
 Allura/allura/controllers/base.py                  |   6 +-
 Allura/allura/controllers/basetest_project_root.py |  16 +-
 Allura/allura/controllers/discuss.py               |  16 +-
 Allura/allura/controllers/error.py                 |   4 +-
 Allura/allura/controllers/feed.py                  |  12 +-
 Allura/allura/controllers/newforge.py              |   2 +-
 Allura/allura/controllers/project.py               |  32 +--
 Allura/allura/controllers/repository.py            |  28 +--
 Allura/allura/controllers/rest.py                  |  22 +--
 Allura/allura/controllers/root.py                  |   6 +-
 Allura/allura/controllers/search.py                |   2 +-
 Allura/allura/controllers/site_admin.py            |  26 ++-
 Allura/allura/controllers/task.py                  |   4 +-
 Allura/allura/controllers/trovecategories.py       |  20 +-
 Allura/allura/ext/admin/admin_main.py              |  25 ++-
 Allura/allura/ext/admin/widgets.py                 |   3 +-
 .../ext/personal_dashboard/dashboard_main.py       |   1 -
 Allura/allura/ext/project_home/project_main.py     |   2 +-
 Allura/allura/ext/search/search_main.py            |   1 -
 Allura/allura/ext/user_profile/user_main.py        |   4 +-
 Allura/allura/lib/__init__.py                      |   2 -
 Allura/allura/lib/app_globals.py                   |  26 ++-
 Allura/allura/lib/base.py                          |   4 +-
 Allura/allura/lib/custom_middleware.py             |  46 ++---
 Allura/allura/lib/decorators.py                    |  13 +-
 Allura/allura/lib/diff.py                          |   2 +-
 Allura/allura/lib/exceptions.py                    |   3 +-
 Allura/allura/lib/gravatar.py                      |   2 +-
 Allura/allura/lib/helpers.py                       |  81 ++++----
 Allura/allura/lib/import_api.py                    |   6 +-
 Allura/allura/lib/macro.py                         |  26 +--
 Allura/allura/lib/mail_util.py                     |  24 +--
 Allura/allura/lib/markdown_extensions.py           |   6 +-
 Allura/allura/lib/multifactor.py                   |  19 +-
 Allura/allura/lib/patches.py                       |   2 +-
 Allura/allura/lib/phone/__init__.py                |   2 +-
 Allura/allura/lib/plugin.py                        |  51 +++--
 Allura/allura/lib/project_create_helpers.py        |   7 +-
 Allura/allura/lib/repository.py                    |   6 +-
 Allura/allura/lib/search.py                        |  17 +-
 Allura/allura/lib/security.py                      |  14 +-
 Allura/allura/lib/solr.py                          |   8 +-
 Allura/allura/lib/spam/__init__.py                 |   2 +-
 Allura/allura/lib/spam/stopforumspamfilter.py      |   6 +-
 Allura/allura/lib/utils.py                         |  41 ++--
 Allura/allura/lib/validators.py                    |  14 +-
 Allura/allura/lib/widgets/auth_widgets.py          |   2 +-
 Allura/allura/lib/widgets/discuss.py               |  42 ++--
 Allura/allura/lib/widgets/form_fields.py           |  47 ++---
 Allura/allura/lib/widgets/forms.py                 |  59 +++---
 Allura/allura/lib/widgets/project_list.py          |  17 +-
 Allura/allura/lib/widgets/repo.py                  |   3 +-
 Allura/allura/lib/widgets/search.py                |   5 +-
 Allura/allura/lib/widgets/subscriptions.py         |   3 +-
 Allura/allura/lib/widgets/user_profile.py          |   4 +-
 Allura/allura/model/__init__.py                    |   2 -
 Allura/allura/model/artifact.py                    |  40 ++--
 Allura/allura/model/attachments.py                 |   2 +-
 Allura/allura/model/auth.py                        |  26 +--
 Allura/allura/model/discuss.py                     |  26 ++-
 Allura/allura/model/filesystem.py                  |   9 +-
 Allura/allura/model/index.py                       |   8 +-
 Allura/allura/model/monq_model.py                  |   2 +-
 Allura/allura/model/multifactor.py                 |   4 +-
 Allura/allura/model/neighborhood.py                |   2 +-
 Allura/allura/model/notification.py                |  11 +-
 Allura/allura/model/oauth.py                       |  10 +-
 Allura/allura/model/project.py                     |  29 ++-
 Allura/allura/model/repo_refresh.py                |  10 +-
 Allura/allura/model/repository.py                  |  68 ++++---
 Allura/allura/model/session.py                     |   8 +-
 Allura/allura/model/stats.py                       |   3 +-
 Allura/allura/model/timeline.py                    |   8 +-
 Allura/allura/model/types.py                       |   6 +-
 Allura/allura/model/webhook.py                     |  10 +-
 Allura/allura/scripts/create_sitemap_files.py      |   2 -
 Allura/allura/scripts/delete_projects.py           |   2 +-
 Allura/allura/scripts/refresh_last_commits.py      |   2 +-
 Allura/allura/scripts/refreshrepo.py               |   2 +-
 Allura/allura/scripts/scripttask.py                |   2 +-
 Allura/allura/scripts/trac_export.py               |  10 +-
 Allura/allura/tasks/activity_tasks.py              |   2 +-
 Allura/allura/tasks/event_tasks.py                 |   2 +-
 Allura/allura/tasks/export_tasks.py                |   2 +-
 Allura/allura/tasks/index_tasks.py                 |   4 +-
 Allura/allura/tasks/mail_tasks.py                  |  12 +-
 Allura/allura/templates/__init__.py                |   3 -
 .../templates_responsive/responsive_overrides.py   |   2 +-
 Allura/allura/tests/__init__.py                    |   2 -
 Allura/allura/tests/decorators.py                  |  12 +-
 Allura/allura/tests/functional/__init__.py         |   3 -
 Allura/allura/tests/functional/test_admin.py       |  22 +--
 Allura/allura/tests/functional/test_auth.py        | 163 ++++++++-------
 Allura/allura/tests/functional/test_discuss.py     |  34 ++--
 Allura/allura/tests/functional/test_feeds.py       |   2 +-
 Allura/allura/tests/functional/test_home.py        |   9 +-
 Allura/allura/tests/functional/test_nav.py         |   4 +-
 .../allura/tests/functional/test_neighborhood.py   | 219 ++++++++++-----------
 .../tests/functional/test_personal_dashboard.py    |   3 +-
 Allura/allura/tests/functional/test_rest.py        |  22 +--
 Allura/allura/tests/functional/test_root.py        |  10 +-
 Allura/allura/tests/functional/test_search.py      |   2 +-
 Allura/allura/tests/functional/test_site_admin.py  |  59 +++---
 .../allura/tests/functional/test_user_profile.py   |   5 +-
 Allura/allura/tests/model/__init__.py              |   3 -
 Allura/allura/tests/model/test_artifact.py         |   6 +-
 Allura/allura/tests/model/test_auth.py             |   6 +-
 Allura/allura/tests/model/test_discussion.py       |   7 +-
 Allura/allura/tests/model/test_filesystem.py       |   4 -
 Allura/allura/tests/model/test_neighborhood.py     |   2 -
 Allura/allura/tests/model/test_notification.py     |   4 +-
 Allura/allura/tests/model/test_oauth.py            |   2 -
 Allura/allura/tests/model/test_project.py          |   2 -
 Allura/allura/tests/model/test_repo.py             |   9 +-
 Allura/allura/tests/model/test_timeline.py         |   2 +-
 .../tests/scripts/test_create_sitemap_files.py     |   2 +-
 .../allura/tests/scripts/test_delete_projects.py   |  17 +-
 Allura/allura/tests/scripts/test_misc_scripts.py   |   2 +-
 Allura/allura/tests/scripts/test_reindexes.py      |   4 +-
 .../tests/templates/jinja_master/test_lib.py       |   4 +-
 Allura/allura/tests/test_app.py                    |  10 +-
 Allura/allura/tests/test_commands.py               |  23 +--
 Allura/allura/tests/test_decorators.py             |   6 +-
 Allura/allura/tests/test_diff.py                   |   1 -
 Allura/allura/tests/test_globals.py                |  13 +-
 Allura/allura/tests/test_helpers.py                |  23 +--
 Allura/allura/tests/test_mail_util.py              |  26 ++-
 Allura/allura/tests/test_markdown.py               |   2 -
 Allura/allura/tests/test_middlewares.py            |   2 +-
 Allura/allura/tests/test_multifactor.py            |  20 +-
 Allura/allura/tests/test_patches.py                |   2 -
 Allura/allura/tests/test_plugin.py                 |  16 +-
 Allura/allura/tests/test_security.py               |   2 +-
 Allura/allura/tests/test_tasks.py                  |  13 +-
 Allura/allura/tests/test_utils.py                  |  10 +-
 Allura/allura/tests/test_validators.py             |   4 +-
 Allura/allura/tests/test_webhooks.py               |  73 ++++---
 Allura/allura/tests/unit/__init__.py               |   6 +-
 .../test_discussion_moderation_controller.py       |   4 +-
 Allura/allura/tests/unit/factories.py              |   2 +-
 Allura/allura/tests/unit/phone/test_nexmo.py       |   2 +-
 .../allura/tests/unit/phone/test_phone_service.py  |   2 +-
 Allura/allura/tests/unit/spam/test_akismet.py      |  12 +-
 Allura/allura/tests/unit/spam/test_spam_filter.py  |   6 +-
 .../allura/tests/unit/spam/test_stopforumspam.py   |   4 +-
 Allura/allura/tests/unit/test_app.py               |   4 +-
 Allura/allura/tests/unit/test_helpers/test_ago.py  |   2 +-
 .../tests/unit/test_helpers/test_set_context.py    |   8 +-
 .../allura/tests/unit/test_ldap_auth_provider.py   |  10 +-
 Allura/allura/tests/unit/test_mixins.py            |   2 +-
 Allura/allura/tests/unit/test_post_model.py        |   4 +-
 Allura/allura/tests/unit/test_repo.py              |   2 +-
 Allura/allura/tests/unit/test_session.py           |   2 -
 Allura/allura/version.py                           |   1 -
 Allura/allura/webhooks.py                          |  25 ++-
 Allura/allura/websetup/__init__.py                 |   2 -
 Allura/allura/websetup/bootstrap.py                |   6 +-
 Allura/allura/websetup/schema.py                   |   4 +-
 Allura/docs/conf.py                                |   2 -
 Allura/ldap-setup.py                               |   4 +-
 Allura/ldap-userconfig.py                          |   1 -
 Allura/setup.py                                    |   4 +-
 AlluraTest/alluratest/controller.py                |  16 +-
 AlluraTest/alluratest/smtp_debug.py                |   6 +-
 AlluraTest/alluratest/test_syntax.py               |  10 +-
 AlluraTest/alluratest/validation.py                |  32 ++-
 ForgeActivity/forgeactivity/main.py                |  13 +-
 .../forgeactivity/tests/functional/test_rest.py    |   4 +-
 .../forgeactivity/tests/functional/test_root.py    |  13 +-
 ForgeActivity/forgeactivity/widgets/follow.py      |   2 +-
 ForgeBlog/forgeblog/command/rssfeeds.py            |   4 +-
 ForgeBlog/forgeblog/main.py                        |  10 +-
 ForgeBlog/forgeblog/model/blog.py                  |  24 +--
 ForgeBlog/forgeblog/tests/functional/test_feeds.py |   3 +-
 ForgeBlog/forgeblog/tests/functional/test_rest.py  |  26 ++-
 ForgeBlog/forgeblog/tests/functional/test_root.py  |  17 +-
 ForgeBlog/forgeblog/tests/test_app.py              |   4 +-
 ForgeBlog/forgeblog/tests/unit/__init__.py         |   2 +-
 ForgeBlog/forgeblog/version.py                     |   1 -
 ForgeBlog/forgeblog/widgets.py                     |   7 +-
 ForgeChat/forgechat/command.py                     |   6 +-
 ForgeChat/forgechat/main.py                        |   8 +-
 ForgeChat/forgechat/model/chat.py                  |   8 +-
 ForgeChat/forgechat/version.py                     |   1 -
 .../forgediscussion/controllers/forum.py           |  14 +-
 .../forgediscussion/controllers/root.py            |   2 +-
 ForgeDiscussion/forgediscussion/forum_main.py      |   4 +-
 ForgeDiscussion/forgediscussion/import_support.py  |  10 +-
 ForgeDiscussion/forgediscussion/model/forum.py     |  20 +-
 .../forgediscussion/tests/functional/test_forum.py |  34 ++--
 .../tests/functional/test_forum_admin.py           |  14 +-
 .../tests/functional/test_import.py                |   2 +-
 .../forgediscussion/tests/functional/test_rest.py  |  12 +-
 ForgeDiscussion/forgediscussion/tests/test_app.py  |   2 -
 ForgeDiscussion/forgediscussion/version.py         |   1 -
 .../forgediscussion/widgets/forum_widgets.py       |   5 +-
 ForgeFeedback/forgefeedback/feedback_main.py       |   4 +-
 ForgeFeedback/forgefeedback/model/feedback.py      |   2 +-
 ForgeFeedback/forgefeedback/tests/unit/__init__.py |   2 +-
 .../tests/unit/test_root_controller.py             |   2 +-
 ForgeFeedback/forgefeedback/version.py             |   1 -
 ForgeFiles/forgefiles/files_main.py                |   5 +-
 .../forgefiles/tests/functional/test_root.py       |   1 -
 ForgeFiles/forgefiles/tests/model/__init__.py      |   2 +-
 ForgeGit/forgegit/git_main.py                      |   6 +-
 ForgeGit/forgegit/model/git_repo.py                |  16 +-
 ForgeGit/forgegit/tests/__init__.py                |   2 -
 ForgeGit/forgegit/tests/functional/test_auth.py    |   2 -
 .../forgegit/tests/functional/test_controllers.py  |  37 ++--
 ForgeGit/forgegit/tests/model/test_repository.py   |   4 +-
 ForgeGit/forgegit/tests/test_tasks.py              |   2 +-
 ForgeGit/forgegit/version.py                       |   1 -
 ForgeImporters/docs/conf.py                        |   2 -
 ForgeImporters/forgeimporters/base.py              |  25 ++-
 .../forgeimporters/forge/alluraImporter.py         |   2 +-
 ForgeImporters/forgeimporters/forge/discussion.py  |   2 +-
 ForgeImporters/forgeimporters/forge/tracker.py     |   2 +-
 ForgeImporters/forgeimporters/github/__init__.py   |  20 +-
 ForgeImporters/forgeimporters/github/code.py       |   2 +-
 .../forgeimporters/github/tests/test_code.py       |   4 +-
 .../forgeimporters/github/tests/test_oauth.py      |   2 +-
 .../forgeimporters/github/tests/test_utils.py      |   2 +-
 .../forgeimporters/github/tests/test_wiki.py       |   3 -
 ForgeImporters/forgeimporters/github/tracker.py    |   6 +-
 ForgeImporters/forgeimporters/github/utils.py      |  14 +-
 ForgeImporters/forgeimporters/github/wiki.py       |  15 +-
 .../forgeimporters/tests/forge/test_discussion.py  |   8 +-
 .../forgeimporters/tests/forge/test_tracker.py     |   6 +-
 .../tests/github/functional/test_github.py         |   6 +-
 .../forgeimporters/tests/github/test_extractor.py  |   1 -
 .../forgeimporters/tests/github/test_tracker.py    |   4 +-
 ForgeImporters/forgeimporters/tests/test_base.py   |   2 +-
 ForgeImporters/forgeimporters/trac/__init__.py     |   4 +-
 .../trac/tests/functional/test_trac.py             |   2 +-
 .../forgeimporters/trac/tests/test_tickets.py      |   3 +-
 ForgeImporters/forgeimporters/trac/tickets.py      |   2 +-
 ForgeLink/forgelink/link_main.py                   |   8 +-
 ForgeLink/forgelink/tests/functional/test_rest.py  |  14 +-
 ForgeLink/forgelink/tests/functional/test_root.py  |   1 -
 ForgeLink/forgelink/tests/test_app.py              |   2 +-
 ForgeLink/forgelink/version.py                     |   1 -
 ForgeSVN/forgesvn/controllers.py                   |   2 +-
 ForgeSVN/forgesvn/model/svn.py                     |  19 +-
 ForgeSVN/forgesvn/svn_main.py                      |   9 +-
 ForgeSVN/forgesvn/tests/__init__.py                |   2 -
 ForgeSVN/forgesvn/tests/functional/test_auth.py    |   2 -
 .../forgesvn/tests/functional/test_controllers.py  |   3 +-
 ForgeSVN/forgesvn/tests/model/test_repository.py   |  23 +--
 .../forgesvn/tests/model/test_svnimplementation.py |   2 +-
 ForgeSVN/forgesvn/tests/test_tasks.py              |   2 -
 ForgeSVN/forgesvn/version.py                       |   1 -
 ForgeShortUrl/forgeshorturl/main.py                |  10 +-
 ForgeShortUrl/forgeshorturl/model/shorturl.py      |   4 +-
 .../forgeshorturl/tests/functional/test.py         |   8 +-
 ForgeTracker/forgetracker/import_support.py        |  10 +-
 ForgeTracker/forgetracker/model/ticket.py          |  58 +++---
 ForgeTracker/forgetracker/search.py                |   3 +-
 .../forgetracker/tests/functional/test_rest.py     |   8 +-
 .../forgetracker/tests/functional/test_root.py     | 112 +++++------
 ForgeTracker/forgetracker/tests/test_app.py        |   4 +-
 ForgeTracker/forgetracker/tests/unit/__init__.py   |   2 +-
 .../forgetracker/tests/unit/test_globals_model.py  |   2 +-
 .../tests/unit/test_milestone_controller.py        |   4 +-
 .../tests/unit/test_root_controller.py             |   6 +-
 ForgeTracker/forgetracker/tracker_main.py          |  29 ++-
 ForgeTracker/forgetracker/version.py               |   1 -
 .../forgetracker/widgets/admin_custom_fields.py    |  14 +-
 ForgeTracker/forgetracker/widgets/ticket_form.py   |  17 +-
 ForgeTracker/forgetracker/widgets/ticket_search.py |  12 +-
 .../forgeuserstats/controllers/userstats.py        |   2 +-
 ForgeUserStats/forgeuserstats/model/stats.py       |   4 +-
 ForgeUserStats/forgeuserstats/tests/test_stats.py  |   4 +-
 ForgeUserStats/forgeuserstats/version.py           |   1 -
 ForgeWiki/forgewiki/converters.py                  |   4 +-
 ForgeWiki/forgewiki/model/wiki.py                  |  14 +-
 ForgeWiki/forgewiki/tests/functional/test_rest.py  |   7 +-
 ForgeWiki/forgewiki/tests/functional/test_root.py  |  56 +++---
 ForgeWiki/forgewiki/tests/test_app.py              |   9 +-
 ForgeWiki/forgewiki/tests/test_models.py           |   3 +-
 ForgeWiki/forgewiki/version.py                     |   1 -
 ForgeWiki/forgewiki/wiki_main.py                   |   8 +-
 fuse/accessfs.py                                   |  18 +-
 scripts/ApacheAccessHandler.py                     |  12 +-
 scripts/add_user_to_group.py                       |   2 +-
 scripts/changelog.py                               |   6 +-
 .../migrations/024-migrate-custom-profile-text.py  |   2 +-
 scripts/migrations/028-remove-svn-trees.py         |   2 +-
 .../migrations/031-set-user-pending-to-false.py    |   2 +-
 .../032-subscribe-merge-request-submitters.py      |   2 +-
 .../033-change-comment-anon-permissions.py         |   2 +-
 scripts/new_ticket.py                              |   3 +-
 scripts/perf/call_count.py                         |   6 +-
 scripts/perf/generate-projects.py                  |   5 +-
 scripts/perf/load-up-forum.py                      |   3 +-
 scripts/perf/parse_timings.py                      |   1 -
 scripts/perf/sstress.py                            |   1 -
 scripts/perf/test_git_lcd.py                       |   2 +-
 scripts/project-import.py                          |   3 +-
 scripts/scrub-allura-data.py                       |   2 +-
 scripts/teamforge-import.py                        |  16 +-
 scripts/trac_import.py                             |   5 +-
 scripts/wiki-copy.py                               |   4 +-
 318 files changed, 1569 insertions(+), 1909 deletions(-)

diff --git a/Allura/allura/__init__.py b/Allura/allura/__init__.py
index 15c3ab2..5d86261 100644
--- a/Allura/allura/__init__.py
+++ b/Allura/allura/__init__.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-
 #       Licensed to the Apache Software Foundation (ASF) under one
 #       or more contributor license agreements.  See the NOTICE file
 #       distributed with this work for additional information
diff --git a/Allura/allura/app.py b/Allura/allura/app.py
index 831d9fa..9f50c18 100644
--- a/Allura/allura/app.py
+++ b/Allura/allura/app.py
@@ -48,15 +48,14 @@ from allura.lib.utils import permanent_redirect, ConfigProxy
 from allura import model as M
 from allura.tasks import index_tasks
 import six
-from io import open, BytesIO
-from six.moves import map
+from io import BytesIO
 
 log = logging.getLogger(__name__)
 
 config = ConfigProxy(common_suffix='forgemail.domain')
 
 
-class ConfigOption(object):
+class ConfigOption:
 
     """Definition of a configuration option for an :class:`Application`.
 
@@ -91,7 +90,7 @@ class ConfigOption(object):
         return ew._Jinja2Widget().j2_attrs(self.extra_attrs or {})
 
 
-class SitemapEntry(object):
+class SitemapEntry:
 
     """A labeled URL, which may optionally have
     :class:`children <SitemapEntry>`.
@@ -219,7 +218,7 @@ class SitemapEntry(object):
         )
 
 
-class Application(object):
+class Application:
 
     """
     The base Allura pluggable application
@@ -625,7 +624,7 @@ class Application(object):
         :return: a list of :class:`WebhookSender <allura.webhooks.WebhookSender>`
         """
         tool_name = self.config.tool_name.lower()
-        webhooks = [w for w in six.itervalues(g.entry_points['webhooks'])
+        webhooks = [w for w in g.entry_points['webhooks'].values()
                     if tool_name in w.triggered_by]
         return webhooks
 
@@ -668,7 +667,7 @@ class Application(object):
     def admin_menu_collapse_button(self):
         """Returns button for showing/hiding admin sidebar menu"""
         return SitemapEntry(
-            label='Admin - {}'.format(self.config.options.mount_label),
+            label=f'Admin - {self.config.options.mount_label}',
             extra_html_attrs={
                 'id': 'sidebar-admin-menu-trigger',
             })
@@ -811,7 +810,7 @@ class Application(object):
         return None
 
 
-class AdminControllerMixin(object):
+class AdminControllerMixin:
     """Provides common functionality admin controllers need"""
     def _before(self, *remainder, **params):
         # Display app's sidebar on admin page, instead of :class:`AdminApp`'s
@@ -837,7 +836,7 @@ class DefaultAdminController(BaseController, AdminControllerMixin):
         """Instantiate this controller for an :class:`app <Application>`.
 
         """
-        super(DefaultAdminController, self).__init__()
+        super().__init__()
         self.app = app
         self.webhooks = WebhooksLookup(app)
 
@@ -976,7 +975,7 @@ class DefaultAdminController(BaseController, AdminControllerMixin):
                 try:
                     val = opt.validate(val)
                 except fev.Invalid as e:
-                    flash('{}: {}'.format(opt.name, str(e)), 'error')
+                    flash(f'{opt.name}: {str(e)}', 'error')
                     continue
                 self.app.config.options[opt.name] = val
             if is_admin:
@@ -1005,9 +1004,9 @@ class DefaultAdminController(BaseController, AdminControllerMixin):
             new_group_ids = args.get('new', [])
             del_group_ids = []
             group_ids = args.get('value', [])
-            if isinstance(new_group_ids, six.string_types):
+            if isinstance(new_group_ids, str):
                 new_group_ids = [new_group_ids]
-            if isinstance(group_ids, six.string_types):
+            if isinstance(group_ids, str):
                 group_ids = [group_ids]
 
             for acl in old_acl:
@@ -1047,7 +1046,7 @@ class DefaultAdminController(BaseController, AdminControllerMixin):
 class WebhooksLookup(BaseController, AdminControllerMixin):
 
     def __init__(self, app):
-        super(WebhooksLookup, self).__init__()
+        super().__init__()
         self.app = app
 
     @without_trailing_slash
diff --git a/Allura/allura/command/base.py b/Allura/allura/command/base.py
index 8e38b57..0d66564 100644
--- a/Allura/allura/command/base.py
+++ b/Allura/allura/command/base.py
@@ -51,7 +51,7 @@ def run_command(command, args):
     return command.run(arg_list)
 
 
-class EmptyClass(object):
+class EmptyClass:
     pass
 
 
@@ -61,7 +61,7 @@ class MetaParserDocstring(type):
         return cls.parser.format_help()
 
 
-class Command(six.with_metaclass(MetaParserDocstring, command.Command)):
+class Command(command.Command, metaclass=MetaParserDocstring):
     min_args = 1
     max_args = 1
     usage = '[<ini file>]'
@@ -69,7 +69,7 @@ class Command(six.with_metaclass(MetaParserDocstring, command.Command)):
 
     @classmethod
     def post(cls, *args, **kw):
-        cmd = '{}.{}'.format(cls.__module__, cls.__name__)
+        cmd = f'{cls.__module__}.{cls.__name__}'
         return run_command.post(cmd, *args, **kw)
 
     @ming.utils.LazyProperty
diff --git a/Allura/allura/command/create_neighborhood.py b/Allura/allura/command/create_neighborhood.py
index dc4b2cd..5bad4dc 100644
--- a/Allura/allura/command/create_neighborhood.py
+++ b/Allura/allura/command/create_neighborhood.py
@@ -48,7 +48,7 @@ class CreateNeighborhoodCommand(base.Command):
                           google_analytics=False))
         project_reg = plugin.ProjectRegistrationProvider.get()
         project_reg.register_neighborhood_project(n, admins)
-        log.info('Successfully created neighborhood "{}"'.format(shortname))
+        log.info(f'Successfully created neighborhood "{shortname}"')
 
 
 class UpdateNeighborhoodCommand(base.Command):
diff --git a/Allura/allura/command/create_trove_categories.py b/Allura/allura/command/create_trove_categories.py
index 43bbb4e..eebdf53 100644
--- a/Allura/allura/command/create_trove_categories.py
+++ b/Allura/allura/command/create_trove_categories.py
@@ -63,7 +63,7 @@ class CreateTroveCategoriesCommand(base.Command):
             sys.exit("Couldn't find TroveCategory with trove_cat_id=%s" %
                      trove_cat_id)
         for t in ts:
-            for k, v in six.iteritems(attr_dict):
+            for k, v in attr_dict.items():
                 setattr(t, k, v)
 
     # patching to avoid a *lot* of event hooks firing, and taking a long long time
diff --git a/Allura/allura/command/script.py b/Allura/allura/command/script.py
index 31e9674..9506075 100644
--- a/Allura/allura/command/script.py
+++ b/Allura/allura/command/script.py
@@ -28,7 +28,6 @@ from ming.orm import session
 from allura.lib import helpers as h
 from allura.lib import utils
 from . import base
-from io import open
 
 
 class ScriptCommand(base.Command):
diff --git a/Allura/allura/command/show_models.py b/Allura/allura/command/show_models.py
index 4e39448..5e21753 100644
--- a/Allura/allura/command/show_models.py
+++ b/Allura/allura/command/show_models.py
@@ -235,14 +235,14 @@ class EnsureIndexCommand(base.Command):
                 idx = project_indexes[cname]
             idx.extend(mgr.indexes)
         base.log.info('Updating indexes for main DB')
-        for odm_session, db_indexes in six.iteritems(main_indexes):
+        for odm_session, db_indexes in main_indexes.items():
             db = odm_session.impl.db
-            for name, indexes in six.iteritems(db_indexes):
+            for name, indexes in db_indexes.items():
                 self._update_indexes(db[name], indexes)
         base.log.info('Updating indexes for project DB')
         db = M.project_doc_session.db
         base.log.info('... DB: %s', db)
-        for name, indexes in six.iteritems(project_indexes):
+        for name, indexes in project_indexes.items():
             self._update_indexes(db[name], indexes)
         base.log.info('Done updating indexes')
 
@@ -261,7 +261,7 @@ class EnsureIndexCommand(base.Command):
         unique_flag_drop = {}
         unique_flag_add = {}
         try:
-            existing_indexes = six.iteritems(collection.index_information())
+            existing_indexes = collection.index_information().items()
         except OperationFailure:
             # exception is raised if db or collection doesn't exist yet
             existing_indexes = {}
@@ -280,13 +280,13 @@ class EnsureIndexCommand(base.Command):
                 else:
                     prev_indexes[iname] = keys
 
-        for iname, keys in six.iteritems(unique_flag_drop):
+        for iname, keys in unique_flag_drop.items():
             self._recreate_index(collection, iname, list(keys), unique=False)
-        for iname, keys in six.iteritems(unique_flag_add):
+        for iname, keys in unique_flag_add.items():
             self._recreate_index(collection, iname, list(keys), unique=True)
 
         # Ensure all indexes
-        for keys, idx in six.iteritems(uindexes):
+        for keys, idx in uindexes.items():
             base.log.info('...... ensure %s:%s', collection.name, idx)
             while True:
                 try:
@@ -301,11 +301,11 @@ class EnsureIndexCommand(base.Command):
                 except DuplicateKeyError as err:
                     base.log.info('Found dupe key(%s), eliminating dupes', err)
                     self._remove_dupes(collection, idx.index_spec)
-        for keys, idx in six.iteritems(indexes):
+        for keys, idx in indexes.items():
             base.log.info('...... ensure %s:%s', collection.name, idx)
             collection.ensure_index(idx.index_spec, background=True, **idx.index_options)
         # Drop obsolete indexes
-        for iname, keys in six.iteritems(prev_indexes):
+        for iname, keys in prev_indexes.items():
             if keys not in indexes:
                 if self.options.clean:
                     base.log.info('...... drop index %s:%s', collection.name, iname)
@@ -313,7 +313,7 @@ class EnsureIndexCommand(base.Command):
                 else:
                     base.log.info('...... potentially unneeded index, could be removed by running with --clean %s:%s',
                                   collection.name, iname)
-        for iname, keys in six.iteritems(prev_uindexes):
+        for iname, keys in prev_uindexes.items():
             if keys not in uindexes:
                 if self.options.clean:
                     base.log.info('...... drop index %s:%s', collection.name, iname)
@@ -357,7 +357,7 @@ class EnsureIndexCommand(base.Command):
 
 def build_model_inheritance_graph():
     graph = {m.mapped_class: ([], []) for m in Mapper.all_mappers()}
-    for cls, (parents, children) in six.iteritems(graph):
+    for cls, (parents, children) in graph.items():
         for b in cls.__bases__:
             if b not in graph:
                 continue
@@ -368,7 +368,7 @@ def build_model_inheritance_graph():
 
 def dump_cls(depth, cls):
     indent = ' ' * 4 * depth
-    yield indent + '{}.{}'.format(cls.__module__, cls.__name__)
+    yield indent + f'{cls.__module__}.{cls.__name__}'
     m = mapper(cls)
     for p in m.properties:
         s = indent * 2 + ' - ' + str(p)
@@ -380,5 +380,4 @@ def dump_cls(depth, cls):
 def dfs(root, graph, depth=0):
     yield depth, root
     for node in graph[root][1]:
-        for r in dfs(node, graph, depth + 1):
-            yield r
+        yield from dfs(node, graph, depth + 1)
diff --git a/Allura/allura/command/taskd.py b/Allura/allura/command/taskd.py
index ae684e6..6d5bfe8 100644
--- a/Allura/allura/command/taskd.py
+++ b/Allura/allura/command/taskd.py
@@ -101,7 +101,7 @@ class TaskdCommand(base.Command):
 
     def worker(self):
         from allura import model as M
-        name = '{} pid {}'.format(os.uname()[1], os.getpid())
+        name = f'{os.uname()[1]} pid {os.getpid()}'
         wsgi_app = loadapp('config:%s#task' %
                            self.args[0], relative_to=os.getcwd())
         poll_interval = asint(tg.config.get('monq.poll_interval', 10))
@@ -175,7 +175,7 @@ class TaskCommand(base.Command):
     parser.add_option('-s', '--state', dest='state', default=None,
                       help='state of processes for "list", "count", or "purge" subcommands.  * means all. '
                            '(Defaults per command: %s)' %
-                           ", ".join(['{}="{}"'.format(k, v) for k, v in cmd_default_states.items()]))
+                           ", ".join([f'{k}="{v}"' for k, v in cmd_default_states.items()]))
     parser.add_option('-t', '--timeout', dest='timeout', type=int, default=60,
                       help='timeout (in seconds) for busy tasks (only applies to "timeout" command)')
     parser.add_option('--filter-name-prefix', dest='filter_name_prefix', default=None,
@@ -226,7 +226,7 @@ class TaskCommand(base.Command):
 
     def _add_filters(self, q):
         if self.options.filter_name_prefix:
-            q['task_name'] = {'$regex': r'^{}.*'.format(re.escape(self.options.filter_name_prefix))}
+            q['task_name'] = {'$regex': fr'^{re.escape(self.options.filter_name_prefix)}.*'}
         if self.options.filter_result_regex:
             q['result'] = {'$regex': self.options.filter_result_regex}
         if self.options.days_ago:
@@ -236,7 +236,7 @@ class TaskCommand(base.Command):
         return q
 
     def _print_query(self, cmd, *args):
-        print('running mongod cmd: {}, {}'.format(cmd, args))
+        print(f'running mongod cmd: {cmd}, {args}')
 
     def _list(self):
         '''List tasks'''
diff --git a/Allura/allura/command/taskd_cleanup.py b/Allura/allura/command/taskd_cleanup.py
index 7bf95b5..e9cff5e 100644
--- a/Allura/allura/command/taskd_cleanup.py
+++ b/Allura/allura/command/taskd_cleanup.py
@@ -25,7 +25,6 @@ import six
 
 from allura import model as M
 from . import base
-from six.moves import range
 
 
 class TaskdCleanupCommand(base.Command):
@@ -123,7 +122,7 @@ class TaskdCleanupCommand(base.Command):
     def _busy_tasks(self, pid=None):
         regex = '^%s ' % self.hostname
         if pid is not None:
-            regex = '^{} pid {}'.format(self.hostname, pid)
+            regex = f'^{self.hostname} pid {pid}'
         return M.MonQTask.query.find({
             'state': 'busy',
             'process': {'$regex': regex}
diff --git a/Allura/allura/config/__init__.py b/Allura/allura/config/__init__.py
index cbf1ae0..144e298 100644
--- a/Allura/allura/config/__init__.py
+++ b/Allura/allura/config/__init__.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-
 #       Licensed to the Apache Software Foundation (ASF) under one
 #       or more contributor license agreements.  See the NOTICE file
 #       distributed with this work for additional information
diff --git a/Allura/allura/config/app_cfg.py b/Allura/allura/config/app_cfg.py
index 8194d70..6bde340 100644
--- a/Allura/allura/config/app_cfg.py
+++ b/Allura/allura/config/app_cfg.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-
 #       Licensed to the Apache Software Foundation (ASF) under one
 #       or more contributor license agreements.  See the NOTICE file
 #       distributed with this work for additional information
@@ -48,7 +46,6 @@ import allura
 # needed for tg.configuration to work
 from allura.lib import app_globals, helpers
 from allura.lib.package_path_loader import PackagePathLoader
-from six.moves import filter
 
 log = logging.getLogger(__name__)
 
@@ -84,13 +81,12 @@ class AlluraJinjaRenderer(JinjaRenderer):
                 import pylibmc
                 from jinja2 import MemcachedBytecodeCache
                 client = pylibmc.Client([config['memcached_host']])
-                bcc_prefix = 'jinja2/{}/'.format(jinja2.__version__)
-                if six.PY3:
-                    bcc_prefix += 'py{}{}/'.format(sys.version_info.major, sys.version_info.minor)
+                bcc_prefix = f'jinja2/{jinja2.__version__}/'
+                bcc_prefix += f'py{sys.version_info.major}{sys.version_info.minor}/'
                 bcc = MemcachedBytecodeCache(client, prefix=bcc_prefix)
             elif cache_type == 'filesystem':
                 from jinja2 import FileSystemBytecodeCache
-                bcc = FileSystemBytecodeCache(pattern='__jinja2_{}_%s.cache'.format(jinja2.__version__))
+                bcc = FileSystemBytecodeCache(pattern=f'__jinja2_{jinja2.__version__}_%s.cache')
         except Exception:
             log.exception("Error encountered while setting up a" +
                           " %s-backed bytecode cache for Jinja" % cache_type)
diff --git a/Allura/allura/config/environment.py b/Allura/allura/config/environment.py
index bf0f5b6..4fdc823 100644
--- a/Allura/allura/config/environment.py
+++ b/Allura/allura/config/environment.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-
 #       Licensed to the Apache Software Foundation (ASF) under one
 #       or more contributor license agreements.  See the NOTICE file
 #       distributed with this work for additional information
diff --git a/Allura/allura/config/middleware.py b/Allura/allura/config/middleware.py
index 5a34445..e5ace85 100644
--- a/Allura/allura/config/middleware.py
+++ b/Allura/allura/config/middleware.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-
 #       Licensed to the Apache Software Foundation (ASF) under one
 #       or more contributor license agreements.  See the NOTICE file
 #       distributed with this work for additional information
@@ -76,7 +74,7 @@ def make_app(global_conf, full_stack=True, **app_conf):
     if override_root_module_name:
         # get an actual instance of it, like BasetestProjectRootController or TaskController
         className = override_root_module_name.title().replace('_', '') + 'Controller'
-        module = importlib.import_module('allura.controllers.{}'.format(override_root_module_name))
+        module = importlib.import_module(f'allura.controllers.{override_root_module_name}')
         rootClass = getattr(module, className)
         root = rootClass()
     else:
diff --git a/Allura/allura/controllers/__init__.py b/Allura/allura/controllers/__init__.py
index 417b8ad..a72724a 100644
--- a/Allura/allura/controllers/__init__.py
+++ b/Allura/allura/controllers/__init__.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-
 #       Licensed to the Apache Software Foundation (ASF) under one
 #       or more contributor license agreements.  See the NOTICE file
 #       distributed with this work for additional information
diff --git a/Allura/allura/controllers/attachments.py b/Allura/allura/controllers/attachments.py
index d1e5119..9d1ec5e 100644
--- a/Allura/allura/controllers/attachments.py
+++ b/Allura/allura/controllers/attachments.py
@@ -50,7 +50,7 @@ class AttachmentsController(BaseController):
     def _lookup(self, filename=None, *args):
         if filename:
             if not args:
-                filename = request.path.rsplit(str('/'), 1)[-1]
+                filename = request.path.rsplit('/', 1)[-1]
             filename = unquote(filename)
             return self.AttachmentControllerClass(filename, self.artifact), args
         else:
diff --git a/Allura/allura/controllers/auth.py b/Allura/allura/controllers/auth.py
index 72bbed8..ac0c7b4 100644
--- a/Allura/allura/controllers/auth.py
+++ b/Allura/allura/controllers/auth.py
@@ -59,12 +59,11 @@ from allura.lib import utils
 from allura.controllers import BaseController
 from allura.tasks.mail_tasks import send_system_mail_to_user
 import six
-from six.moves import zip
 
 log = logging.getLogger(__name__)
 
 
-class F(object):
+class F:
     login_form = LoginForm()
     password_change_form = forms.PasswordChangeForm(action='/auth/preferences/change_password')
     upload_key_form = forms.UploadKeyForm(action='/auth/preferences/upload_sshkey')
@@ -119,7 +118,7 @@ class AuthController(BaseController):
             raise AttributeError("TG decoratedcontroller calls this during import time, can't do anything complex")
         urls = plugin.UserPreferencesProvider.get().additional_urls()
         if name not in urls:
-            raise AttributeError("'{}' object has no attribute '{}'".format(type(self).__name__, name))
+            raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'")
         return urls[name]
 
     @expose()
@@ -166,13 +165,13 @@ class AuthController(BaseController):
         user_record = M.User.query.find(
             {'tool_data.AuthPasswordReset.hash': hash}).first()
         if not user_record:
-            log.info('Reset hash not found: {}'.format(hash))
+            log.info(f'Reset hash not found: {hash}')
             flash('Unable to process reset, please try again')
             redirect(login_url)
         hash_expiry = user_record.get_tool_data(
             'AuthPasswordReset', 'hash_expiry')
         if not hash_expiry or hash_expiry < datetime.utcnow():
-            log.info('Reset hash expired: {} {}'.format(hash, hash_expiry))
+            log.info(f'Reset hash expired: {hash} {hash_expiry}')
             flash('Unable to process reset, please try again')
             redirect(login_url)
         return user_record
@@ -198,7 +197,7 @@ class AuthController(BaseController):
         if not provider.forgotten_password_process:
             raise wexc.HTTPNotFound()
         user = self._validate_hash(hash)
-        enforce_hibp_password_check(provider, pw, '/auth/forgotten_password/{}'.format(hash))
+        enforce_hibp_password_check(provider, pw, f'/auth/forgotten_password/{hash}')
 
         user.set_password(pw)
         user.set_tool_data('AuthPasswordReset', hash='', hash_expiry='')  # Clear password reset token
@@ -423,7 +422,7 @@ class AuthController(BaseController):
             return_to = self._verify_return_to(kwargs.get('return_to'))
             redirect(return_to)
 
-    @expose(content_type=str('text/plain'))
+    @expose(content_type='text/plain')
     def refresh_repo(self, *repo_path):
         # post-commit hooks use this
         if not repo_path:
@@ -548,7 +547,7 @@ class AuthController(BaseController):
         expired_reason = session.pop('expired-reason', None)
 
         session.save()
-        h.auditlog_user('Password reset ({})'.format(expired_reason))
+        h.auditlog_user(f'Password reset ({expired_reason})')
         if return_to and return_to != request.url:
             redirect(return_to)
         else:
@@ -694,7 +693,7 @@ class PreferencesController(BaseController):
             c.user.set_pref('display_name', preferences['display_name'])
             if old != preferences['display_name']:
                 h.auditlog_user('Display Name changed %s => %s', old, preferences['display_name'])
-            for k, v in six.iteritems(preferences):
+            for k, v in preferences.items():
                 if k == 'results_per_page':
                     v = int(v)
                 c.user.set_pref(k, v)
@@ -942,7 +941,7 @@ class UserSkillsController(BaseController):
 
     def __init__(self, category=None):
         self.category = category
-        super(UserSkillsController, self).__init__()
+        super().__init__()
 
     def _check_security(self):
         require_authenticated()
diff --git a/Allura/allura/controllers/base.py b/Allura/allura/controllers/base.py
index 883783e..2e859c0 100644
--- a/Allura/allura/controllers/base.py
+++ b/Allura/allura/controllers/base.py
@@ -27,7 +27,7 @@ from tg import tmpl_context as c
 log = logging.getLogger(__name__)
 
 
-class BaseController(object):
+class BaseController:
 
     @expose()
     def _lookup(self, name=None, *remainder):
@@ -37,13 +37,13 @@ class BaseController(object):
 
     def rate_limit(self, artifact_class, message, redir='..'):
         if artifact_class.is_limit_exceeded(c.app.config, user=c.user):
-            msg = '{} rate limit exceeded. '.format(message)
+            msg = f'{message} rate limit exceeded. '
             log.warn(msg + c.app.config.url())
             flash(msg + 'Please try again later.', 'error')
             redirect(redir or '/')
 
 
-class DispatchIndex(object):
+class DispatchIndex:
 
     """Rewrite default url dispatching for controller.
 
diff --git a/Allura/allura/controllers/basetest_project_root.py b/Allura/allura/controllers/basetest_project_root.py
index b2db6d9..dc63048 100644
--- a/Allura/allura/controllers/basetest_project_root.py
+++ b/Allura/allura/controllers/basetest_project_root.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-
 #       Licensed to the Apache Software Foundation (ASF) under one
 #       or more contributor license agreements.  See the NOTICE file
 #       distributed with this work for additional information
@@ -71,7 +69,7 @@ class BasetestProjectRootController(WsgiDispatchController, ProjectController):
 
         # neighborhoods & projects handled in _lookup
 
-        super(BasetestProjectRootController, self).__init__()
+        super().__init__()
 
     def _setup_request(self):
         pass
@@ -129,10 +127,10 @@ class BasetestProjectRootController(WsgiDispatchController, ProjectController):
             environ['beaker.session'].save()
             environ['beaker.session'].persist()
             c.user = auth.authenticate_request()
-        return super(BasetestProjectRootController, self)._perform_call(context)
+        return super()._perform_call(context)
 
 
-class DispatchTest(object):
+class DispatchTest:
     @expose()
     def _lookup(self, *args):
         if args:
@@ -141,7 +139,7 @@ class DispatchTest(object):
             raise exc.HTTPNotFound()
 
 
-class NamedController(object):
+class NamedController:
     def __init__(self, name):
         self.name = name
 
@@ -151,10 +149,10 @@ class NamedController(object):
 
     @expose()
     def _default(self, *args):
-        return 'default({})({!r})'.format(self.name, args)
+        return f'default({self.name})({args!r})'
 
 
-class SecurityTests(object):
+class SecurityTests:
     @expose()
     def _lookup(self, name, *args):
         name = unquote(name)
@@ -163,7 +161,7 @@ class SecurityTests(object):
         return SecurityTest(), args
 
 
-class SecurityTest(object):
+class SecurityTest:
     def __init__(self):
         from forgewiki import model as WM
         c.app = c.project.app_instance('wiki')
diff --git a/Allura/allura/controllers/discuss.py b/Allura/allura/controllers/discuss.py
index 397e5ff..5fd8956 100644
--- a/Allura/allura/controllers/discuss.py
+++ b/Allura/allura/controllers/discuss.py
@@ -51,7 +51,7 @@ import six
 log = logging.getLogger(__name__)
 
 
-class pass_validator(object):
+class pass_validator:
     def validate(self, v, s):
         return v
 
@@ -59,14 +59,14 @@ class pass_validator(object):
 pass_validator = pass_validator()
 
 
-class ModelConfig(object):
+class ModelConfig:
     Discussion = M.Discussion
     Thread = M.Thread
     Post = M.Post
     Attachment = M.DiscussionAttachment
 
 
-class WidgetConfig(object):
+class WidgetConfig:
     # Forms
     subscription_form = DW.SubscriptionForm()
     edit_post = DW.EditPost()
@@ -148,7 +148,7 @@ class AppDiscussionController(DiscussionController):
             app_config_id=c.app.config._id)
 
 
-class ThreadsController(six.with_metaclass(h.ProxiedAttrMeta, BaseController)):
+class ThreadsController(BaseController, metaclass=h.ProxiedAttrMeta):
     M = h.attrproxy('_discussion_controller', 'M')
     W = h.attrproxy('_discussion_controller', 'W')
     ThreadController = h.attrproxy(
@@ -169,7 +169,7 @@ class ThreadsController(six.with_metaclass(h.ProxiedAttrMeta, BaseController)):
             raise exc.HTTPNotFound()
 
 
-class ThreadController(six.with_metaclass(h.ProxiedAttrMeta, BaseController, FeedController)):
+class ThreadController(BaseController, FeedController, metaclass=h.ProxiedAttrMeta):
     M = h.attrproxy('_discussion_controller', 'M')
     W = h.attrproxy('_discussion_controller', 'W')
     ThreadController = h.attrproxy(
@@ -277,7 +277,7 @@ def handle_post_or_reply(thread, edit_widget, rate_limit, kw, parent_post_id=Non
     redirect(six.ensure_text(request.referer or '/'))
 
 
-class PostController(six.with_metaclass(h.ProxiedAttrMeta, BaseController)):
+class PostController(BaseController, metaclass=h.ProxiedAttrMeta):
     M = h.attrproxy('_discussion_controller', 'M')
     W = h.attrproxy('_discussion_controller', 'W')
     ThreadController = h.attrproxy(
@@ -316,7 +316,7 @@ class PostController(six.with_metaclass(h.ProxiedAttrMeta, BaseController)):
             post_fields = self.W.edit_post.to_python(kw, None)  # could raise Invalid, but doesn't seem like it does
             file_info = post_fields.pop('file_info', None)
             self.post.add_multiple_attachments(file_info)
-            for k, v in six.iteritems(post_fields):
+            for k, v in post_fields.items():
                 try:
                     setattr(self.post, k, v)
                 except AttributeError:
@@ -468,7 +468,7 @@ class DiscussionAttachmentsController(AttachmentsController):
     AttachmentControllerClass = DiscussionAttachmentController
 
 
-class ModerationController(six.with_metaclass(h.ProxiedAttrMeta, BaseController)):
+class ModerationController(BaseController, metaclass=h.ProxiedAttrMeta):
     PostModel = M.Post
     M = h.attrproxy('_discussion_controller', 'M')
     W = h.attrproxy('_discussion_controller', 'W')
diff --git a/Allura/allura/controllers/error.py b/Allura/allura/controllers/error.py
index 5cb26d0..ae1f8a5 100644
--- a/Allura/allura/controllers/error.py
+++ b/Allura/allura/controllers/error.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-
 #       Licensed to the Apache Software Foundation (ASF) under one
 #       or more contributor license agreements.  See the NOTICE file
 #       distributed with this work for additional information
@@ -24,7 +22,7 @@ from tg import request, expose
 __all__ = ['ErrorController']
 
 
-class ErrorController(object):
+class ErrorController:
 
     @expose('jinja:allura:templates/error.html')
     def document(self, *args, **kwargs):
diff --git a/Allura/allura/controllers/feed.py b/Allura/allura/controllers/feed.py
index 72cfad2..41aae93 100644
--- a/Allura/allura/controllers/feed.py
+++ b/Allura/allura/controllers/feed.py
@@ -25,7 +25,7 @@ from allura import model as M
 from allura.lib import helpers as h
 
 
-class FeedArgs(object):
+class FeedArgs:
 
     """A facade for the arguments required by
     :meth:`allura.model.artifact.Feed.feed`.
@@ -50,7 +50,7 @@ class FeedArgs(object):
         self.description = description or title
 
 
-class FeedController(object):
+class FeedController:
 
     """Mixin class which adds RSS and Atom feed endpoints to an existing
     controller.
@@ -65,8 +65,8 @@ class FeedController(object):
     a customized feed should override :meth:`get_feed`.
 
     """
-    FEED_TYPES = [str('.atom'), str('.rss')]
-    FEED_NAMES = [str('feed{}'.format(typ)) for typ in FEED_TYPES]
+    FEED_TYPES = ['.atom', '.rss']
+    FEED_NAMES = [str(f'feed{typ}') for typ in FEED_TYPES]
 
     def __getattr__(self, name):
         if name in self.FEED_NAMES:
@@ -99,8 +99,8 @@ class FeedController(object):
             feed_def.url,
             feed_def.description,
             since, until, page, limit)
-        response.headers['Content-Type'] = str('')
-        response.content_type = str('application/xml')
+        response.headers['Content-Type'] = ''
+        response.content_type = 'application/xml'
         return feed.writeString('utf-8')
 
     def get_feed(self, project, app, user):
diff --git a/Allura/allura/controllers/newforge.py b/Allura/allura/controllers/newforge.py
index 1ee2ce6..70aea56 100644
--- a/Allura/allura/controllers/newforge.py
+++ b/Allura/allura/controllers/newforge.py
@@ -27,7 +27,7 @@ from allura.lib import helpers as h
 from allura.lib import utils
 
 
-class NewForgeController(object):
+class NewForgeController:
 
     # /nf/_static_/... (or whatever static.script_name is set to) is handled by StaticFilesMiddleware
     # /nf/admin/... is handled by SiteAdminController
diff --git a/Allura/allura/controllers/project.py b/Allura/allura/controllers/project.py
index a55b296..05dbb2b 100644
--- a/Allura/allura/controllers/project.py
+++ b/Allura/allura/controllers/project.py
@@ -62,7 +62,7 @@ class W:
     award_grant_form = ff.AwardGrantForm
 
 
-class NeighborhoodController(object):
+class NeighborhoodController:
 
     '''Manages a neighborhood of projects.
     '''
@@ -267,7 +267,7 @@ class NeighborhoodController(object):
         return {
             'options': [{
                 'value': u.username,
-                'label': '{} ({})'.format(u.display_name, u.username)
+                'label': f'{u.display_name} ({u.username})'
             } for u in p.users()]
         }
 
@@ -276,7 +276,7 @@ class NeighborhoodProjectBrowseController(ProjectBrowseController):
 
     def __init__(self, neighborhood=None, category_name=None, parent_category=None):
         self.neighborhood = neighborhood
-        super(NeighborhoodProjectBrowseController, self).__init__(
+        super().__init__(
             category_name=category_name, parent_category=parent_category)
         self.nav_stub = '%sbrowse/' % self.neighborhood.url()
         self.additional_filters = {'neighborhood_id': self.neighborhood._id}
@@ -305,7 +305,7 @@ class NeighborhoodProjectBrowseController(ProjectBrowseController):
                     limit=limit, page=page, count=count)
 
 
-class ToolListController(object):
+class ToolListController:
 
     """Renders a list of all tools of a given type in the current project."""
 
@@ -471,23 +471,23 @@ class ProjectController(FeedController):
         return {
             'options': [{
                 'value': u.username,
-                'label': '{} ({})'.format(u.display_name, u.username)
+                'label': f'{u.display_name} ({u.username})'
             } for u in users]
         }
 
 
-class ScreenshotsController(object):
+class ScreenshotsController:
 
     @expose()
     def _lookup(self, filename, *args):
         if args:
             filename = unquote(filename)
         else:
-            filename = unquote(request.path.rsplit(str('/'), 1)[-1])
+            filename = unquote(request.path.rsplit('/', 1)[-1])
         return ScreenshotController(filename), args
 
 
-class ScreenshotController(object):
+class ScreenshotController:
 
     def __init__(self, filename):
         self.filename = filename
@@ -533,7 +533,7 @@ def set_nav(neighborhood):
             SitemapEntry('Awards', admin_url + 'accolades')]
 
 
-class NeighborhoodAdminController(object):
+class NeighborhoodAdminController:
 
     def __init__(self, neighborhood):
         self.neighborhood = neighborhood
@@ -696,7 +696,7 @@ class NeighborhoodAdminController(object):
         )
 
 
-class NeighborhoodStatsController(object):
+class NeighborhoodStatsController:
 
     def __init__(self, neighborhood):
         self.neighborhood = neighborhood
@@ -772,7 +772,7 @@ class NeighborhoodStatsController(object):
                     )
 
 
-class NeighborhoodModerateController(object):
+class NeighborhoodModerateController:
 
     def __init__(self, neighborhood):
         self.neighborhood = neighborhood
@@ -833,7 +833,7 @@ class NeighborhoodModerateController(object):
         redirect('.')
 
 
-class NeighborhoodAwardsController(object):
+class NeighborhoodAwardsController:
 
     def __init__(self, neighborhood=None):
         if neighborhood is not None:
@@ -901,7 +901,7 @@ class NeighborhoodAwardsController(object):
         redirect(six.ensure_text(request.referer or '/'))
 
 
-class AwardController(object):
+class AwardController:
 
     def __init__(self, neighborhood=None, award_id=None):
         self.neighborhood = neighborhood
@@ -969,7 +969,7 @@ class AwardController(object):
         redirect(six.ensure_text(request.referer or '/'))
 
 
-class GrantController(object):
+class GrantController:
 
     def __init__(self, neighborhood=None, award=None, recipient=None):
         self.neighborhood = neighborhood
@@ -1011,10 +1011,10 @@ class GrantController(object):
         redirect(six.ensure_text(request.referer or '/'))
 
 
-class ProjectImporterController(object):
+class ProjectImporterController:
 
     def __init__(self, neighborhood, *a, **kw):
-        super(ProjectImporterController, self).__init__(*a, **kw)
+        super().__init__(*a, **kw)
         self.neighborhood = neighborhood
 
     @expose()
diff --git a/Allura/allura/controllers/repository.py b/Allura/allura/controllers/repository.py
index 134dd23..933b6f3 100644
--- a/Allura/allura/controllers/repository.py
+++ b/Allura/allura/controllers/repository.py
@@ -76,7 +76,7 @@ class RepoRootController(BaseController, FeedController):
     def get_feed(self, project, app, user):
         query = dict(project_id=project._id, app_config_id=app.config._id)
         pname, repo = (project.shortname, app.config.options.mount_label)
-        title = '{} {} changes'.format(pname, repo)
+        title = f'{pname} {repo} changes'
         description = 'Recent changes to {} repository in {} project'.format(
             repo, pname)
         return FeedArgs(query, title, app.url, description=description)
@@ -266,7 +266,7 @@ class RepoRootController(BaseController, FeedController):
         parents = {}
         children = defaultdict(list)
         dates = {}
-        for row, (oid, ci) in enumerate(six.iteritems(commits_by_id)):
+        for row, (oid, ci) in enumerate(commits_by_id.items()):
             parents[oid] = list(ci.parent_ids)
             dates[oid] = ci.committed.date
             for p_oid in ci.parent_ids:
@@ -346,7 +346,7 @@ class RepoRestController(RepoRootController, AppRestControllerMixin):
             ]}
 
 
-class MergeRequestsController(object):
+class MergeRequestsController:
 
     @with_trailing_slash
     @expose('jinja:allura:templates/repo/merge_requests.html')
@@ -367,7 +367,7 @@ class MergeRequestsController(object):
         return MergeRequestController(num), remainder
 
 
-class MergeRequestController(object):
+class MergeRequestController:
     log_widget = SCMLogWidget(show_paging=False)
     thread_widget = w.Thread(
         page=None, limit=None, page_size=None, count=None,
@@ -589,7 +589,7 @@ class MergeRequestController(object):
         }
 
 
-class RefsController(object):
+class RefsController:
 
     def __init__(self, BranchBrowserClass):
         self.BranchBrowserClass = BranchBrowserClass
@@ -606,7 +606,7 @@ class RefsController(object):
         return self.BranchBrowserClass(ref), remainder
 
 
-class CommitsController(object):
+class CommitsController:
 
     @expose()
     def _lookup(self, ci=None, *remainder):
@@ -796,7 +796,7 @@ class TreeBrowser(BaseController, DispatchIndex):
         next = h.really_unicode(unquote(next))
         if not rest:
             # Might be a file rather than a dir
-            filename = h.really_unicode(request.path_info.rsplit(str('/'))[-1])
+            filename = h.really_unicode(request.path_info.rsplit('/')[-1])
             if filename:
                 try:
                     obj = self._tree[filename]
@@ -808,7 +808,7 @@ class TreeBrowser(BaseController, DispatchIndex):
                         self._tree,
                         filename), rest
         elif rest == ('index', ):
-            rest = (request.path_info.rsplit(str('/'))[-1],)
+            rest = (request.path_info.rsplit('/')[-1],)
         try:
             tree = self._tree[next]
         except KeyError:
@@ -846,7 +846,7 @@ class FileBrowser(BaseController):
         if kw.pop('format', 'html') == 'raw':
             if self._blob.size > asint(tg.config.get('scm.download.max_file_bytes', 30*1000*1000)):
                 large_size = self._blob.size
-                flash('File is {}.  Too large to download.'.format(h.do_filesizeformat(large_size)),
+                flash(f'File is {h.do_filesizeformat(large_size)}.  Too large to download.',
                       'warning', sticky=True)
                 raise exc.HTTPForbidden
             else:
@@ -877,15 +877,15 @@ class FileBrowser(BaseController):
     def raw(self, **kw):
         content_type = self._blob.content_type
         filename = self._blob.name
-        response.headers['Content-Type'] = str('')
+        response.headers['Content-Type'] = ''
         response.content_type = str(content_type)
         if self._blob.content_encoding is not None:
             content_encoding = self._blob.content_encoding
-            response.headers['Content-Encoding'] = str('')
+            response.headers['Content-Encoding'] = ''
             response.content_encoding = str(content_encoding)
         response.headers.add(
-            str('Content-Disposition'),
-            str('attachment;filename="%s"') % h.urlquote(filename))
+            'Content-Disposition',
+            'attachment;filename="%s"' % h.urlquote(filename))
         return iter(self._blob)
 
     def diff(self, prev_commit, fmt=None, prev_file=None, **kw):
@@ -933,7 +933,7 @@ class FileBrowser(BaseController):
         else:
             # py2 unified_diff can handle some unicode but not consistently, so best to do str() and ensure_str()
             # (can drop it on py3)
-            diff = str('').join(difflib.unified_diff(la, lb, six.ensure_str(adesc), six.ensure_str(bdesc)))
+            diff = ''.join(difflib.unified_diff(la, lb, six.ensure_str(adesc), six.ensure_str(bdesc)))
         return dict(a=a, b=b, diff=diff)
 
 
diff --git a/Allura/allura/controllers/rest.py b/Allura/allura/controllers/rest.py
index ee7689f..310ac5c 100644
--- a/Allura/allura/controllers/rest.py
+++ b/Allura/allura/controllers/rest.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-
 #       Licensed to the Apache Software Foundation (ASF) under one
 #       or more contributor license agreements.  See the NOTICE file
 #       distributed with this work for additional information
@@ -46,7 +44,7 @@ from datetime import datetime
 log = logging.getLogger(__name__)
 
 
-class RestController(object):
+class RestController:
 
     def __init__(self):
         self.oauth = OAuthNegotiator()
@@ -86,7 +84,7 @@ class RestController(object):
         """
         summary = dict()
         stats = dict()
-        for stat, provider in six.iteritems(g.entry_points['site_stats']):
+        for stat, provider in g.entry_points['site_stats'].items():
             stats[stat] = provider()
         if stats:
             summary['site_stats'] = stats
@@ -117,7 +115,7 @@ class RestController(object):
         return NeighborhoodRestController(neighborhood), remainder
 
 
-class OAuthNegotiator(object):
+class OAuthNegotiator:
 
     @property
     def server(self):
@@ -292,7 +290,7 @@ def rest_has_access(obj, user, perm):
     return resp
 
 
-class AppRestControllerMixin(object):
+class AppRestControllerMixin:
     @expose('json:')
     def has_access(self, user, perm, **kw):
         return rest_has_access(c.app, user, perm)
@@ -345,7 +343,7 @@ def nbhd_lookup_first_path(nbhd, name, current_user, remainder, api=False):
             raise exc.HTTPNotFound
         if user.disabled and not is_site_admin:
             raise exc.HTTPNotFound
-        if not api and user.url() != '/{}{}/'.format(prefix, pname):
+        if not api and user.url() != f'/{prefix}{pname}/':
             # might be different URL than the URL requested
             # e.g. if username isn't valid project name and user_project_shortname() converts the name
             new_url = user.url()
@@ -366,7 +364,7 @@ def nbhd_lookup_first_path(nbhd, name, current_user, remainder, api=False):
     return project, remainder
 
 
-class NeighborhoodRestController(object):
+class NeighborhoodRestController:
 
     def __init__(self, neighborhood):
         # type: (M.Neighborhood) -> None
@@ -407,7 +405,7 @@ class NeighborhoodRestController(object):
         except (colander.Invalid, ForgeError) as e:
             response.status_int = 400
             return {
-                'error': six.text_type(e) or repr(e),
+                'error': str(e) or repr(e),
             }
 
         project = create_project_with_attrs(pdata, self._neighborhood)
@@ -420,7 +418,7 @@ class NeighborhoodRestController(object):
         }
 
 
-class ProjectRestController(object):
+class ProjectRestController:
 
     @expose()
     def _lookup(self, name, *remainder):
@@ -445,8 +443,8 @@ class ProjectRestController(object):
     @expose('json:')
     def index(self, **kw):
         if 'doap' in kw:
-            response.headers['Content-Type'] = str('')
-            response.content_type = str('application/rdf+xml')
+            response.headers['Content-Type'] = ''
+            response.content_type = 'application/rdf+xml'
             return b'<?xml version="1.0" encoding="UTF-8" ?>' + c.project.doap()
         return c.project.__json__()
 
diff --git a/Allura/allura/controllers/root.py b/Allura/allura/controllers/root.py
index 94086bf..72dacc7 100644
--- a/Allura/allura/controllers/root.py
+++ b/Allura/allura/controllers/root.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-
 #       Licensed to the Apache Software Foundation (ASF) under one
 #       or more contributor license agreements.  See the NOTICE file
 #       distributed with this work for additional information
@@ -82,7 +80,7 @@ class RootController(WsgiDispatchController):
     browse = ProjectBrowseController()
 
     def __init__(self):
-        super(RootController, self).__init__()
+        super().__init__()
         self.nf.admin = SiteAdminController()
 
     @expose()
@@ -110,7 +108,7 @@ class RootController(WsgiDispatchController):
             # pylons.configuration defaults to "no-cache" only.
             # See also http://blog.55minutes.com/2011/10/how-to-defeat-the-browser-back-button-cache/ and
             # https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching?hl=en#defining_optimal_cache-control_policy
-            response.headers[str('Cache-Control')] = str('no-cache, no-store, must-revalidate')
+            response.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate'
 
     @expose()
     @with_trailing_slash
diff --git a/Allura/allura/controllers/search.py b/Allura/allura/controllers/search.py
index c5c454c..f0db69f 100644
--- a/Allura/allura/controllers/search.py
+++ b/Allura/allura/controllers/search.py
@@ -75,7 +75,7 @@ class ProjectBrowseController(BaseController):
         if self.category:
             title = self.category.label
             if self.parent_category:
-                title = "{}: {}".format(self.parent_category.label, title)
+                title = f"{self.parent_category.label}: {title}"
         return title
 
     def _build_nav(self):
diff --git a/Allura/allura/controllers/site_admin.py b/Allura/allura/controllers/site_admin.py
index 2bcf8b0..64fe690 100644
--- a/Allura/allura/controllers/site_admin.py
+++ b/Allura/allura/controllers/site_admin.py
@@ -50,8 +50,6 @@ import allura
 
 from six.moves.urllib.parse import urlparse
 import six
-from six.moves import range
-from six.moves import map
 
 
 log = logging.getLogger(__name__)
@@ -64,7 +62,7 @@ class W:
     admin_search_form = forms.AdminSearchForm
 
 
-class SiteAdminController(object):
+class SiteAdminController:
 
     def __init__(self):
         self.task_manager = TaskManagerController()
@@ -226,7 +224,7 @@ class SiteAdminController(object):
                 for msg in list(c.form_errors):
                     names = {'prefix': 'Neighborhood prefix', 'shortname':
                              'Project shortname', 'mount_point': 'Repository mount point'}
-                    error_msg += '{}: {} '.format(names[msg], c.form_errors[msg])
+                    error_msg += f'{names[msg]}: {c.form_errors[msg]} '
                     flash(error_msg, 'error')
                 return dict(prefix=prefix, shortname=shortname, mount_point=mount_point)
             nbhd = M.Neighborhood.query.get(url_prefix='/%s/' % prefix)
@@ -290,7 +288,7 @@ class SiteAdminController(object):
         def convert_fields(obj):
             # throw the type away (e.g. '_s' from 'url_s')
             result = {}
-            for k,val in six.iteritems(obj):
+            for k,val in obj.items():
                 name = k.rsplit('_', 1)
                 if len(name) == 2:
                     name = name[0]
@@ -343,7 +341,7 @@ class SiteAdminController(object):
         return r
 
 
-class DeleteProjectsController(object):
+class DeleteProjectsController:
     delete_form_validators = dict(
         projects=v.UnicodeString(if_empty=None),
         reason=v.UnicodeString(if_empty=None),
@@ -368,7 +366,7 @@ class DeleteProjectsController(object):
         template = '{}    # {}'
         lines = []
         for input, p, error in projects:
-            comment = 'OK: {}'.format(p.url()) if p else error
+            comment = f'OK: {p.url()}' if p else error
             lines.append(template.format(input, comment))
         return '\n'.join(lines)
 
@@ -415,15 +413,15 @@ class DeleteProjectsController(object):
             redirect('.')
         task_params = ' '.join(task_params)
         if reason:
-            task_params = '-r {} {}'.format(pipes.quote(reason), task_params)
+            task_params = f'-r {pipes.quote(reason)} {task_params}'
         if disable_users:
-            task_params = '--disable-users {}'.format(task_params)
+            task_params = f'--disable-users {task_params}'
         DeleteProjects.post(task_params)
         flash('Delete scheduled', 'ok')
         redirect('.')
 
 
-class SiteNotificationController(object):
+class SiteNotificationController:
 
     def __init__(self, note=None):
         self.note = note
@@ -517,7 +515,7 @@ class SiteNotificationController(object):
         redirect(six.ensure_text(request.referer or '/'))
 
 
-class TaskManagerController(object):
+class TaskManagerController:
 
     def _check_security(self):
         require_site_admin(c.user)
@@ -627,7 +625,7 @@ class TaskManagerController(object):
         return dict(doc=doc, error=error)
 
 
-class StatsController(object):
+class StatsController:
     """Show neighborhood stats."""
     @expose('jinja:allura:templates/site_admin_stats.html')
     @with_trailing_slash
@@ -643,7 +641,7 @@ class StatsController(object):
         return dict(neighborhoods=neighborhoods)
 
 
-class AdminUserDetailsController(object):
+class AdminUserDetailsController:
 
     @expose('jinja:allura:templates/site_admin_user_details.html')
     @without_trailing_slash
@@ -697,7 +695,7 @@ class AdminUserDetailsController(object):
             M.AuditLog.comment_user(c.user, comment, user=user)
             flash('Comment added', 'ok')
         else:
-            flash('Can not add comment "{}" for user {}'.format(comment, user))
+            flash(f'Can not add comment "{comment}" for user {user}')
         redirect(six.ensure_text(request.referer or '/'))
 
     @expose()
diff --git a/Allura/allura/controllers/task.py b/Allura/allura/controllers/task.py
index c9e46f6..dfaf418 100644
--- a/Allura/allura/controllers/task.py
+++ b/Allura/allura/controllers/task.py
@@ -20,7 +20,7 @@
 import six
 
 
-class TaskController(object):
+class TaskController:
 
     '''WSGI app providing web-like RPC
 
@@ -36,6 +36,6 @@ class TaskController(object):
         nocapture = environ['nocapture']
         result = task(restore_context=False, nocapture=nocapture)
         py_response = context.response
-        py_response.headers['Content-Type'] = str('text/plain')  # `None` default is problematic for some middleware
+        py_response.headers['Content-Type'] = 'text/plain'  # `None` default is problematic for some middleware
         py_response.body = six.ensure_binary(result or b'')
         return py_response
diff --git a/Allura/allura/controllers/trovecategories.py b/Allura/allura/controllers/trovecategories.py
index 0e2e41f..1c0c14a 100644
--- a/Allura/allura/controllers/trovecategories.py
+++ b/Allura/allura/controllers/trovecategories.py
@@ -34,14 +34,14 @@ from allura.app import SitemapEntry
 import six
 
 
-class F(object):
+class F:
     remove_category_form = forms.RemoveTroveCategoryForm()
     add_category_form = forms.AddTroveCategoryForm()
 
 
 class TroveAdminException(Exception):
     def __init__(self, flash_args, redir_params='', upper=None):
-        super(TroveAdminException, self).__init__()
+        super().__init__()
 
         self.flash_args = flash_args
         self.redir_params = redir_params
@@ -67,7 +67,7 @@ class TroveCategoryController(BaseController):
 
     def __init__(self, category=None):
         self.category = category
-        super(TroveCategoryController, self).__init__()
+        super().__init__()
 
     @expose('jinja:allura:templates/trovecategories.html')
     def index(self, **kw):
@@ -99,7 +99,7 @@ class TroveCategoryController(BaseController):
             (self.generate_category(child) for child in category.subcategories)
         }
 
-        return category.fullname, OrderedDict(sorted(six.iteritems(children)))
+        return category.fullname, OrderedDict(sorted(children.items()))
 
     @without_trailing_slash
     @expose('jinja:allura:templates/browse_trove_categories.html')
@@ -110,7 +110,7 @@ class TroveCategoryController(BaseController):
             for (key, value) in
             (self.generate_category(child) for child in parent_categories)
         }
-        return dict(tree=OrderedDict(sorted(six.iteritems(tree))))
+        return dict(tree=OrderedDict(sorted(tree.items())))
 
     @classmethod
     def _create(cls, name, upper_id, shortname):
@@ -131,7 +131,7 @@ class TroveCategoryController(BaseController):
 
         if upper:
             trove_type = upper.fullpath.split(' :: ')[0]
-            fullpath_re = re.compile(r'^{} :: '.format(re.escape(trove_type)))  # e.g. scope within "Topic :: "
+            fullpath_re = re.compile(fr'^{re.escape(trove_type)} :: ')  # e.g. scope within "Topic :: "
         else:
             # no parent, so making a top-level.  Don't limit fullpath_re, so enforcing global uniqueness
             fullpath_re = re.compile(r'')
@@ -139,8 +139,8 @@ class TroveCategoryController(BaseController):
 
         if oldcat:
             raise TroveAdminException(
-                ('A category with shortname "{}" already exists ({}).  Try a different, unique shortname'.format(shortname, oldcat.fullpath), "error"),
-                '?categoryname={}&shortname={}'.format(name, shortname),
+                (f'A category with shortname "{shortname}" already exists ({oldcat.fullpath}).  Try a different, unique shortname', "error"),
+                f'?categoryname={name}&shortname={shortname}',
                 upper
             )
         else:
@@ -171,9 +171,9 @@ class TroveCategoryController(BaseController):
         flash(*flash_args)
 
         if upper:
-            redirect('/categories/{}/{}'.format(upper.trove_cat_id, redir_params))
+            redirect(f'/categories/{upper.trove_cat_id}/{redir_params}')
         else:
-            redirect('/categories/{}'.format(redir_params))
+            redirect(f'/categories/{redir_params}')
 
     @expose()
     @require_post()
diff --git a/Allura/allura/ext/admin/admin_main.py b/Allura/allura/ext/admin/admin_main.py
index 314051f..43cacf3 100644
--- a/Allura/allura/ext/admin/admin_main.py
+++ b/Allura/allura/ext/admin/admin_main.py
@@ -52,7 +52,6 @@ from allura.lib.widgets.project_list import ProjectScreenshots
 
 from . import widgets as aw
 import six
-from six.moves import map
 
 
 log = logging.getLogger(__name__)
@@ -102,7 +101,7 @@ class AdminApp(Application):
     @staticmethod
     def installable_tools_for(project):
         tools = []
-        for name, App in six.iteritems(g.entry_points['tool']):
+        for name, App in g.entry_points['tool'].items():
             cfg = M.AppConfig(project_id=project._id, tool_name=name)
             if App._installable(name, project.neighborhood, project.app_configs):
                 tools.append(dict(name=name, app=App))
@@ -186,7 +185,7 @@ class AdminApp(Application):
         json.dump(self.project, f, cls=jsonify.JSONEncoder, indent=2)
 
 
-class AdminExtensionLookup(object):
+class AdminExtensionLookup:
     @expose()
     def _lookup(self, name, *remainder):
         for ep_name in sorted(g.entry_points['admin'].keys()):
@@ -250,7 +249,7 @@ class ProjectAdminController(BaseController):
 
         trove_recommendations = {}
         for trove in base_troves:
-            config_name = 'trovecategories.admin.recommended.{}'.format(trove.shortname)
+            config_name = f'trovecategories.admin.recommended.{trove.shortname}'
             recommendation_pairs = aslist(config.get(config_name, []), ',')
             trove_recommendations[trove.shortname] = OrderedDict()
             for pair in recommendation_pairs:
@@ -618,7 +617,7 @@ class ProjectAdminController(BaseController):
                 App = g.entry_points['tool'][ep_name]
                 # pass only options which app expects
                 config_on_install = {
-                    k: v for (k, v) in six.iteritems(kw)
+                    k: v for (k, v) in kw.items()
                     if k in [o.name for o in App.options_on_install()]
                 }
                 new_app = c.project.install_app(
@@ -641,14 +640,14 @@ class ProjectAdminController(BaseController):
             new_app = self._update_mounts(subproject, tool, new, **kw)
             if new_app:
                 if getattr(new_app, 'tool_label', '') == 'External Link':
-                    flash('{} installed successfully.'.format(new_app.tool_label))
+                    flash(f'{new_app.tool_label} installed successfully.')
                 else:
                     new_url = new_app.url
                     if callable(new_url):  # subprojects have a method instead of property
                         new_url = new_url()
                     redirect(new_url)
         except forge_exc.ForgeError as exc:
-            flash('{}: {}'.format(exc.__class__.__name__, exc.args[0]),
+            flash(f'{exc.__class__.__name__}: {exc.args[0]}',
                   'error')
         if request.referer is not None and tool is not None and 'delete' in tool[0] and \
             re.search(c.project.url() + r'(admin\/|)' + tool[0]['mount_point']+ r'\/*',
@@ -720,7 +719,7 @@ class ProjectAdminRestController(BaseController):
                 c.project.app_config(mount_point).options.ordinal = int(ordinal)
             except AttributeError as e:
                 # Handle sub project
-                p = M.Project.query.get(shortname="{}/{}".format(c.project.shortname, mount_point),
+                p = M.Project.query.get(shortname=f"{c.project.shortname}/{mount_point}",
                                         neighborhood_id=c.project.neighborhood_id)
                 if p:
                     p.ordinal = int(ordinal)
@@ -998,9 +997,9 @@ class PermissionsController(BaseController):
             perm = args['id']
             new_group_ids = args.get('new', [])
             group_ids = args.get('value', [])
-            if isinstance(new_group_ids, six.string_types):
+            if isinstance(new_group_ids, str):
                 new_group_ids = [new_group_ids]
-            if isinstance(group_ids, six.string_types):
+            if isinstance(group_ids, str):
                 group_ids = [group_ids]
             # make sure the admin group has the admin permission
             if perm == 'admin':
@@ -1018,7 +1017,7 @@ class PermissionsController(BaseController):
             role_ids = list(map(ObjectId, group_ids + new_group_ids))
             permissions[perm] = role_ids
         c.project.acl = []
-        for perm, role_ids in six.iteritems(permissions):
+        for perm, role_ids in permissions.items():
             role_names = lambda ids: ','.join(sorted(
                 pr.name for pr in M.ProjectRole.query.find(dict(_id={'$in': ids}))))
             old_role_ids = old_permissions.get(perm, [])
@@ -1146,7 +1145,7 @@ class GroupsController(BaseController):
             return dict(error='User %s not found' % username)
         user_role = M.ProjectRole.by_user(user, upsert=True)
         if group._id in user_role.roles:
-            return dict(error='{} ({}) is already in the group {}.'.format(user.display_name, username, group.name))
+            return dict(error=f'{user.display_name} ({username}) is already in the group {group.name}.')
         M.AuditLog.log('add user %s to %s', username, group.name)
         user_role.roles.append(group._id)
         if group.name == 'Admin':
@@ -1170,7 +1169,7 @@ class GroupsController(BaseController):
             return dict(error='User %s not found' % username)
         user_role = M.ProjectRole.by_user(user)
         if not user_role or group._id not in user_role.roles:
-            return dict(error='{} ({}) is not in the group {}.'.format(user.display_name, username, group.name))
+            return dict(error=f'{user.display_name} ({username}) is not in the group {group.name}.')
         M.AuditLog.log('remove user %s from %s', username, group.name)
         user_role.roles.remove(group._id)
         if len(user_role.roles) == 0:
diff --git a/Allura/allura/ext/admin/widgets.py b/Allura/allura/ext/admin/widgets.py
index 6ce8280..d41ef33 100644
--- a/Allura/allura/ext/admin/widgets.py
+++ b/Allura/allura/ext/admin/widgets.py
@@ -241,8 +241,7 @@ class AuditLog(ew_core.Widget):
 
     def resources(self):
         for f in self.fields:
-            for r in f.resources():
-                yield r
+            yield from f.resources()
 
 
 class BlockUser(ffw.Lightbox):
diff --git a/Allura/allura/ext/personal_dashboard/dashboard_main.py b/Allura/allura/ext/personal_dashboard/dashboard_main.py
index 7c227a2..d87aee7 100644
--- a/Allura/allura/ext/personal_dashboard/dashboard_main.py
+++ b/Allura/allura/ext/personal_dashboard/dashboard_main.py
@@ -28,7 +28,6 @@ from allura.controllers.feed import FeedController
 from allura.lib.widgets.user_profile import SectionBase, SectionsUtil, ProjectsSectionBase
 from allura.lib.widgets import form_fields as ffw
 from paste.deploy.converters import asbool
-from six.moves import filter
 
 log = logging.getLogger(__name__)
 
diff --git a/Allura/allura/ext/project_home/project_main.py b/Allura/allura/ext/project_home/project_main.py
index 0710ea6..a860774 100644
--- a/Allura/allura/ext/project_home/project_main.py
+++ b/Allura/allura/ext/project_home/project_main.py
@@ -75,7 +75,7 @@ class ProjectHomeApp(Application):
         return []
 
     def install(self, project):
-        super(ProjectHomeApp, self).install(project)
+        super().install(project)
         pr = model.ProjectRole.by_user(c.user)
         if pr:
             self.config.acl = [
diff --git a/Allura/allura/ext/search/search_main.py b/Allura/allura/ext/search/search_main.py
index 08ca425..0c54021 100644
--- a/Allura/allura/ext/search/search_main.py
+++ b/Allura/allura/ext/search/search_main.py
@@ -28,7 +28,6 @@ from allura import version
 from allura.lib.search import search_app
 from allura.lib.widgets.search import SearchResults, SearchHelp
 from allura.controllers import BaseController
-from six.moves import map
 
 log = logging.getLogger(__name__)
 
diff --git a/Allura/allura/ext/user_profile/user_main.py b/Allura/allura/ext/user_profile/user_main.py
index 6c5168b..c23e481 100644
--- a/Allura/allura/ext/user_profile/user_main.py
+++ b/Allura/allura/ext/user_profile/user_main.py
@@ -45,7 +45,7 @@ from allura.model import User, ACE, ProjectRole
 log = logging.getLogger(__name__)
 
 
-class F(object):
+class F:
     send_message = SendMessageForm()
 
 
@@ -281,7 +281,7 @@ class ProfileSectionBase(SectionBase):
         :param:`project`.  Stores the values as attributes of
         the same name.
         """
-        super(ProfileSectionBase, self).__init__(user)
+        super().__init__(user)
         self.project = project
 
 
diff --git a/Allura/allura/lib/__init__.py b/Allura/allura/lib/__init__.py
index cbf1ae0..144e298 100644
--- a/Allura/allura/lib/__init__.py
+++ b/Allura/allura/lib/__init__.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-
 #       Licensed to the Apache Software Foundation (ASF) under one
 #       or more contributor license agreements.  See the NOTICE file
 #       distributed with this work for additional information
diff --git a/Allura/allura/lib/app_globals.py b/Allura/allura/lib/app_globals.py
index e8d6037..500c4f7 100644
--- a/Allura/allura/lib/app_globals.py
+++ b/Allura/allura/lib/app_globals.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-
 #       Licensed to the Apache Software Foundation (ASF) under one
 #       or more contributor license agreements.  See the NOTICE file
 #       distributed with this work for additional information
@@ -149,7 +147,7 @@ class ForgeMarkdown(markdown.Markdown):
         return html
 
 
-class Globals(object):
+class Globals:
 
     """Container for objects available throughout the life of the application.
 
@@ -297,7 +295,7 @@ class Globals(object):
 
         # Set listeners to update stats
         statslisteners = []
-        for name, ep in six.iteritems(self.entry_points['stats']):
+        for name, ep in self.entry_points['stats'].items():
             statslisteners.append(ep())
         self.statsUpdater = PostEvent(statslisteners)
 
@@ -322,7 +320,7 @@ class Globals(object):
         if asbool(config.get('activitystream.recording.enabled', False)):
             return activitystream.director()
         else:
-            class NullActivityStreamDirector(object):
+            class NullActivityStreamDirector:
 
                 def connect(self, *a, **kw):
                     pass
@@ -353,7 +351,7 @@ class Globals(object):
             except AttributeError:
                 script_without_ming_middleware = True
             else:
-                script_without_ming_middleware = env['PATH_INFO'] == str('--script--')
+                script_without_ming_middleware = env['PATH_INFO'] == '--script--'
             if script_without_ming_middleware:
                 kwargs['flush_immediately'] = True
             else:
@@ -535,12 +533,12 @@ class Globals(object):
     def register_app_css(self, href, **kw):
         app = kw.pop('app', c.app)
         self.resource_manager.register(
-            ew.CSSLink('tool/{}/{}'.format(app.config.tool_name.lower(), href), **kw))
+            ew.CSSLink(f'tool/{app.config.tool_name.lower()}/{href}', **kw))
 
     def register_app_js(self, href, **kw):
         app = kw.pop('app', c.app)
         self.resource_manager.register(
-            ew.JSLink('tool/{}/{}'.format(app.config.tool_name.lower(), href), **kw))
+            ew.JSLink(f'tool/{app.config.tool_name.lower()}/{href}', **kw))
 
     def register_theme_css(self, href, **kw):
         self.resource_manager.register(ew.CSSLink(self.theme_href(href), **kw))
@@ -571,7 +569,7 @@ class Globals(object):
         'h.set_context() is preferred over this method'
         if isinstance(pid_or_project, M.Project):
             c.project = pid_or_project
-        elif isinstance(pid_or_project, six.string_types):
+        elif isinstance(pid_or_project, str):
             raise TypeError('need a Project instance, got %r' % pid_or_project)
         elif pid_or_project is None:
             c.project = None
@@ -588,7 +586,7 @@ class Globals(object):
 
     @LazyProperty
     def noreply(self):
-        return six.text_type(config.get('noreply', 'noreply@%s' % config['domain']))
+        return str(config.get('noreply', 'noreply@%s' % config['domain']))
 
     @property
     def build_key(self):
@@ -632,7 +630,7 @@ class Globals(object):
             "image_height": logo['image_height']
         }
 
-class Icon(object):
+class Icon:
 
     def __init__(self, css, title=None):
         self.css = css
@@ -650,7 +648,7 @@ class Icon(object):
         attrs = ew._Jinja2Widget().j2_attrs(attrs)
         visible_title = ''
         if show_title:
-            visible_title = '&nbsp;{}'.format(Markup.escape(title))
-        closing_tag = '</{}>'.format(tag) if closing_tag else ''
-        icon = '<{} {}><i class="{}"></i>{}{}'.format(tag, attrs, self.css, visible_title, closing_tag)
+            visible_title = f'&nbsp;{Markup.escape(title)}'
+        closing_tag = f'</{tag}>' if closing_tag else ''
+        icon = f'<{tag} {attrs}><i class="{self.css}"></i>{visible_title}{closing_tag}'
         return Markup(icon)
diff --git a/Allura/allura/lib/base.py b/Allura/allura/lib/base.py
index c0781f8..4dbde9c 100644
--- a/Allura/allura/lib/base.py
+++ b/Allura/allura/lib/base.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-
 #       Licensed to the Apache Software Foundation (ASF) under one
 #       or more contributor license agreements.  See the NOTICE file
 #       distributed with this work for additional information
@@ -39,4 +37,4 @@ class WsgiDispatchController(TGController):
 
     def _perform_call(self, context):
         self._setup_request()
-        return super(WsgiDispatchController, self)._perform_call(context)
+        return super()._perform_call(context)
diff --git a/Allura/allura/lib/custom_middleware.py b/Allura/allura/lib/custom_middleware.py
index f121f49..503881d 100644
--- a/Allura/allura/lib/custom_middleware.py
+++ b/Allura/allura/lib/custom_middleware.py
@@ -35,7 +35,6 @@ from allura.lib import helpers as h
 from allura.lib.utils import is_ajax
 from allura import model as M
 import allura.model.repository
-from six.moves import range
 
 log = logging.getLogger(__name__)
 
@@ -43,7 +42,7 @@ log = logging.getLogger(__name__)
 tool_entry_points = list(h.iter_entry_points('allura'))
 
 
-class StaticFilesMiddleware(object):
+class StaticFilesMiddleware:
 
     '''Custom static file middleware
 
@@ -80,13 +79,13 @@ class StaticFilesMiddleware(object):
                 resource_cls = ep.load().has_resource(resource_path)
                 if resource_cls:
                     file_path = pkg_resources.resource_filename(resource_cls.__module__, resource_path)
-                    return fileapp.FileApp(file_path, [(str('Access-Control-Allow-Origin'), str('*'))])
+                    return fileapp.FileApp(file_path, [('Access-Control-Allow-Origin', '*')])
         filename = environ['PATH_INFO'][len(self.script_name):]
         file_path = pkg_resources.resource_filename('allura', os.path.join('public', 'nf', filename))
-        return fileapp.FileApp(file_path, [(str('Access-Control-Allow-Origin'), str('*'))])
+        return fileapp.FileApp(file_path, [('Access-Control-Allow-Origin', '*')])
 
 
-class CORSMiddleware(object):
+class CORSMiddleware:
     '''Enables Cross-Origin Resource Sharing for REST API'''
 
     def __init__(self, app, allowed_methods, allowed_headers, cache=None):
@@ -96,7 +95,7 @@ class CORSMiddleware(object):
         self.cache_preflight = cache or None
 
     def __call__(self, environ, start_response):
-        is_api_request = environ.get('PATH_INFO', '').startswith(str('/rest/'))
+        is_api_request = environ.get('PATH_INFO', '').startswith('/rest/')
         valid_cors = 'HTTP_ORIGIN' in environ
         if not is_api_request or not valid_cors:
             return self.app(environ, start_response)
@@ -125,17 +124,17 @@ class CORSMiddleware(object):
         return r(environ, start_response)
 
     def get_response_headers(self, preflight=False):
-        headers = [(str('Access-Control-Allow-Origin'), str('*'))]
+        headers = [('Access-Control-Allow-Origin', '*')]
         if preflight:
             ac_methods = ', '.join(self.allowed_methods)
             ac_headers = ', '.join(sorted(self.allowed_headers))
             headers.extend([
-                (str('Access-Control-Allow-Methods'), str(ac_methods)),
-                (str('Access-Control-Allow-Headers'), str(ac_headers)),
+                ('Access-Control-Allow-Methods', str(ac_methods)),
+                ('Access-Control-Allow-Headers', str(ac_headers)),
             ])
             if self.cache_preflight:
                 headers.append(
-                    (str('Access-Control-Max-Age'), str(self.cache_preflight))
+                    ('Access-Control-Max-Age', str(self.cache_preflight))
                 )
         return headers
 
@@ -144,7 +143,7 @@ class CORSMiddleware(object):
         return {h.strip().lower() for h in headers.split(',') if h.strip()}
 
 
-class LoginRedirectMiddleware(object):
+class LoginRedirectMiddleware:
 
     '''Actually converts a 401 into a 302 so we can do a redirect to a different
     app for login.  (StatusCodeRedirect does a WSGI-only redirect which cannot
@@ -155,7 +154,7 @@ class LoginRedirectMiddleware(object):
 
     def __call__(self, environ, start_response):
         status, headers, app_iter, exc_info = call_wsgi_application(self.app, environ)
-        is_api_request = environ.get('PATH_INFO', '').startswith(str('/rest/'))
+        is_api_request = environ.get('PATH_INFO', '').startswith('/rest/')
         if status[:3] == '401' and not is_api_request and not is_ajax(Request(environ)):
             login_url = tg.config.get('auth.login_url', '/auth/')
             if environ['REQUEST_METHOD'] == 'GET':
@@ -172,7 +171,7 @@ class LoginRedirectMiddleware(object):
         return app_iter
 
 
-class CSRFMiddleware(object):
+class CSRFMiddleware:
 
     '''On POSTs, looks for a special field name that matches the value of a given
     cookie.  If this field is missing, the cookies are cleared to anonymize the
@@ -217,14 +216,14 @@ class CSRFMiddleware(object):
             if dict(headers).get('Content-Type', '').startswith('text/html'):
                 use_secure = 'secure; ' if environ['beaker.session'].secure else ''
                 headers.append(
-                    (str('Set-cookie'),
-                     str('{}={}; {}Path=/'.format(self._cookie_name, cookie, use_secure))))
+                    ('Set-cookie',
+                     str(f'{self._cookie_name}={cookie}; {use_secure}Path=/')))
             return start_response(status, headers, exc_info)
 
         return self._app(environ, session_start_response)
 
 
-class SSLMiddleware(object):
+class SSLMiddleware:
 
     'Verify the https/http schema is correct'
 
@@ -267,7 +266,7 @@ class SSLMiddleware(object):
         return resp(environ, start_response)
 
 
-class SetRequestHostFromConfig(object):
+class SetRequestHostFromConfig:
     """
     Set request properties for host and port, based on the 'base_url' config setting.
     This permits code to use request.host etc to construct URLs correctly, even when behind a proxy, like in docker
@@ -300,10 +299,7 @@ class AlluraTimerMiddleware(TimerMiddleware):
         import ming
         import pymongo
         import socket
-        if six.PY2:
-            import urllib2 as urlopen_pkg
-        else:
-            import urllib.request as urlopen_pkg
+        import urllib.request as urlopen_pkg
         import activitystream
         import pygments
         import difflib
@@ -400,7 +396,7 @@ class AlluraTimerMiddleware(TimerMiddleware):
         return timers
 
 
-class RememberLoginMiddleware(object):
+class RememberLoginMiddleware:
     '''
     This middleware changes session's cookie expiration time according to login_expires
     session variable'''
@@ -424,19 +420,19 @@ class RememberLoginMiddleware(object):
                     session._set_cookie_expires(login_expires)
                 # Replace the cookie header that SessionMiddleware set
                 # with one that has the new expires parameter value
-                cookie = session.cookie[session.key].output(header=str(''))
+                cookie = session.cookie[session.key].output(header='')
                 for i in range(len(headers)):
                     header, contents = headers[i]
                     if header == 'Set-cookie' and \
                             contents.lstrip().startswith(session.key):
-                        headers[i] = (str('Set-cookie'), cookie)
+                        headers[i] = ('Set-cookie', cookie)
                         break
             return start_response(status, headers, exc_info)
 
         return self.app(environ, remember_login_start_response)
 
 
-class MingTaskSessionSetupMiddleware(object):
+class MingTaskSessionSetupMiddleware:
     '''
     This middleware ensures there is a "task" session always established.  This avoids:
 
diff --git a/Allura/allura/lib/decorators.py b/Allura/allura/lib/decorators.py
index edfcaca..241164a 100644
--- a/Allura/allura/lib/decorators.py
+++ b/Allura/allura/lib/decorators.py
@@ -20,10 +20,7 @@ import sys
 import json
 import logging
 import six
-if six.PY3:
-    from http.cookies import SimpleCookie as Cookie
-else:
-    from Cookie import Cookie
+from http.cookies import SimpleCookie as Cookie
 from collections import defaultdict
 from six.moves.urllib.parse import unquote
 from datetime import datetime
@@ -85,7 +82,7 @@ def task(*args, **kw):
     return task_
 
 
-class event_handler(object):
+class event_handler:
 
     '''Decorator to register event handlers'''
     listeners = defaultdict(set)
@@ -99,7 +96,7 @@ class event_handler(object):
         return func
 
 
-class require_post(object):
+class require_post:
     '''
     A decorator to require controllers by accessed with a POST only.  Use whenever data will be modified by a
     controller, since that's what POST is good for.  We have CSRF protection middleware on POSTs, too.
@@ -113,7 +110,7 @@ class require_post(object):
             if request.method != 'POST':
                 if self.redir is not None:
                     redirect(self.redir)
-                raise exc.HTTPMethodNotAllowed(headers={str('Allow'): str('POST')})
+                raise exc.HTTPMethodNotAllowed(headers={'Allow': 'POST'})
         before_validate(check_method)(func)
         return func
 
@@ -161,7 +158,7 @@ def memoize(func, instance, args, kwargs):
         dic = getattr_(func, "_memoize_dic", dict)
     else:
         # decorating a method
-        dic = getattr_(instance, "_memoize_dic__{}".format(func.__name__), dict)
+        dic = getattr_(instance, f"_memoize_dic__{func.__name__}", dict)
 
     cache_key = (args, frozenset(list(kwargs.items())))
     if cache_key in dic:
diff --git a/Allura/allura/lib/diff.py b/Allura/allura/lib/diff.py
index 85b1023..77cffdf 100644
--- a/Allura/allura/lib/diff.py
+++ b/Allura/allura/lib/diff.py
@@ -22,7 +22,7 @@ import six
 from allura.lib import helpers as h
 
 
-class HtmlSideBySideDiff(object):
+class HtmlSideBySideDiff:
 
     table_tmpl = '''
 <table class="side-by-side-diff">
diff --git a/Allura/allura/lib/exceptions.py b/Allura/allura/lib/exceptions.py
index c271348..47a8cdb 100644
--- a/Allura/allura/lib/exceptions.py
+++ b/Allura/allura/lib/exceptions.py
@@ -17,7 +17,6 @@
 
 import webob.exc
 from formencode import Invalid
-from six.moves import map
 
 
 class ForgeError(Exception):
@@ -29,7 +28,7 @@ class ProjectConflict(ForgeError, Invalid):
     # support the single string constructor in addition to full set of params
     # that Invalid.__init__ requires
     def __init__(self, msg, value=None, state=None, error_list=None, error_dict=None):
-        super(ProjectConflict, self).__init__(
+        super().__init__(
             msg, value, state, error_list, error_dict)
 
 
diff --git a/Allura/allura/lib/gravatar.py b/Allura/allura/lib/gravatar.py
index bde5305..61a0e48 100644
--- a/Allura/allura/lib/gravatar.py
+++ b/Allura/allura/lib/gravatar.py
@@ -88,7 +88,7 @@ def url(email=None, gravatar_id=None, **kw):
         kw['r'] = 'pg'
     if 'd' not in kw and config.get('default_avatar_image'):
         kw['d'] = h.absurl(config['default_avatar_image'])
-    return ('https://secure.gravatar.com/avatar/{}?{}'.format(gravatar_id, six.moves.urllib.parse.urlencode(kw)))
+    return (f'https://secure.gravatar.com/avatar/{gravatar_id}?{six.moves.urllib.parse.urlencode(kw)}')
 
 
 def for_user(user):
diff --git a/Allura/allura/lib/helpers.py b/Allura/allura/lib/helpers.py
index 64331b3..0e280b1 100644
--- a/Allura/allura/lib/helpers.py
+++ b/Allura/allura/lib/helpers.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-
 #       Licensed to the Apache Software Foundation (ASF) under one
 #       or more contributor license agreements.  See the NOTICE file
 #       distributed with this work for additional information
@@ -45,8 +43,6 @@ import cgi
 import emoji
 import tg
 import six
-from six.moves import range
-from six.moves import map
 import cchardet as chardet
 import pkg_resources
 from formencode.validators import FancyValidator
@@ -213,13 +209,13 @@ def _attempt_encodings(s, encodings):
                 if six.PY3 and isinstance(s, bytes):
                     # special handling for bytes (avoid b'asdf' turning into "b'asfd'")
                     return s.decode('utf-8')
-                return six.text_type(s)  # try default encoding, and handle other types like int, etc
+                return str(s)  # try default encoding, and handle other types like int, etc
             else:
-                return six.text_type(s, enc)
+                return str(s, enc)
         except (UnicodeDecodeError, LookupError):
             pass
     # Return the repr of the str -- should always be safe
-    return six.text_type(repr(str(s)))[1:-1]
+    return str(repr(str(s)))[1:-1]
 
 
 def really_unicode(s):
@@ -295,7 +291,7 @@ def push_config(obj, **kw):
 
     saved_attrs = {}
     new_attrs = []
-    for k, v in six.iteritems(kw):
+    for k, v in kw.items():
         try:
             saved_attrs[k] = getattr(obj, k)
         except AttributeError:
@@ -304,7 +300,7 @@ def push_config(obj, **kw):
     try:
         yield obj
     finally:
-        for k, v in six.iteritems(saved_attrs):
+        for k, v in saved_attrs.items():
             setattr(obj, k, v)
         for k in new_attrs:
             delattr(obj, k)
@@ -363,7 +359,7 @@ def set_context(project_shortname_or_id, mount_point=None, app_config_id=None, n
     if app_config_id is None:
         c.app = p.app_instance(mount_point)
     else:
-        if isinstance(app_config_id, six.string_types):
+        if isinstance(app_config_id, str):
             app_config_id = ObjectId(app_config_id)
         app_config = model.AppConfig.query.get(_id=app_config_id)
         c.app = p.app_instance(app_config)
@@ -405,7 +401,7 @@ def encode_keys(d):
     a valid kwargs argument'''
     return {
         six.ensure_str(k): v
-        for k, v in six.iteritems(d)}
+        for k, v in d.items()}
 
 
 def vardec(fun):
@@ -428,7 +424,7 @@ def convert_bools(conf, prefix=''):
     :return: dict
     '''
     def convert_value(val):
-        if isinstance(val, six.string_types):
+        if isinstance(val, str):
             if val.strip().lower() == 'true':
                 return True
             elif val.strip().lower() == 'false':
@@ -437,7 +433,7 @@ def convert_bools(conf, prefix=''):
 
     return {
         k: (convert_value(v) if k.startswith(prefix) else v)
-        for k, v in six.iteritems(conf)
+        for k, v in conf.items()
     }
 
 
@@ -447,10 +443,7 @@ def nonce(length=4):
 
 def cryptographic_nonce(length=40):
     rand_bytes = os.urandom(length)
-    if six.PY2:
-        rand_ints = tuple(map(ord, rand_bytes))
-    else:
-        rand_ints = tuple(rand_bytes)
+    rand_ints = tuple(rand_bytes)
     hex_format = '%.2x' * length
     return hex_format % rand_ints
 
@@ -573,12 +566,12 @@ def gen_message_id(_id=None):
 class ProxiedAttrMeta(type):
 
     def __init__(cls, name, bases, dct):
-        for v in six.itervalues(dct):
+        for v in dct.values():
             if isinstance(v, attrproxy):
                 v.cls = cls
 
 
-class attrproxy(object):
+class attrproxy:
     cls = None
 
     def __init__(self, *attrs):
@@ -606,18 +599,18 @@ class attrproxy(object):
 class promised_attrproxy(attrproxy):
 
     def __init__(self, promise, *attrs):
-        super(promised_attrproxy, self).__init__(*attrs)
+        super().__init__(*attrs)
         self._promise = promise
 
     def __repr__(self):
-        return '<promised_attrproxy for {}>'.format(self.attrs)
+        return f'<promised_attrproxy for {self.attrs}>'
 
     def __getattr__(self, name):
         cls = self._promise()
         return getattr(cls, name)
 
 
-class proxy(object):
+class proxy:
 
     def __init__(self, obj):
         self._obj = obj
@@ -637,15 +630,15 @@ class fixed_attrs_proxy(proxy):
     """
     def __init__(self, obj, **kw):
         self._obj = obj
-        for k, v in six.iteritems(kw):
+        for k, v in kw.items():
             setattr(self, k, v)
 
 
-@tg.expose(content_type=str('text/plain'))
+@tg.expose(content_type='text/plain')
 def json_validation_error(controller, **kwargs):
     exc = request.validation['exception']
     result = dict(status='Validation Error',
-                  errors={fld: str(err) for fld, err in six.iteritems(exc.error_dict)},
+                  errors={fld: str(err) for fld, err in exc.error_dict.items()},
                   value=exc.value,
                   params=kwargs)
     response.status = 400
@@ -673,7 +666,7 @@ def config_with_prefix(d, prefix):
     with the prefix stripped
     '''
     plen = len(prefix)
-    return {k[plen:]: v for k, v in six.iteritems(d)
+    return {k[plen:]: v for k, v in d.items()
                 if k.startswith(prefix)}
 
 
@@ -752,7 +745,7 @@ def _add_table_line_numbers_to_text(txt):
         linenumbers + '<td class="code"><div class="codehilite"><pre>'
     for line_num, line in enumerate(lines, 1):
         markup_text = markup_text + \
-            '<span id="l{}" class="code_block">{}</span>'.format(line_num, line)
+            f'<span id="l{line_num}" class="code_block">{line}</span>'
     markup_text = markup_text + '</pre></div></td></tr></tbody></table>'
     return markup_text
 
@@ -834,7 +827,7 @@ def datetimeformat(value, format='%Y-%m-%d %H:%M:%S'):
 
 @contextmanager
 def log_output(log):
-    class Writer(object):
+    class Writer:
 
         def __init__(self, func):
             self.func = func
@@ -940,7 +933,7 @@ def ming_config(**conf):
         yield
     finally:
         Session._datastores = datastores
-        for name, session in six.iteritems(Session._registry):
+        for name, session in Session._registry.items():
             session.bind = datastores.get(name, None)
             session._name = name
 
@@ -998,7 +991,7 @@ def null_contextmanager(returning=None, *args, **kw):
     yield returning
 
 
-class exceptionless(object):
+class exceptionless:
 
     '''Decorator making the decorated function return 'error_result' on any
     exceptions rather than propagating exceptions up the stack
@@ -1038,7 +1031,7 @@ def urlopen(url, retries=3, codes=(408, 500, 502, 503, 504), timeout=None):
     while True:
         try:
             return six.moves.urllib.request.urlopen(url, timeout=timeout)
-        except IOError as e:
+        except OSError as e:
             no_retry = isinstance(e, six.moves.urllib.error.HTTPError) and e.code not in codes
             if attempts < retries and not no_retry:
                 attempts += 1
@@ -1049,7 +1042,7 @@ def urlopen(url, retries=3, codes=(408, 500, 502, 503, 504), timeout=None):
                 except Exception:
                     url_string = url
                 if hasattr(e, 'filename') and url_string != e.filename:
-                    url_string += ' => {}'.format(e.filename)
+                    url_string += f' => {e.filename}'
                 if timeout is None:
                     timeout = socket.getdefaulttimeout()
                 if getattr(e, 'fp', None):
@@ -1086,15 +1079,7 @@ def plain2markdown(txt, preserve_multiple_spaces=False, has_html_entities=False)
     return txt
 
 
-if six.PY2:
-    # https://stackoverflow.com/a/35968897
-    # in python 3.7 this can just be a defaultdict, probably
-    class OrderedDefaultDict(OrderedDict, defaultdict):
-        def __init__(self, default_factory=None, *args, **kwargs):
-            super(OrderedDefaultDict, self).__init__(*args, **kwargs)
-            self.default_factory = default_factory
-else:
-    OrderedDefaultDict = defaultdict  # py3.7 dicts are always ordered
+OrderedDefaultDict = defaultdict  # py3.7 dicts are always ordered
 
 
 def iter_entry_points(group, *a, **kw):
@@ -1121,7 +1106,7 @@ def iter_entry_points(group, *a, **kw):
         by_name = OrderedDefaultDict(list)
         for ep in entry_points:
             by_name[ep.name].append(ep)
-        for name, eps in six.iteritems(by_name):
+        for name, eps in by_name.items():
             ep_count = len(eps)
             if ep_count == 1:
                 yield eps[0]
@@ -1130,7 +1115,7 @@ def iter_entry_points(group, *a, **kw):
 
     def subclass(entry_points):
         loaded = {ep: ep.load() for ep in entry_points}
-        for ep, cls in six.iteritems(loaded):
+        for ep, cls in loaded.items():
             others = list(loaded.values())[:]
             others.remove(cls)
             if all([issubclass(cls, other) for other in others]):
@@ -1213,9 +1198,9 @@ def auditlog_user(message, *args, **kwargs):
     """
     from allura import model as M
     ip_address = utils.ip_address(request)
-    message = 'IP Address: {}\nUser-Agent: {}\n'.format(ip_address, request.user_agent) + message
+    message = f'IP Address: {ip_address}\nUser-Agent: {request.user_agent}\n' + message
     if c.user and kwargs.get('user') and kwargs['user'] != c.user:
-        message = 'Done by user: {}\n'.format(c.user.username) + message
+        message = f'Done by user: {c.user.username}\n' + message
     return M.AuditLog.log_user(message, *args, **kwargs)
 
 
@@ -1269,7 +1254,7 @@ def base64uri(content_or_image, image_format='PNG', mimetype='image/png', window
         content = content.replace('\n', '\r\n')
 
     data = six.ensure_text(base64.b64encode(six.ensure_binary(content)))
-    return 'data:{};base64,{}'.format(mimetype, data)
+    return f'data:{mimetype};base64,{data}'
 
 
 def slugify(name, allow_periods=False):
@@ -1289,7 +1274,7 @@ email_re = re.compile(r'(([a-z0-9_]|\-|\.)+)@([\w\.-]+)', re.IGNORECASE)
 def hide_private_info(message):
     if asbool(tg.config.get('hide_private_info', 'true')) and message:
         hidden = email_re.sub(r'\1@...', message)
-        if type(message) not in six.string_types:
+        if type(message) not in (str,):
             # custom subclass like markupsafe.Markup, convert to that type again
             hidden = type(message)(hidden)
         return hidden
@@ -1315,7 +1300,7 @@ def username_project_url(user_or_username):
     if not user_or_username:
         return url
 
-    if isinstance(user_or_username, six.string_types):
+    if isinstance(user_or_username, str):
         class UserName:
             def __init__(self, username):
                 self.username = username
diff --git a/Allura/allura/lib/import_api.py b/Allura/allura/lib/import_api.py
index 768ac75..141c03c 100644
--- a/Allura/allura/lib/import_api.py
+++ b/Allura/allura/lib/import_api.py
@@ -26,7 +26,7 @@ from allura.lib.utils import urlencode
 log = logging.getLogger(__name__)
 
 
-class AlluraImportApiClient(object):
+class AlluraImportApiClient:
 
     def __init__(self, base_url, token, verbose=False, retry=True):
         self.base_url = base_url
@@ -51,12 +51,12 @@ class AlluraImportApiClient(object):
                 resp = result.read()
                 return json.loads(resp)
             except six.moves.urllib.error.HTTPError as e:
-                e.msg += ' ({})'.format(url)
+                e.msg += f' ({url})'
                 if self.verbose:
                     error_content = e.read()
                     e.msg += '. Error response:\n' + six.ensure_text(error_content)
                 raise e
-            except (six.moves.urllib.error.URLError, IOError):
+            except (six.moves.urllib.error.URLError, OSError):
                 if self.retry:
                     log.exception('Error making API request, will retry')
                     continue
diff --git a/Allura/allura/lib/macro.py b/Allura/allura/lib/macro.py
index 1fec43d..35eafe7 100644
--- a/Allura/allura/lib/macro.py
+++ b/Allura/allura/lib/macro.py
@@ -44,7 +44,7 @@ log = logging.getLogger(__name__)
 _macros = {}
 
 
-class macro(object):
+class macro:
 
     def __init__(self, context=None):
         self._context = context
@@ -54,7 +54,7 @@ class macro(object):
         return func
 
 
-class parse(object):
+class parse:
 
     def __init__(self, context):
         self._context = context
@@ -80,11 +80,11 @@ class parse(object):
                 log.warn('macro error.  Upwards stack is %s',
                          ''.join(traceback.format_stack()),
                          exc_info=True)
-                msg = cgi.escape('[[{}]] ({})'.format(s, repr(ex)))
+                msg = cgi.escape(f'[[{s}]] ({repr(ex)})')
                 return '\n<div class="error"><pre><code>%s</code></pre></div>' % msg
         except Exception as ex:
             raise
-            return '[[Error parsing {}: {}]]'.format(s, ex)
+            return f'[[Error parsing {s}: {ex}]]'
 
     def _lookup_macro(self, s):
         macro, context = _macros.get(s, (None, None))
@@ -360,7 +360,7 @@ def include_file(repo, path=None, rev=None, **kw):
     try:
         file = app.repo.commit(rev).get_path(path)
     except Exception:
-        return "[[include can't find file {} in revision {}]]".format(path, rev)
+        return f"[[include can't find file {path} in revision {rev}]]"
 
     text = ''
     if file.has_pypeline_view:
@@ -368,7 +368,7 @@ def include_file(repo, path=None, rev=None, **kw):
     elif file.has_html_view:
         text = g.highlight(file.text, filename=file.name)
     else:
-        return "[[include can't display file {} in revision {}]]".format(path, rev)
+        return f"[[include can't display file {path} in revision {rev}]]"
 
     from allura.lib.widgets.macros import Include
     sb = Include()
@@ -405,7 +405,7 @@ def include(ref=None, repo=None, **kw):
 
 @macro()
 def img(src=None, **kw):
-    attrs = ('%s="%s"' % t for t in six.iteritems(kw))
+    attrs = ('%s="%s"' % t for t in kw.items())
     included = request.environ.setdefault('allura.macro.att_embedded', set())
     included.add(src)
     if '://' in src:
@@ -452,8 +452,8 @@ def members(limit=20):
 def embed(url=None):
     consumer = oembed.OEmbedConsumer()
     endpoint = oembed.OEmbedEndpoint('https://www.youtube.com/oembed',
-                                     [str('http://*.youtube.com/*'), str('https://*.youtube.com/*'),
-                                      str('http://*.youtube-nocookie.com/*'), str('https://*.youtube-nocookie.com/*'),
+                                     ['http://*.youtube.com/*', 'https://*.youtube.com/*',
+                                      'http://*.youtube-nocookie.com/*', 'https://*.youtube-nocookie.com/*',
                                       ])
     consumer.addEndpoint(endpoint)
 
@@ -465,14 +465,14 @@ def embed(url=None):
         except oembed.OEmbedNoEndpoint:
             html = None
         except oembed.OEmbedError:
-            log.exception('Could not embed: {}'.format(url))
-            return 'Could not embed: {}'.format(url)
+            log.exception(f'Could not embed: {url}')
+            return f'Could not embed: {url}'
         except six.moves.urllib.error.HTTPError as e:
             if e.code in (403, 404):
                 return 'Video not available'
             else:
-                log.exception('Could not embed: {}'.format(url))
-                return 'Could not embed: {}'.format(url)
+                log.exception(f'Could not embed: {url}')
+                return f'Could not embed: {url}'
 
     if html:
         # youtube has a trailing ")" at the moment
diff --git a/Allura/allura/lib/mail_util.py b/Allura/allura/lib/mail_util.py
index 4869c1f..67808b7 100644
--- a/Allura/allura/lib/mail_util.py
+++ b/Allura/allura/lib/mail_util.py
@@ -34,7 +34,6 @@ from tg import app_globals as g
 from allura.lib.utils import ConfigProxy
 from allura.lib import exceptions as exc
 from allura.lib import helpers as h
-from six.moves import map
 
 log = logging.getLogger(__name__)
 
@@ -57,11 +56,11 @@ def Header(text, *more_text):
     # email.header.Header handles str vs unicode differently
     # see
     # http://docs.python.org/library/email.header.html#email.header.Header.append
-    if not isinstance(text, six.text_type):
+    if not isinstance(text, str):
         raise TypeError('This must be unicode: %r' % text)
     head = header.Header(text)
     for m in more_text:
-        if not isinstance(m, six.text_type):
+        if not isinstance(m, str):
             raise TypeError('This must be unicode: %r' % text)
         head.append(m)
     return head
@@ -73,7 +72,7 @@ def AddrHeader(fromaddr):
         foo@bar.com
         "Foo Bar" <fo...@bar.com>
     '''
-    if isinstance(fromaddr, six.string_types) and ' <' in fromaddr:
+    if isinstance(fromaddr, str) and ' <' in fromaddr:
         name, addr = fromaddr.rsplit(' <', 1)
         addr = '<' + addr  # restore the char we just split off
         addrheader = Header(name, addr)
@@ -134,14 +133,9 @@ def parse_message(data):
     # > A unicode string has no RFC defintion as an email, so things do not work right...
     # > You do have to conditionalize your 2/3 code to use the bytes parser and generator if you are dealing with 8-bit
     # > messages. There's just no way around that.
-    if six.PY2:
-        parser = email.feedparser.FeedParser()
-        parser.feed(data)
-        msg = parser.close()
-    else:
-        # works the same as BytesFeedParser, and better than non-"Bytes" parsers for some messages
-        parser = email.parser.BytesParser()
-        msg = parser.parsebytes(data.encode('utf-8'))
+    # works the same as BytesFeedParser, and better than non-"Bytes" parsers for some messages
+    parser = email.parser.BytesParser()
+    msg = parser.parsebytes(data.encode('utf-8'))
     # Extract relevant data
     result = {}
     result['multipart'] = multipart = msg.is_multipart()
@@ -256,7 +250,7 @@ def isvalid(addr):
         return False
 
 
-class SMTPClient(object):
+class SMTPClient:
 
     def __init__(self):
         self._client = None
@@ -281,7 +275,7 @@ class SMTPClient(object):
             message['CC'] = AddrHeader(cc)
             addrs.append(cc)
         if in_reply_to:
-            if not isinstance(in_reply_to, six.string_types):
+            if not isinstance(in_reply_to, str):
                 raise TypeError('Only strings are supported now, not lists')
             message['In-Reply-To'] = Header('<%s>' % in_reply_to)
             if not references:
@@ -305,7 +299,7 @@ class SMTPClient(object):
         smtp_addrs = [a for a in smtp_addrs if isvalid(a)]
         if not smtp_addrs:
             log.warning('No valid addrs in %s, so not sending mail',
-                        list(map(six.text_type, addrs)))
+                        list(map(str, addrs)))
             return
         try:
             self._client.sendmail(
diff --git a/Allura/allura/lib/markdown_extensions.py b/Allura/allura/lib/markdown_extensions.py
index ab11ca9..ba3fcf0 100644
--- a/Allura/allura/lib/markdown_extensions.py
+++ b/Allura/allura/lib/markdown_extensions.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-
 #       Licensed to the Apache Software Foundation (ASF) under one
 #       or more contributor license agreements.  See the NOTICE file
 #       distributed with this work for additional information
@@ -124,7 +122,7 @@ class CommitMessageExtension(markdown.Extension):
         self.forge_link_tree_processor.reset()
 
 
-class Pattern(object):
+class Pattern:
 
     """Base class for regex patterns used by the :class:`PatternReplacingProcessor`.
 
@@ -479,7 +477,7 @@ class RelativeLinkRewriter(markdown.postprocessors.Postprocessor):
             rewrite(link, 'src')
 
         # html5lib parser adds html/head/body tags, so output <body> without its own tags
-        return six.text_type(soup.body)[len('<body>'):-len('</body>')]
+        return str(soup.body)[len('<body>'):-len('</body>')]
 
     def _rewrite(self, tag, attr):
         val = tag.get(attr)
diff --git a/Allura/allura/lib/multifactor.py b/Allura/allura/lib/multifactor.py
index a85fa9e..82b924b 100644
--- a/Allura/allura/lib/multifactor.py
+++ b/Allura/allura/lib/multifactor.py
@@ -43,9 +43,6 @@ from ming.odm import session
 from allura.model.multifactor import RecoveryCode
 from allura.lib.utils import umask
 import six
-from io import open
-from six.moves import range
-from six.moves import map
 
 
 log = logging.getLogger(__name__)
@@ -69,7 +66,7 @@ def check_rate_limit(num_allowed, time_allowed, attempts):
     return ok, attempts_in_limit
 
 
-class TotpService(object):
+class TotpService:
     '''
     An interface for handling multifactor auth TOTP secret keys.  Common functionality
     is provided in this base class, and specific subclasses implement different storage options.
@@ -163,7 +160,7 @@ class TotpService(object):
         raise NotImplementedError('enforce_rate_limit')
 
 
-class MongodbMultifactorCommon(object):
+class MongodbMultifactorCommon:
 
     def enforce_rate_limit(self, user):
         prev_attempts = user.get_tool_data('allura', 'multifactor_attempts') or []
@@ -201,7 +198,7 @@ class MongodbTotpService(MongodbMultifactorCommon, TotpService):
                                upsert=True)
 
 
-class GoogleAuthenticatorFile(object):
+class GoogleAuthenticatorFile:
     '''
     Parse & write server-side .google_authenticator files for PAM.
     https://github.com/google/google-authenticator/blob/master/libpam/FILEFORMAT
@@ -235,7 +232,7 @@ class GoogleAuthenticatorFile(object):
     def dump(self):
         lines = []
         lines.append(six.ensure_text(b32encode(self.key)).replace('=', ''))
-        for opt, value in six.iteritems(self.options):
+        for opt, value in self.options.items():
             parts = ['"', opt]
             if value is not None:
                 parts.append(value)
@@ -245,7 +242,7 @@ class GoogleAuthenticatorFile(object):
         return '\n'.join(lines)
 
 
-class GoogleAuthenticatorPamFilesystemMixin(object):
+class GoogleAuthenticatorPamFilesystemMixin:
 
     @property
     def basedir(self):
@@ -266,7 +263,7 @@ class GoogleAuthenticatorPamFilesystemMixin(object):
         try:
             with open(self.config_file(user)) as f:
                 return GoogleAuthenticatorFile.load(f.read())
-        except IOError as e:
+        except OSError as e:
             if e.errno == errno.ENOENT:  # file doesn't exist
                 if autocreate:
                     gaf = GoogleAuthenticatorFile()
@@ -339,7 +336,7 @@ class GoogleAuthenticatorPamFilesystemTotpService(GoogleAuthenticatorPamFilesyst
             self.write_file(user, gaf)
 
 
-class RecoveryCodeService(object):
+class RecoveryCodeService:
     '''
     An interface for handling multifactor recovery codes.  Common functionality
     is provided in this base class, and specific subclasses implement different storage options.
@@ -447,7 +444,7 @@ class GoogleAuthenticatorPamFilesystemRecoveryCodeService(GoogleAuthenticatorPam
             gaf.recovery_codes = codes
             self.write_file(user, gaf)
         elif codes:
-            raise IOError('No .google-authenticator file exists, cannot add recovery codes.')
+            raise OSError('No .google-authenticator file exists, cannot add recovery codes.')
 
     def verify_and_remove_code(self, user, code):
         gaf = self.read_file(user)
diff --git a/Allura/allura/lib/patches.py b/Allura/allura/lib/patches.py
index a304a07..876a3c0 100644
--- a/Allura/allura/lib/patches.py
+++ b/Allura/allura/lib/patches.py
@@ -59,7 +59,7 @@ def apply():
         else:
             return
 
-        for content_type, content_engine in six.iteritems(engines):
+        for content_type, content_engine in engines.items():
             template = template.split(':', 1)
             template.extend(content_engine[2:])
             try:
diff --git a/Allura/allura/lib/phone/__init__.py b/Allura/allura/lib/phone/__init__.py
index dd46f2f..fc12189 100644
--- a/Allura/allura/lib/phone/__init__.py
+++ b/Allura/allura/lib/phone/__init__.py
@@ -20,7 +20,7 @@ import logging
 log = logging.getLogger(__name__)
 
 
-class PhoneService(object):
+class PhoneService:
     """
     Defines the phone verification service interface and provides a default
     no-op implementation.
diff --git a/Allura/allura/lib/plugin.py b/Allura/allura/lib/plugin.py
index b3dcb15..fb379b2 100644
--- a/Allura/allura/lib/plugin.py
+++ b/Allura/allura/lib/plugin.py
@@ -34,7 +34,6 @@ from base64 import b64encode
 from datetime import datetime, timedelta
 import calendar
 import six
-from six.moves import range
 
 try:
     import ldap
@@ -63,7 +62,7 @@ from allura.tasks.index_tasks import solr_del_project_artifacts
 log = logging.getLogger(__name__)
 
 
-class AuthenticationProvider(object):
+class AuthenticationProvider:
 
     '''
     An interface to provide authentication services for Allura.
@@ -364,7 +363,7 @@ class AuthenticationProvider(object):
         :rtype: str
         '''
         # default implementation for any providers that haven't implemented this newer method yet
-        return '/{}/'.format(self.user_project_shortname(user))
+        return f'/{self.user_project_shortname(user)}/'
 
     def user_by_project_shortname(self, shortname, include_disabled=False):
         '''
@@ -596,7 +595,7 @@ class LocalAuthenticationProvider(AuthenticationProvider):
         # in contrast with above user_project_shortname()
         # we allow the URL of a user-project to match the username exactly, even if user-project's name is different
         # (nbhd_lookup_first_path will figure it out)
-        return '/u/{}/'.format(user.username)
+        return f'/u/{user.username}/'
 
     def user_by_project_shortname(self, shortname, include_disabled=False):
         from allura import model as M
@@ -728,7 +727,7 @@ class LdapAuthenticationProvider(AuthenticationProvider):
         salt = self._get_salt(salt_len) if salt is None else salt
         encrypted = crypt.crypt(
             six.ensure_str(password),
-            '${}$rounds={}${}'.format(algorithm, rounds, salt))
+            f'${algorithm}$rounds={rounds}${salt}')
         return b'{CRYPT}%s' % encrypted.encode('utf-8')
 
     def by_username(self, username):
@@ -774,10 +773,10 @@ class LdapAuthenticationProvider(AuthenticationProvider):
                                                                                                 'display_name'),
                                         })
             else:
-                log.debug('LdapAuth: no user {} found in local mongo'.format(username))
+                log.debug(f'LdapAuth: no user {username} found in local mongo')
                 raise exc.HTTPUnauthorized()
         elif user.disabled or user.pending:
-            log.debug('LdapAuth: user {} is disabled or pending in Allura'.format(username))
+            log.debug(f'LdapAuth: user {username} is disabled or pending in Allura')
             raise exc.HTTPUnauthorized()
         return user
 
@@ -797,7 +796,7 @@ class LdapAuthenticationProvider(AuthenticationProvider):
             con.unbind_s()
             return True
         except (ldap.INVALID_CREDENTIALS, ldap.UNWILLING_TO_PERFORM, ldap.NO_SUCH_OBJECT):
-            log.debug('LdapAuth: could not authenticate {}'.format(username), exc_info=True)
+            log.debug(f'LdapAuth: could not authenticate {username}', exc_info=True)
         return False
 
     def user_project_shortname(self, user):
@@ -832,7 +831,7 @@ class LdapAuthenticationProvider(AuthenticationProvider):
         return super().recover_password(user)
 
 
-class ProjectRegistrationProvider(object):
+class ProjectRegistrationProvider:
     '''
     Project registration services for Allura.  This is a full implementation
     and the default.  Extend this class with your own if you need to add more
@@ -934,10 +933,10 @@ class ProjectRegistrationProvider(object):
         res = g.phone_service.check(request_id, pin)
         if res.get('status') == 'ok':
             user.set_tool_data('phone_verification', number_hash=number_hash)
-            msg = 'Phone verification succeeded. Hash: {}'.format(number_hash)
+            msg = f'Phone verification succeeded. Hash: {number_hash}'
             h.auditlog_user(msg, user=user)
         else:
-            msg = 'Phone verification failed. Hash: {}'.format(number_hash)
+            msg = f'Phone verification failed. Hash: {number_hash}'
             h.auditlog_user(msg, user=user)
         return res
 
@@ -1019,7 +1018,7 @@ class ProjectRegistrationProvider(object):
         p = M.Project.query.get(shortname=shortname, neighborhood_id=neighborhood._id)
         if p:
             raise forge_exc.ProjectConflict(
-                '{} already exists in nbhd {}'.format(shortname, neighborhood._id))
+                f'{shortname} already exists in nbhd {neighborhood._id}')
 
     def index_project(self, project):
         """
@@ -1084,8 +1083,8 @@ class ProjectRegistrationProvider(object):
             for i, tool in enumerate(project_template['tools'].keys()):
                 tool_config = project_template['tools'][tool]
                 tool_options = tool_config.get('options', {})
-                for k, v in six.iteritems(tool_options):
-                    if isinstance(v, six.string_types):
+                for k, v in tool_options.items():
+                    if isinstance(v, str):
                         tool_options[k] = \
                             string.Template(v).safe_substitute(
                                 p.__dict__.get('root_project', {}))
@@ -1259,8 +1258,8 @@ class ProjectRegistrationProvider(object):
                 return None, 'Project not found'
             if cnt == 1:
                 return q.first(), None
-            return None, 'Too many matches for project: {}'.format(cnt)
-        n = Neighborhood.query.get(url_prefix='/{}/'.format(url[0]))
+            return None, f'Too many matches for project: {cnt}'
+        n = Neighborhood.query.get(url_prefix=f'/{url[0]}/')
         if not n:
             return None, 'Neighborhood not found'
         p = Project.query.get(neighborhood_id=n._id, shortname=n.shortname_prefix + url[1])
@@ -1272,7 +1271,7 @@ class ProjectRegistrationProvider(object):
         return (p, 'Project not found' if p is None else None)
 
 
-class ThemeProvider(object):
+class ThemeProvider:
 
     '''
     Theme information for Allura.  This is a full implementation
@@ -1328,7 +1327,7 @@ class ThemeProvider(object):
         '''
         if theme_name is None:
             theme_name = config.get('theme', 'allura')
-        return g.resource_manager.absurl('theme/{}/{}'.format(theme_name, href))
+        return g.resource_manager.absurl(f'theme/{theme_name}/{href}')
 
     @LazyProperty
     def personal_data_form(self):
@@ -1491,7 +1490,7 @@ class ThemeProvider(object):
             Takes an instance of class Application, or else a string.
             Expected to be overriden by derived Themes.
         """
-        if isinstance(app, six.text_type):
+        if isinstance(app, str):
             app = str(app)
         if isinstance(app, str):
             if app in self.icons and size in self.icons[app]:
@@ -1554,8 +1553,8 @@ class ThemeProvider(object):
 
         if note_to_show:
             cookie_chunks = []
-            for note_id, views_closed in six.iteritems(cookie_info):
-                cookie_chunks.append('{}-{}-{}'.format(note_id, views_closed[0], views_closed[1]))
+            for note_id, views_closed in cookie_info.items():
+                cookie_chunks.append(f'{note_id}-{views_closed[0]}-{views_closed[1]}')
             set_cookie_value = '_'.join(sorted(cookie_chunks))
             return note_to_show, set_cookie_value
 
@@ -1598,7 +1597,7 @@ class LocalProjectRegistrationProvider(ProjectRegistrationProvider):
     pass
 
 
-class UserPreferencesProvider(object):
+class UserPreferencesProvider:
 
     '''
     An interface for user preferences, like display_name and email_address
@@ -1746,7 +1745,7 @@ class LdapUserPreferencesProvider(UserPreferencesProvider):
         else:
             con.unbind_s()
         if not rs:
-            log.warning('LdapUserPref: No user record found for: {}'.format(username))
+            log.warning(f'LdapUserPref: No user record found for: {username}')
             return ''
         user_dn, user_attrs = rs[0]
         ldap_attr = self.fields[pref_name]
@@ -1773,7 +1772,7 @@ class LdapUserPreferencesProvider(UserPreferencesProvider):
             return LocalUserPreferencesProvider().set_pref(user, pref_name, pref_value)
 
 
-class AdminExtension(object):
+class AdminExtension:
 
     """
     A base class for extending the admin areas in Allura.
@@ -1806,7 +1805,7 @@ class AdminExtension(object):
         pass
 
 
-class SiteAdminExtension(object):
+class SiteAdminExtension:
     """
     A base class for extending the site admin area in Allura.
 
@@ -1836,7 +1835,7 @@ class SiteAdminExtension(object):
         pass
 
 
-class ImportIdConverter(object):
+class ImportIdConverter:
 
     '''
     An interface to convert to and from import_id values for indexing,
diff --git a/Allura/allura/lib/project_create_helpers.py b/Allura/allura/lib/project_create_helpers.py
index fdee7c5..1141ae6 100644
--- a/Allura/allura/lib/project_create_helpers.py
+++ b/Allura/allura/lib/project_create_helpers.py
@@ -31,7 +31,6 @@ import requests
 import formencode
 import six
 from six.moves.urllib.parse import urlparse
-from six.moves import range
 
 from allura.lib.helpers import slugify
 from allura.model import Neighborhood
@@ -269,13 +268,13 @@ def create_project_with_attrs(p, nbhd, update=False, ensure_tools=False):
     project.notifications_disabled = True
 
     if ensure_tools and 'tools' in project_template:
-        for i, tool in enumerate(six.iterkeys(project_template['tools'])):
+        for i, tool in enumerate(project_template['tools'].keys()):
             tool_config = project_template['tools'][tool]
             if project.app_instance(tool_config['mount_point']):
                 continue
             tool_options = tool_config.get('options', {})
-            for k, v in six.iteritems(tool_options):
-                if isinstance(v, six.string_types):
+            for k, v in tool_options.items():
+                if isinstance(v, str):
                     tool_options[k] = string.Template(v).safe_substitute(
                         project.root_project.__dict__.get('root_project', {}))
             project.install_app(tool,
diff --git a/Allura/allura/lib/repository.py b/Allura/allura/lib/repository.py
index 30a1817..36eb3d7 100644
--- a/Allura/allura/lib/repository.py
+++ b/Allura/allura/lib/repository.py
@@ -118,7 +118,7 @@ class RepositoryApp(Application):
                 self.config.options.mount_point +
                 '/refresh'),
         ]
-        links += super(RepositoryApp, self).admin_menu()
+        links += super().admin_menu()
         [links.remove(l) for l in links[:] if l.label == 'Options']
         return links
 
@@ -223,7 +223,7 @@ class RepositoryApp(Application):
 
     def install(self, project):
         self.config.options['project_name'] = project.name
-        super(RepositoryApp, self).install(project)
+        super().install(project)
         role_admin = M.ProjectRole.by_name('Admin')._id
         role_developer = M.ProjectRole.by_name('Developer')._id
         role_auth = M.ProjectRole.authenticated()._id
@@ -285,7 +285,7 @@ class RepoAdminController(DefaultAdminController):
     @expose('jinja:allura:templates/repo/checkout_url.html')
     def checkout_url(self):
         return dict(app=self.app,
-                    merge_allowed=not asbool(config.get('scm.merge.{}.disabled'.format(self.app.config.tool_name))),
+                    merge_allowed=not asbool(config.get(f'scm.merge.{self.app.config.tool_name}.disabled')),
                     )
 
     @without_trailing_slash
diff --git a/Allura/allura/lib/search.py b/Allura/allura/lib/search.py
index d936f00..7c3346f 100644
--- a/Allura/allura/lib/search.py
+++ b/Allura/allura/lib/search.py
@@ -34,12 +34,11 @@ from allura.lib import helpers as h
 from allura.lib.solr import escape_solr_arg
 from allura.lib.utils import urlencode
 import six
-from six.moves import map
 
 log = getLogger(__name__)
 
 
-class SearchIndexable(object):
+class SearchIndexable:
 
     """
     Base class for anything you want to search on.
@@ -156,10 +155,10 @@ def search(q, short_timeout=False, ignore_errors=True, **kw):
             return g.solr_short_timeout.search(q, **kw)
         else:
             return g.solr.search(q, **kw)
-    except (SolrError, socket.error) as e:
+    except (SolrError, OSError) as e:
         log.exception('Error in solr search')
         if not ignore_errors:
-            match = re.search(r'<pre>(.*)</pre>', six.text_type(e))
+            match = re.search(r'<pre>(.*)</pre>', str(e))
             raise SearchError('Error running search query: %s' %
                               (match.group(1) if match else e))
 
@@ -186,17 +185,17 @@ def search_artifact(atype, q, history=False, rows=10, short_timeout=False, filte
         fq.append('project_id_s:%s' % c.project._id)
 
     fq += kw.pop('fq', [])
-    if isinstance(filter, six.string_types):  # may be stringified after a ticket filter, then bulk edit
+    if isinstance(filter, str):  # may be stringified after a ticket filter, then bulk edit
         filter = ast.literal_eval(filter)
-    for name, values in six.iteritems(filter or {}):
+    for name, values in (filter or {}).items():
         field_name = name + '_s'
         parts = []
         for v in values:
             # Specific solr syntax for empty fields
             if v == '' or v is None:
-                part = '(-{}:[* TO *] AND *:*)'.format(field_name)
+                part = f'(-{field_name}:[* TO *] AND *:*)'
             else:
-                part = '{}:{}'.format(field_name, escape_solr_arg(v))
+                part = f'{field_name}:{escape_solr_arg(v)}'
             parts.append(part)
         fq.append(' OR '.join(parts))
     if not history:
@@ -223,7 +222,7 @@ def site_admin_search(model, q, field, **kw):
         # escaping spaces with '\ ' isn't sufficient for display_name_t since its stored as text_general (why??)
         # and wouldn't handle foo@bar.com split on @ either
         # This should work, but doesn't for unknown reasons: q = u'{!term f=%s}%s' % (field, q)
-        q = obj.translate_query('{}:({})'.format(field, q), fields)
+        q = obj.translate_query(f'{field}:({q})', fields)
         kw['q.op'] = 'AND'  # so that all terms within the () are required
     fq = ['type_s:%s' % model.type_s]
     return search(q, fq=fq, ignore_errors=False, **kw)
diff --git a/Allura/allura/lib/security.py b/Allura/allura/lib/security.py
index 84ad562..cc87482 100644
--- a/Allura/allura/lib/security.py
+++ b/Allura/allura/lib/security.py
@@ -38,7 +38,7 @@ from allura.lib.utils import TruthyCallable
 log = logging.getLogger(__name__)
 
 
-class Credentials(object):
+class Credentials:
 
     '''
     Role graph logic & caching
@@ -104,7 +104,7 @@ class Credentials(object):
         roles_by_project = {pid: [] for pid in project_ids}
         for role in q:
             roles_by_project[role['project_id']].append(role)
-        for pid, roles in six.iteritems(roles_by_project):
+        for pid, roles in roles_by_project.items():
             self.users[user_id, pid] = RoleCache(self, roles)
 
     def load_project_roles(self, *project_ids):
@@ -119,7 +119,7 @@ class Credentials(object):
         roles_by_project = {pid: [] for pid in project_ids}
         for role in q:
             roles_by_project[role['project_id']].append(role)
-        for pid, roles in six.iteritems(roles_by_project):
+        for pid, roles in roles_by_project.items():
             self.projects[pid] = RoleCache(self, roles)
 
     def project_roles(self, project_id):
@@ -164,7 +164,7 @@ class Credentials(object):
         return role.userids_that_reach
 
 
-class RoleCache(object):
+class RoleCache:
     '''
     An iterable collection of :class:`ProjectRoles <allura.model.auth.ProjectRole>` that is cached after first use
     '''
@@ -199,7 +199,7 @@ class RoleCache(object):
         return None
 
     def __iter__(self):
-        return six.itervalues(self.index)
+        return iter(self.index.values())
 
     def __len__(self):
         return len(self.index)
@@ -553,7 +553,7 @@ class HIBPCompromisedCredentials(Exception):
         self.partial_hash = partial_hash
 
 
-class HIBPClient(object):
+class HIBPClient:
 
     @classmethod
     def check_breached_password(cls, password):
@@ -572,7 +572,7 @@ class HIBPClient(object):
 
             # hit HIBP API
             headers = {'User-Agent': '{}-pwnage-checker'.format(tg.config.get('site_name', 'Allura'))}
-            resp = requests.get('https://api.pwnedpasswords.com/range/{}'.format(sha_1_first_5), timeout=1,
+            resp = requests.get(f'https://api.pwnedpasswords.com/range/{sha_1_first_5}', timeout=1,
                                 headers=headers)
             # check results
             result = cls.scan_response(resp, sha_1)
diff --git a/Allura/allura/lib/solr.py b/Allura/allura/lib/solr.py
index 76f1a6c..69445c9 100644
--- a/Allura/allura/lib/solr.py
+++ b/Allura/allura/lib/solr.py
@@ -53,7 +53,7 @@ def escape_solr_arg(term):
     """ Apply escaping to the passed in query terms
         escaping special characters like : , etc"""
     term = term.replace('\\', r'\\')   # escape \ first
-    for char, escaped_char in six.iteritems(escape_rules):
+    for char, escaped_char in escape_rules.items():
         term = term.replace(char, escaped_char)
 
     return term
@@ -73,7 +73,7 @@ def make_solr_from_config(push_servers, query_server=None, **kwargs):
     return Solr(push_servers, query_server, **solr_kwargs)
 
 
-class Solr(object):
+class Solr:
 
     """Solr interface that pushes updates to multiple solr instances.
 
@@ -123,7 +123,7 @@ class Solr(object):
         return self.query_server.search(*args, **kw)
 
 
-class MockSOLR(object):
+class MockSOLR:
 
     class MockHits(list):
 
@@ -163,7 +163,7 @@ class MockSOLR(object):
             if part in ('&&', 'AND'):
                 continue
             if part in ('||', 'OR'):
-                log.warn("MockSOLR doesn't implement OR yet; treating as AND. q={} fq={}".format(q, fq))
+                log.warn(f"MockSOLR doesn't implement OR yet; treating as AND. q={q} fq={fq}")
                 continue
             if ':' in part:
                 field, value = part.split(':', 1)
diff --git a/Allura/allura/lib/spam/__init__.py b/Allura/allura/lib/spam/__init__.py
index 28065b8..316fb0f 100644
--- a/Allura/allura/lib/spam/__init__.py
+++ b/Allura/allura/lib/spam/__init__.py
@@ -26,7 +26,7 @@ from allura.model.artifact import SpamCheckResult
 log = logging.getLogger(__name__)
 
 
-class SpamFilter(object):
+class SpamFilter:
 
     """Defines the spam checker interface and provides a default no-op impl."""
 
diff --git a/Allura/allura/lib/spam/stopforumspamfilter.py b/Allura/allura/lib/spam/stopforumspamfilter.py
index f84fc4d..80d234c 100644
--- a/Allura/allura/lib/spam/stopforumspamfilter.py
+++ b/Allura/allura/lib/spam/stopforumspamfilter.py
@@ -25,8 +25,6 @@ from tg import request
 from allura.lib import utils
 from allura.lib.spam import SpamFilter
 import six
-from io import open
-from six.moves import range
 
 log = logging.getLogger(__name__)
 
@@ -54,7 +52,7 @@ class StopForumSpamSpamFilter(SpamFilter):
                 if record[1] not in threshold_strs:
                     ip = record[0]
                     # int is the smallest memory representation of an IP addr
-                    ip_int = int(ipaddress.ip_address(six.text_type(ip)))
+                    ip_int = int(ipaddress.ip_address(str(ip)))
                     self.packed_ips.add(ip_int)
         # to get actual memory usage, use: from pympler.asizeof import asizeof
         log.info('Read stopforumspam file; %s recs, probably %s bytes stored in memory', len(self.packed_ips),
@@ -63,7 +61,7 @@ class StopForumSpamSpamFilter(SpamFilter):
     def check(self, text, artifact=None, user=None, content_type='comment', **kw):
         ip = utils.ip_address(request)
         if ip:
-            ip_int = int(ipaddress.ip_address(six.text_type(ip)))
+            ip_int = int(ipaddress.ip_address(str(ip)))
             res = ip_int in self.packed_ips
             self.record_result(res, artifact, user)
         else:
diff --git a/Allura/allura/lib/utils.py b/Allura/allura/lib/utils.py
index 9391e82..e6ddc51 100644
--- a/Allura/allura/lib/utils.py
+++ b/Allura/allura/lib/utils.py
@@ -61,9 +61,6 @@ from ming.utils import LazyProperty
 from ming.odm.odmsession import ODMCursor
 from ming.odm import session
 import six
-from six.moves import range
-from six.moves import zip
-from six.moves import map
 
 MARKDOWN_EXTENSIONS = ['.markdown', '.mdown', '.mkdn', '.mkd', '.md']
 
@@ -103,7 +100,7 @@ def guess_mime_type(filename):
     return content_type
 
 
-class ConfigProxy(object):
+class ConfigProxy:
 
     '''Wrapper for loading config values at module-scope so we don't
     have problems when a module is imported before tg.config is initialized
@@ -122,7 +119,7 @@ class ConfigProxy(object):
         return asbool(self.get(key))
 
 
-class lazy_logger(object):
+class lazy_logger:
 
     '''Lazy instatiation of a logger, to ensure that it does not get
     created before logging is configured (which would make it disabled)'''
@@ -154,8 +151,8 @@ class CustomWatchedFileHandler(logging.handlers.WatchedFileHandler):
         """
         title = getproctitle()
         if title.startswith('taskd:'):
-            record.name = "{}:{}".format(title, record.name)
-        return super(CustomWatchedFileHandler, self).format(record)
+            record.name = f"{title}:{record.name}"
+        return super().format(record)
 
 
 def chunked_find(cls, query=None, pagesize=1024, sort_key='_id', sort_dir=1):
@@ -204,7 +201,7 @@ def chunked_iter(iterable, max_size):
         yield (x for i, x in chunk)
 
 
-class AntiSpam(object):
+class AntiSpam:
 
     '''Helper class for bot-protecting forms'''
     honey_field_template = string.Template('''<p class="$honey_class">
@@ -280,7 +277,7 @@ class AntiSpam(object):
         plain = ([len(plain)]
                  + list(map(ord, plain))
                  + self.random_padding[:len(self.spinner_ord) - len(plain) - 1])
-        enc = ''.join(six.unichr(p ^ s) for p, s in zip(plain, self.spinner_ord))
+        enc = ''.join(chr(p ^ s) for p, s in zip(plain, self.spinner_ord))
         enc = six.ensure_binary(enc)
         enc = self._wrap(enc)
         enc = six.ensure_text(enc)
@@ -294,7 +291,7 @@ class AntiSpam(object):
         enc = list(map(ord, enc))
         plain = [e ^ s for e, s in zip(enc, self.spinner_ord)]
         plain = plain[1:1 + plain[0]]
-        plain = ''.join(map(six.unichr, plain))
+        plain = ''.join(map(chr, plain))
         return plain
 
     def extra_fields(self):
@@ -363,7 +360,7 @@ class AntiSpam(object):
                         raise ValueError('Value in honeypot field: %s' % value)
             except Exception as ex:
                 attrs = dict(now=now, obj=vars(obj) if obj else None)
-                log.info('Form validation failure: {}'.format(attrs))
+                log.info(f'Form validation failure: {attrs}')
                 log.info('Error is', exc_info=ex)
                 raise
         return new_params
@@ -392,7 +389,7 @@ class AntiSpam(object):
         return before_validate(antispam_hook)
 
 
-class TruthyCallable(object):
+class TruthyCallable:
 
     '''
     Wraps a callable to make it truthy in a boolean context.
@@ -472,7 +469,7 @@ class LineAnchorCodeHtmlFormatter(HtmlFormatter):
         num = self.linenostart
         yield 0, ('<pre' + (style and ' style="%s"' % style) + '>')
         for tup in inner:
-            yield (tup[0], '<div id="l{}" class="code_block">{}</div>'.format(num, tup[1]))
+            yield (tup[0], f'<div id="l{num}" class="code_block">{tup[1]}</div>')
             num += 1
         yield 0, '</pre>'
 
@@ -510,10 +507,10 @@ def serve_file(fp, filename, content_type, last_modified=None,
                cache_expires=None, size=None, embed=True, etag=None):
     '''Sets the response headers and serves as a wsgi iter'''
     if not etag and filename and last_modified:
-        etag = '{}?{}'.format(filename, last_modified).encode('utf-8')
+        etag = f'{filename}?{last_modified}'.encode()
     if etag:
         etag_cache(etag)
-    tg.response.headers['Content-Type'] = str('')
+    tg.response.headers['Content-Type'] = ''
     tg.response.content_type = str(content_type)
     tg.response.cache_expires = cache_expires or asint(
         tg.config.get('files_expires_header_secs', 60 * 60))
@@ -527,7 +524,7 @@ def serve_file(fp, filename, content_type, last_modified=None,
     if not embed:
         from allura.lib import helpers as h
         tg.response.headers.add(
-            str('Content-Disposition'),
+            'Content-Disposition',
             str('attachment;filename="%s"' % h.urlquote(filename)))
     # http://code.google.com/p/modwsgi/wiki/FileWrapperExtension
     block_size = 4096
@@ -540,7 +537,7 @@ def serve_file(fp, filename, content_type, last_modified=None,
 class ForgeHTMLSanitizerFilter(html5lib.filters.sanitizer.Filter):
 
     def __init__(self, *args, **kwargs):
-        super(ForgeHTMLSanitizerFilter, self).__init__(*args, **kwargs)
+        super().__init__(*args, **kwargs)
         # remove some elements from the sanitizer whitelist
         # <form> and <input> could be used for a social engineering attack to construct a form
         # others are just unexpected and confusing, and have no need to be used in markdown
@@ -603,7 +600,7 @@ class ForgeHTMLSanitizerFilter(html5lib.filters.sanitizer.Filter):
             if attrs.get((None, 'type'), '') == "checkbox":
                 self.allowed_elements.add(input_el)
 
-        return super(ForgeHTMLSanitizerFilter, self).sanitize_token(token)
+        return super().sanitize_token(token)
 
 
 def ip_address(request):
@@ -731,7 +728,7 @@ class JSONForExport(tg.jsonify.JSONEncoder):
                 return obj.__json__(is_export=True)
             except TypeError:
                 return obj.__json__()
-        return super(JSONForExport, self).default(obj)
+        return super().default(obj)
 
 
 @contextmanager
@@ -790,15 +787,15 @@ def smart_str(s):
     '''
     Returns a bytestring version of 's' from any type
     '''
-    if isinstance(s, six.binary_type):
+    if isinstance(s, bytes):
         return s
     else:
-        return six.text_type(s).encode('utf-8')
+        return str(s).encode('utf-8')
 
 
 def generate_smart_str(params):
     if isinstance(params, collections.Mapping):
-        params_list = six.iteritems(params)
+        params_list = params.items()
     else:
         params_list = params
     for key, value in params_list:
diff --git a/Allura/allura/lib/validators.py b/Allura/allura/lib/validators.py
index cffd402..a697148 100644
--- a/Allura/allura/lib/validators.py
+++ b/Allura/allura/lib/validators.py
@@ -80,14 +80,14 @@ class UnicodeString(fev.UnicodeString):
 
 
 # make UnicodeString fix above work through this String alias, just like formencode aliases String
-String = UnicodeString if str is six.text_type else fev.ByteString
+String = UnicodeString if str is str else fev.ByteString
 
 
 class Ming(fev.FancyValidator):
 
     def __init__(self, cls, **kw):
         self.cls = cls
-        super(Ming, self).__init__(**kw)
+        super().__init__(**kw)
 
     def _to_python(self, value, state):
         result = self.cls.query.get(_id=value)
@@ -315,8 +315,8 @@ class UserMapJsonFile(JsonFile):
     def _to_python(self, value, state):
         value = super(self.__class__, self)._to_python(value, state)
         try:
-            for k, v in six.iteritems(value):
-                if not(isinstance(k, six.string_types) and isinstance(v, six.string_types)):
+            for k, v in value.items():
+                if not(isinstance(k, str) and isinstance(v, str)):
                     raise
             return json.dumps(value) if self.as_string else value
         except Exception:
@@ -368,7 +368,7 @@ class OneOfValidator(fev.FancyValidator):
     def __init__(self, validvalues, not_empty=True):
         self.validvalues = validvalues
         self.not_empty = not_empty
-        super(OneOfValidator, self).__init__()
+        super().__init__()
 
     def _to_python(self, value, state):
         if not value.strip():
@@ -393,7 +393,7 @@ class MapValidator(fev.FancyValidator):
     def __init__(self, mapvalues, not_empty=True):
         self.map = mapvalues
         self.not_empty = not_empty
-        super(MapValidator, self).__init__()
+        super().__init__()
 
     def _to_python(self, value, state):
         if not value.strip():
@@ -424,7 +424,7 @@ class YouTubeConverter(fev.FancyValidator):
         match = re.match(YouTubeConverter.REGEX, value)
         if match:
             video_id = match.group(1)
-            return 'www.youtube.com/embed/{}?rel=0'.format(video_id)
+            return f'www.youtube.com/embed/{video_id}?rel=0'
         else:
             raise fe.Invalid(
                 "The URL does not appear to be a valid YouTube video.",
diff --git a/Allura/allura/lib/widgets/auth_widgets.py b/Allura/allura/lib/widgets/auth_widgets.py
index 4ebbf70..52a849f 100644
--- a/Allura/allura/lib/widgets/auth_widgets.py
+++ b/Allura/allura/lib/widgets/auth_widgets.py
@@ -70,7 +70,7 @@ class LoginForm(ForgeForm):
 
     @validator
     def validate(self, value, state=None):
-        super(LoginForm, self).validate(value, state=state)
+        super().validate(value, state=state)
         auth_provider = plugin.AuthenticationProvider.get(request)
 
         # can't use a validator attr on the username TextField, since the antispam encoded name changes and doesn't
diff --git a/Allura/allura/lib/widgets/discuss.py b/Allura/allura/lib/widgets/discuss.py
index b5daa0b..3bbdd88 100644
--- a/Allura/allura/lib/widgets/discuss.py
+++ b/Allura/allura/lib/widgets/discuss.py
@@ -79,8 +79,7 @@ class ModeratePosts(ew.SimpleForm):
         submit_text=None)
 
     def resources(self):
-        for r in super(ModeratePosts, self).resources():
-            yield r
+        yield from super().resources()
         yield ew.JSScript('''
       (function($){
           var tbl = $('form table');
@@ -129,7 +128,7 @@ class TagPost(ff.ForgeForm):
 
     # this ickiness is to override the default submit button
     def __call__(self, **kw):
-        result = super(TagPost, self).__call__(**kw)
+        result = super().__call__(**kw)
         submit_button = ffw.SubmitButton(label=result['submit_text'])
         result['extra_fields'] = [submit_button]
         result['buttons'] = [submit_button]
@@ -138,8 +137,7 @@ class TagPost(ff.ForgeForm):
     fields = [ffw.LabelEdit(label='Labels', name='labels', className='title')]
 
     def resources(self):
-        for r in ffw.LabelEdit(name='labels').resources():
-            yield r
+        yield from ffw.LabelEdit(name='labels').resources()
 
 
 class EditPost(ff.ForgeForm):
@@ -170,10 +168,8 @@ class EditPost(ff.ForgeForm):
         return fields
 
     def resources(self):
-        for r in ew.TextField(name='subject').resources():
-            yield r
-        for r in ffw.MarkdownEdit(name='text').resources():
-            yield r
+        yield from ew.TextField(name='subject').resources()
+        yield from ffw.MarkdownEdit(name='text').resources()
         yield ew.JSScript('''$(document).ready(function () {
             $("a.attachment_form_add_button").click(function(evt){
                 $(this).hide();
@@ -201,7 +197,7 @@ class NewTopicPost(EditPost):
 
     @property
     def fields(self):
-        fields = super(NewTopicPost, self).fields
+        fields = super().fields
         fields.append(ew.InputField(name='attachment', label='Attachment', field_type='file',
                                     attrs={'multiple': 'True'},
                                     validator=fev.FieldStorageUploadConverter(if_missing=None)))
@@ -243,8 +239,7 @@ class SubscriptionForm(ew.SimpleForm):
         _threads = _ThreadsTable()
 
     def resources(self):
-        for r in super(SubscriptionForm, self).resources():
-            yield r
+        yield from super().resources()
         yield ew.JSScript('''
         $(window).load(function () {
             $('tbody').children(':even').addClass('even');
@@ -257,16 +252,15 @@ class HierWidget(ew_core.Widget):
     widgets = {}
 
     def prepare_context(self, context):
-        response = super(HierWidget, self).prepare_context(context)
+        response = super().prepare_context(context)
         response['widgets'] = self.widgets
         for w in self.widgets.values():
             w.parent_widget = self
         return response
 
     def resources(self):
-        for w in six.itervalues(self.widgets):
-            for r in w.resources():
-                yield r
+        for w in self.widgets.values():
+            yield from w.resources()
 
 
 class Attachment(ew_core.Widget):
@@ -309,11 +303,9 @@ class Post(HierWidget):
         attachment=Attachment())
 
     def resources(self):
-        for r in super(Post, self).resources():
-            yield r
-        for w in six.itervalues(self.widgets):
-            for r in w.resources():
-                yield r
+        yield from super().resources()
+        for w in self.widgets.values():
+            yield from w.resources()
         yield ew.CSSScript('''
         div.moderate {
             color:grey;
@@ -355,11 +347,9 @@ class Thread(HierWidget):
         edit_post=EditPost(submit_text='Submit'))
 
     def resources(self):
-        for r in super(Thread, self).resources():
-            yield r
-        for w in six.itervalues(self.widgets):
-            for r in w.resources():
-                yield r
+        yield from super().resources()
+        for w in self.widgets.values():
+            yield from w.resources()
         yield ew.JSScript('''
         $(document).ready(function () {
             var thread_tag = $('a.thread_tag');
diff --git a/Allura/allura/lib/widgets/form_fields.py b/Allura/allura/lib/widgets/form_fields.py
index 3560ad7..7d51cb4 100644
--- a/Allura/allura/lib/widgets/form_fields.py
+++ b/Allura/allura/lib/widgets/form_fields.py
@@ -29,7 +29,6 @@ import paginate
 import ew as ew_core
 import ew.jinja2_ew as ew
 import six
-from six.moves import range
 
 from allura.lib import validators as v
 
@@ -45,15 +44,15 @@ class LabelList(v.UnicodeString):
 
     def __init__(self, *args, **kwargs):
         kwargs.setdefault('if_empty', [])
-        super(LabelList, self).__init__(*args, **kwargs)
+        super().__init__(*args, **kwargs)
 
     def _to_python(self, value, state):
-        value = super(LabelList, self)._to_python(value, state)
+        value = super()._to_python(value, state)
         return value.split(',')
 
     def _from_python(self, value, state):
         value = ','.join(value)
-        value = super(LabelList, self)._from_python(value, state)
+        value = super()._from_python(value, state)
         return value
 
 
@@ -69,7 +68,7 @@ class LabelEdit(ew.InputField):
         placeholder=None)
 
     def from_python(self, value, state=None):
-        if isinstance(value, six.string_types):
+        if isinstance(value, str):
             return value
         elif value is None:
             return ''
@@ -115,7 +114,7 @@ class ProjectUserSelect(ew.InputField):
         className=None)
 
     def __init__(self, **kw):
-        super(ProjectUserSelect, self).__init__(**kw)
+        super().__init__(**kw)
         if not isinstance(self.value, list):
             self.value = [self.value]
 
@@ -123,8 +122,7 @@ class ProjectUserSelect(ew.InputField):
         return value
 
     def resources(self):
-        for r in super(ProjectUserSelect, self).resources():
-            yield r
+        yield from super().resources()
         yield ew.JSLink('allura/js/jquery-ui.min.js', location='body_top_js')
         yield ew.CSSLink('css/autocomplete.css')  # customized in [6b78ed] so we can't just use jquery-ui.min.css
         yield onready('''
@@ -158,8 +156,7 @@ class ProjectUserCombo(ew.SingleSelectField):
         return value
 
     def resources(self):
-        for r in super(ProjectUserCombo, self).resources():
-            yield r
+        yield from super().resources()
         yield ew.JSLink('allura/js/jquery-ui.min.js', location='body_top_js')
         yield ew.CSSLink('css/autocomplete.css')  # customized in [6b78ed] so we can't just use jquery-ui.min.css
         yield ew.CSSLink('css/combobox.css')
@@ -180,7 +177,7 @@ class NeighborhoodProjectSelect(ew.InputField):
         className=None)
 
     def __init__(self, url, **kw):
-        super(NeighborhoodProjectSelect, self).__init__(**kw)
+        super().__init__(**kw)
         if not isinstance(self.value, list):
             self.value = [self.value]
         self.url = url
@@ -189,8 +186,7 @@ class NeighborhoodProjectSelect(ew.InputField):
         return value
 
     def resources(self):
-        for r in super(NeighborhoodProjectSelect, self).resources():
-            yield r
+        yield from super().resources()
         yield ew.JSLink('allura/js/jquery-ui.min.js', location='body_top_js')
         yield ew.CSSLink('css/autocomplete.css')  # customized in [6b78ed] so we can't just use jquery-ui.min.css
         yield onready('''
@@ -227,8 +223,7 @@ class AttachmentAdd(ew_core.Widget):
         name=None)
 
     def resources(self):
-        for r in super(AttachmentAdd, self).resources():
-            yield r
+        yield from super().resources()
         yield onready('''
             $(".attachment_form_add_button").click(function (evt) {
                 $(this).hide();
@@ -283,8 +278,7 @@ class MarkdownEdit(ew.TextArea):
         return value
 
     def resources(self):
-        for r in super(MarkdownEdit, self).resources():
-            yield r
+        yield from super().resources()
         yield ew.JSLink('js/jquery.lightbox_me.js')
         yield ew.CSSLink('css/easymde.min.css', compress=False)
         yield ew.CSSLink('css/markitup_sf.css')
@@ -323,7 +317,7 @@ class PageList(ew_core.Widget):
                              )
 
     def prepare_context(self, context):
-        context = super(PageList, self).prepare_context(context)
+        context = super().prepare_context(context)
         count = context['count']
         page = context['page']
         limit = context['limit']
@@ -338,7 +332,7 @@ class PageList(ew_core.Widget):
     @property
     def url_params(self, **kw):
         url_params = dict()
-        for k, val in six.iteritems(request.params):
+        for k, val in request.params.items():
             if k not in ['limit', 'count', 'page']:
                 url_params[k] = val
         return url_params
@@ -356,7 +350,7 @@ class PageSize(ew_core.Widget):
     @property
     def url_params(self, **kw):
         url_params = dict()
-        for k, val in six.iteritems(request.params):
+        for k, val in request.params.items():
             if k not in ['limit', 'count', 'page']:
                 url_params[k] = val
         return url_params
@@ -367,7 +361,7 @@ class PageSize(ew_core.Widget):
                 this.form.submit();});''')
 
 
-class JQueryMixin(object):
+class JQueryMixin:
     js_widget_name = None
     js_plugin_file = None
     js_params = [
@@ -377,8 +371,7 @@ class JQueryMixin(object):
         container_cls='container')
 
     def resources(self):
-        for r in super(JQueryMixin, self).resources():
-            yield r
+        yield from super().resources()
         if self.js_plugin_file is not None:
             yield self.js_plugin_file
         opts = {
@@ -468,8 +461,7 @@ class DateField(JQueryMixin, ew.TextField):
         css_class='ui-date-field')
 
     def resources(self):
-        for r in super(DateField, self).resources():
-            yield r
+        yield from super().resources()
         yield ew.JSLink('allura/js/jquery-ui.min.js', location='body_top_js')
         yield ew.CSSLink('allura/css/smoothness/jquery-ui.min.css', compress=False)  # compress will also serve from a different location, breaking image refs
 
@@ -489,13 +481,12 @@ class AdminField(ew.InputField):
         errors=None)
 
     def __init__(self, **kw):
-        super(AdminField, self).__init__(**kw)
+        super().__init__(**kw)
         for p in self.field.get_params():
             setattr(self, p, getattr(self.field, p))
 
     def resources(self):
-        for r in self.field.resources():
-            yield r
+        yield from self.field.resources()
 
 
 class Lightbox(ew_core.Widget):
diff --git a/Allura/allura/lib/widgets/forms.py b/Allura/allura/lib/widgets/forms.py
index 14dc669..bd5cbd7 100644
--- a/Allura/allura/lib/widgets/forms.py
+++ b/Allura/allura/lib/widgets/forms.py
@@ -36,7 +36,6 @@ from allura.lib.widgets import form_fields as ffw
 from allura.lib import exceptions as forge_exc
 from allura import model as M
 import six
-from six.moves import filter
 from functools import reduce
 
 
@@ -99,7 +98,7 @@ class ForgeForm(ew.SimpleForm):
         target=None)
 
     def display_label(self, field, label_text=None):
-        ctx = super(ForgeForm, self).context_for(field)
+        ctx = super().context_for(field)
         label_text = (
             label_text
             or ctx.get('label')
@@ -110,7 +109,7 @@ class ForgeForm(ew.SimpleForm):
         return Markup(html)
 
     def context_for(self, field):
-        ctx = super(ForgeForm, self).context_for(field)
+        ctx = super().context_for(field)
         if self.antispam:
             ctx['rendered_name'] = g.antispam.enc(ctx['name'])
         return ctx
@@ -126,7 +125,7 @@ class ForgeForm(ew.SimpleForm):
 
 class ForgeFormResponsive(ForgeForm):
     def __init__(self):
-        super(ForgeFormResponsive, self).__init__()
+        super().__init__()
         # use alternate template if responsive overrides are on, but not actually using template override for this
         # since that would change all forms, and we want to just do individual ones right now
         for tmpl_override_ep in h.iter_entry_points('allura.theme.override'):
@@ -166,7 +165,7 @@ class PasswordChangeBase(ForgeForm):
 
     @ew_core.core.validator
     def to_python(self, value, state):
-        d = super(PasswordChangeBase, self).to_python(value, state)
+        d = super().to_python(value, state)
         if d['pw'] != d['pw2']:
             raise formencode.Invalid('Passwords must match', value, state)
         return d
@@ -216,7 +215,7 @@ class PasswordChangeForm(PasswordChangeBase):
 
     @ew_core.core.validator
     def to_python(self, value, state):
-        d = super(PasswordChangeForm, self).to_python(value, state)
+        d = super().to_python(value, state)
         if d['oldpw'] == d['pw']:
             raise formencode.Invalid(
                 'Your old and new password should not be the same',
@@ -429,7 +428,7 @@ class RemoveTextValueForm(ForgeForm):
 
     @ew_core.core.validator
     def to_python(self, kw, state):
-        d = super(RemoveTextValueForm, self).to_python(kw, state)
+        d = super().to_python(kw, state)
         d["oldvalue"] = kw.get('oldvalue', '')
         return d
 
@@ -484,7 +483,7 @@ class RemoveSocialNetworkForm(ForgeForm):
 
     @ew_core.core.validator
     def to_python(self, kw, state):
-        d = super(RemoveSocialNetworkForm, self).to_python(kw, state)
+        d = super().to_python(kw, state)
         d["account"] = kw.get('account', '')
         d["socialnetwork"] = kw.get('socialnetwork', '')
         return d
@@ -502,7 +501,7 @@ class AddInactivePeriodForm(ForgeForm):
 
     @ew_core.core.validator
     def to_python(self, kw, state):
-        d = super(AddInactivePeriodForm, self).to_python(kw, state)
+        d = super().to_python(kw, state)
         if d['startdate'] > d['enddate']:
                 raise formencode.Invalid(
                     'Invalid period: start date greater than end date.',
@@ -537,7 +536,7 @@ class RemoveInactivePeriodForm(ForgeForm):
 
     @ew_core.core.validator
     def to_python(self, kw, state):
-        d = super(RemoveInactivePeriodForm, self).to_python(kw, state)
+        d = super().to_python(kw, state)
         d['startdate'] = V.convertDate(kw.get('startdate', ''))
         d['enddate'] = V.convertDate(kw.get('enddate', ''))
         return d
@@ -560,7 +559,7 @@ class AddTimeSlotForm(ForgeForm):
 
     @ew_core.core.validator
     def to_python(self, kw, state):
-        d = super(AddTimeSlotForm, self).to_python(kw, state)
+        d = super().to_python(kw, state)
         if (d['starttime']['h'], d['starttime']['m']) > \
            (d['endtime']['h'], d['endtime']['m']):
                 raise formencode.Invalid(
@@ -601,7 +600,7 @@ class RemoveTimeSlotForm(ForgeForm):
 
     @ew_core.core.validator
     def to_python(self, kw, state):
-        d = super(RemoveTimeSlotForm, self).to_python(kw, state)
+        d = super().to_python(kw, state)
         d["weekday"] = kw.get('weekday', None)
         d['starttime'] = V.convertTime(kw.get('starttime', ''))
         d['endtime'] = V.convertTime(kw.get('endtime', ''))
@@ -636,7 +635,7 @@ class RemoveTroveCategoryForm(ForgeForm):
 
     @ew_core.core.validator
     def to_python(self, kw, state):
-        d = super(RemoveTroveCategoryForm, self).to_python(kw, state)
+        d = super().to_python(kw, state)
         d["categoryid"] = kw.get('categoryid')
         if d["categoryid"]:
             d["categoryid"] = int(d['categoryid'])
@@ -667,7 +666,7 @@ class AddTroveCategoryForm(ForgeForm):
 
     @ew_core.core.validator
     def to_python(self, kw, state):
-        d = super(AddTroveCategoryForm, self).to_python(kw, state)
+        d = super().to_python(kw, state)
         d["uppercategory_id"] = kw.get('uppercategory_id', 0)
         return d
 
@@ -748,7 +747,7 @@ class RemoveSkillForm(ForgeForm):
 
     @ew_core.core.validator
     def to_python(self, kw, state):
-        d = super(RemoveSkillForm, self).to_python(kw, state)
+        d = super().to_python(kw, state)
         d["categoryid"] = kw.get('categoryid', None)
         return d
 
@@ -798,7 +797,7 @@ class RegistrationForm(ForgeForm):
 
     @ew_core.core.validator
     def to_python(self, value, state):
-        d = super(RegistrationForm, self).to_python(value, state)
+        d = super().to_python(value, state)
         value['username'] = username = value['username'].lower()
         if M.User.by_username(username):
             raise formencode.Invalid(
@@ -818,7 +817,7 @@ class AdminForm(ForgeForm):
 
 class AdminFormResponsive(ForgeForm):
     def __init__(self):
-        super(AdminFormResponsive, self).__init__()
+        super().__init__()
         # use alternate template if responsive overrides are on, but not actually using template override for this
         # since that would change all forms, and we want to just do individual ones right now
         for tmpl_override_ep in h.iter_entry_points('allura.theme.override'):
@@ -853,7 +852,7 @@ class NeighborhoodOverviewForm(ForgeForm):
         else:
             self.list_color_inputs = False
             self.color_inputs = []
-        return super(NeighborhoodOverviewForm, self).from_python(value, state)
+        return super().from_python(value, state)
 
     def display_field(self, field, ignore_errors=False):
         if field.name == "css" and self.list_color_inputs:
@@ -884,11 +883,11 @@ class NeighborhoodOverviewForm(ForgeForm):
 
             return Markup(display)
         else:
-            return super(NeighborhoodOverviewForm, self).display_field(field, ignore_errors)
+            return super().display_field(field, ignore_errors)
 
     @ew_core.core.validator
     def to_python(self, value, state):
-        d = super(NeighborhoodOverviewForm, self).to_python(value, state)
+        d = super().to_python(value, state)
         neighborhood = M.Neighborhood.query.get(name=d.get('name', None))
         if neighborhood and neighborhood.features['css'] == "picker":
             css_form_dict = {}
@@ -900,8 +899,7 @@ class NeighborhoodOverviewForm(ForgeForm):
         return d
 
     def resources(self):
-        for r in super(NeighborhoodOverviewForm, self).resources():
-            yield r
+        yield from super().resources()
         yield ew.CSSLink('css/colorPicker.css')
         yield ew.CSSLink('css/jqfontselector.css')
         yield ew.CSSScript('''
@@ -963,7 +961,7 @@ class NeighborhoodAddProjectForm(ForgeForm):
     def fields(self):
         provider = plugin.ProjectRegistrationProvider.get()
         tools_options = []
-        for ep, tool in six.iteritems(g.entry_points["tool"]):
+        for ep, tool in g.entry_points["tool"].items():
             if tool.status == 'production' and tool._installable(tool_name=ep,
                                                                  nbhd=c.project.neighborhood,
                                                                  project_tools=[]):
@@ -991,15 +989,14 @@ class NeighborhoodAddProjectForm(ForgeForm):
 
     @ew_core.core.validator
     def validate(self, value, state=None):
-        value = super(NeighborhoodAddProjectForm, self).validate(value, state)
+        value = super().validate(value, state)
         provider = plugin.ProjectRegistrationProvider.get()
         if not provider.phone_verified(c.user, c.project.neighborhood):
             raise formencode.Invalid('phone-verification', value, None)
         return value
 
     def resources(self):
-        for r in super(NeighborhoodAddProjectForm, self).resources():
-            yield r
+        yield from super().resources()
         yield ew.CSSLink('css/add_project.css')
         neighborhood = g.antispam.enc('neighborhood')
         project_name = g.antispam.enc('project_name')
@@ -1110,7 +1107,7 @@ class MoveTicketForm(ForgeForm):
 
     def __init__(self, *args, **kwargs):
         trackers = kwargs.pop('trackers', [])
-        super(MoveTicketForm, self).__init__(*args, **kwargs)
+        super().__init__(*args, **kwargs)
         self.fields.tracker.options = (
             [ew.Option(py_value=v, label=l, selected=s)
              for v, l, s in sorted(trackers, key=lambda x: x[1])])
@@ -1123,7 +1120,7 @@ class CsrfForm(ew.SimpleForm):
         return [ew.HiddenField(name='_session_id')]
 
     def context_for(self, field):
-        ctx = super(CsrfForm, self).context_for(field)
+        ctx = super().context_for(field)
         if field.name == '_session_id':
             ctx['value'] = tg.request.cookies.get('_session_id') or tg.request.environ['_session_id']
         return ctx
@@ -1134,7 +1131,7 @@ class AwardGrantForm(ForgeForm):
     def __init__(self, *args, **kw):
         self._awards = kw.pop('awards', [])
         self._project_select_url = kw.pop('project_select_url', '')
-        super(AwardGrantForm, self).__init__(*args, **kw)
+        super().__init__(*args, **kw)
 
     def award_options(self):
         return [ew.Option(py_value=a.short, label=a.short) for a in self._awards]
@@ -1170,7 +1167,7 @@ class AdminSearchForm(ForgeForm):
         submit_text=None)
 
     def __init__(self, fields, *args, **kw):
-        super(AdminSearchForm, self).__init__(*args, **kw)
+        super().__init__(*args, **kw)
         self._fields = fields
 
     @property
@@ -1194,7 +1191,7 @@ class AdminSearchForm(ForgeForm):
             ])]
 
     def context_for(self, field):
-        ctx = super(AdminSearchForm, self).context_for(field)
+        ctx = super().context_for(field)
         if field.name is None and not ctx.get('value'):
             # RowField does not pass context down to the children :(
             render_ctx = ew_core.widget_context.render_context
diff --git a/Allura/allura/lib/widgets/project_list.py b/Allura/allura/lib/widgets/project_list.py
index bbbe9a2..01bc4e2 100644
--- a/Allura/allura/lib/widgets/project_list.py
+++ b/Allura/allura/lib/widgets/project_list.py
@@ -41,7 +41,7 @@ class ProjectSummary(ew_core.Widget):
         )
 
     def prepare_context(self, context):
-        response = super(ProjectSummary, self).prepare_context(context)
+        response = super().prepare_context(context)
         value = response['value']
 
         if response['icon_url'] is None:
@@ -50,21 +50,21 @@ class ProjectSummary(ew_core.Widget):
         if response['accolades'] is None:
             response['accolades'] = value.accolades
 
-        if isinstance(response['columns'], six.text_type):
+        if isinstance(response['columns'], str):
             response['columns'] = int(response['columns'])
 
         true_list = ['true', 't', '1', 'yes', 'y']
-        if isinstance(response['show_proj_icon'], six.text_type):
+        if isinstance(response['show_proj_icon'], str):
             if response['show_proj_icon'].lower() in true_list:
                 response['show_proj_icon'] = True
             else:
                 response['show_proj_icon'] = False
-        if isinstance(response['show_download_button'], six.text_type):
+        if isinstance(response['show_download_button'], str):
             if response['show_download_button'].lower() in true_list:
                 response['show_download_button'] = True
             else:
                 response['show_download_button'] = False
-        if isinstance(response['show_awards_banner'], six.text_type):
+        if isinstance(response['show_awards_banner'], str):
             if response['show_awards_banner'].lower() in true_list:
                 response['show_awards_banner'] = True
             else:
@@ -88,7 +88,7 @@ class ProjectList(ew_core.Widget):
         )
 
     def prepare_context(self, context):
-        response = super(ProjectList, self).prepare_context(context)
+        response = super().prepare_context(context)
         cred = Credentials.get()
         projects = response['projects']
         cred.load_user_roles(c.user._id, *[p._id for p in projects])
@@ -102,14 +102,13 @@ class ProjectList(ew_core.Widget):
         if response['accolades_index'] is None and response['show_awards_banner']:
             response['accolades_index'] = M.Project.accolades_index(projects)
 
-        if isinstance(response['columns'], six.text_type):
+        if isinstance(response['columns'], str):
             response['columns'] = int(response['columns'])
 
         return response
 
     def resources(self):
-        for r in self.project_summary.resources():
-            yield r
+        yield from self.project_summary.resources()
 
 
 class ProjectScreenshots(ew_core.Widget):
diff --git a/Allura/allura/lib/widgets/repo.py b/Allura/allura/lib/widgets/repo.py
index d73ef0c..db464b1 100644
--- a/Allura/allura/lib/widgets/repo.py
+++ b/Allura/allura/lib/widgets/repo.py
@@ -39,8 +39,7 @@ class SCMLogWidget(ew_core.Widget):
 
     def resources(self):
         for f in self.fields:
-            for r in f.resources():
-                yield r
+            yield from f.resources()
 
 
 class SCMRevisionWidget(ew_core.Widget):
diff --git a/Allura/allura/lib/widgets/search.py b/Allura/allura/lib/widgets/search.py
index b923618..2bef287 100644
--- a/Allura/allura/lib/widgets/search.py
+++ b/Allura/allura/lib/widgets/search.py
@@ -38,8 +38,7 @@ class SearchResults(ew_core.Widget):
 
     def resources(self):
         for f in self.fields:
-            for r in f.resources():
-                yield r
+            yield from f.resources()
         yield ew.CSSLink('css/search.css')
 
 
@@ -50,7 +49,7 @@ class SearchHelp(ffw.Lightbox):
         trigger='a.search_help_modal')
 
     def __init__(self, comments=True, history=True, fields={}):
-        super(SearchHelp, self).__init__()
+        super().__init__()
         # can't use g.jinja2_env since this widget gets imported too early :(
         jinja2_env = jinja2.Environment(
             loader=jinja2.PackageLoader('allura', 'templates/widgets'))
diff --git a/Allura/allura/lib/widgets/subscriptions.py b/Allura/allura/lib/widgets/subscriptions.py
index 83d3037..64bc33c 100644
--- a/Allura/allura/lib/widgets/subscriptions.py
+++ b/Allura/allura/lib/widgets/subscriptions.py
@@ -87,7 +87,6 @@ class SubscribeForm(ew.SimpleForm):
         return value
 
     def resources(self):
-        for r in super(SubscribeForm, self).resources():
-            yield r
+        yield from super().resources()
         if not c.user.is_anonymous():
             yield ew.JSLink('js/subscriptions.js', location='body_js_tail')  # location, to force after react js files
diff --git a/Allura/allura/lib/widgets/user_profile.py b/Allura/allura/lib/widgets/user_profile.py
index 3e77090..727ae4f 100644
--- a/Allura/allura/lib/widgets/user_profile.py
+++ b/Allura/allura/lib/widgets/user_profile.py
@@ -64,7 +64,7 @@ class SendMessageForm(ForgeForm):
         reply_to_real_address = ew.Checkbox(label='Include my email address in the reply field for this message')
 
 
-class SectionsUtil(object):
+class SectionsUtil:
 
     @staticmethod
     def load_sections(app):
@@ -80,7 +80,7 @@ class SectionsUtil(object):
         return sections
 
 
-class SectionBase(object):
+class SectionBase:
     """
     This is the base class for sections in Profile tool and Dashboard.
     """
diff --git a/Allura/allura/model/__init__.py b/Allura/allura/model/__init__.py
index 344b3b3..b7b1fdb 100644
--- a/Allura/allura/model/__init__.py
+++ b/Allura/allura/model/__init__.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-
 #       Licensed to the Apache Software Foundation (ASF) under one
 #       or more contributor license agreements.  See the NOTICE file
 #       distributed with this work for additional information
diff --git a/Allura/allura/model/artifact.py b/Allura/allura/model/artifact.py
index 90fed8b..a40c8ab 100644
--- a/Allura/allura/model/artifact.py
+++ b/Allura/allura/model/artifact.py
@@ -69,7 +69,7 @@ class Artifact(MappedClass, SearchIndexable):
     """
     class __mongometa__:
         session = artifact_orm_session
-        name = str('artifact')
+        name = 'artifact'
         indexes = [
             ('app_config_id', 'labels'),
         ]
@@ -458,7 +458,7 @@ class Artifact(MappedClass, SearchIndexable):
 
         """
         ArtifactReference.query.remove(dict(_id=self.index_id()))
-        super(Artifact, self).delete()
+        super().delete()
 
     def get_mail_footer(self, notification, toaddr):
         allow_email_posting = self.app.config.options.get('AllowEmailPosting', True)
@@ -475,7 +475,7 @@ class Artifact(MappedClass, SearchIndexable):
         False otherwise
         """
         pkg = cls.__module__.split('.', 1)[0]
-        opt = '{}.rate_limits'.format(pkg)
+        opt = f'{pkg}.rate_limits'
 
         def count_in_app():
             return cls.query.find(dict(app_config_id=app_config._id)).count()
@@ -498,7 +498,7 @@ class Snapshot(Artifact):
     """
     class __mongometa__:
         session = artifact_orm_session
-        name = str('artifact_snapshot')
+        name = 'artifact_snapshot'
         unique_indexes = [('artifact_class', 'artifact_id', 'version')]
         indexes = [('artifact_id', 'version'),
                    'author.id',
@@ -540,7 +540,7 @@ class Snapshot(Artifact):
         raise NotImplementedError('original')  # pragma no cover
 
     def shorthand_id(self):
-        return '{}#{}'.format(self.original().shorthand_id(), self.version)
+        return f'{self.original().shorthand_id()}#{self.version}'
 
     def clear_user_data(self):
         """ Redact author data for a given user """
@@ -576,7 +576,7 @@ class VersionedArtifact(Artifact):
     """
     class __mongometa__:
         session = artifact_orm_session
-        name = str('versioned_artifact')
+        name = 'versioned_artifact'
         history_class = Snapshot
 
     query: 'Query[VersionedArtifact]'
@@ -641,7 +641,7 @@ class VersionedArtifact(Artifact):
     def revert(self, version):
         ss = self.get_version(version)
         old_version = self.version
-        for k, v in six.iteritems(ss.data):
+        for k, v in ss.data.items():
             setattr(self, k, v)
         self.version = old_version
 
@@ -661,7 +661,7 @@ class VersionedArtifact(Artifact):
 
     def delete(self):
         # remove history so that the snapshots aren't left orphaned
-        super(VersionedArtifact, self).delete()
+        super().delete()
         HC = self.__mongometa__.history_class
         HC.query.remove(dict(artifact_id=self._id))
 
@@ -682,7 +682,7 @@ class VersionedArtifact(Artifact):
                 """
                 return len(artifacts)
             kwargs['count_by_user'] = distinct_artifacts_by_user
-        return super(VersionedArtifact, cls).is_limit_exceeded(*args, **kwargs)
+        return super().is_limit_exceeded(*args, **kwargs)
 
 
 class Message(Artifact):
@@ -696,7 +696,7 @@ class Message(Artifact):
 
     class __mongometa__:
         session = artifact_orm_session
-        name = str('message')
+        name = 'message'
 
     query: 'Query[Message]'
 
@@ -747,7 +747,7 @@ class AwardFile(File):
 
     class __mongometa__:
         session = main_orm_session
-        name = str('award_file')
+        name = 'award_file'
 
     query: 'Query[AwardFile]'
 
@@ -758,7 +758,7 @@ class Award(Artifact):
 
     class __mongometa__:
         session = main_orm_session
-        name = str('award')
+        name = 'award'
         indexes = ['short']
 
     query: 'Query[Award]'
@@ -804,7 +804,7 @@ class AwardGrant(Artifact):
     "An :class:`Award <allura.model.artifact.Award>` can be bestowed upon a project by a neighborhood"
     class __mongometa__:
         session = main_orm_session
-        name = str('grant')
+        name = 'grant'
         indexes = ['short']
 
     query: 'Query[AwardGrant]'
@@ -857,12 +857,12 @@ class AwardGrant(Artifact):
 
 class RssFeed(FG.Rss201rev2Feed):
     def rss_attributes(self):
-        attrs = super(RssFeed, self).rss_attributes()
+        attrs = super().rss_attributes()
         attrs['xmlns:atom'] = 'http://www.w3.org/2005/Atom'
         return attrs
 
     def add_root_elements(self, handler):
-        super(RssFeed, self).add_root_elements(handler)
+        super().add_root_elements(handler)
         if self.feed['feed_url'] is not None:
             handler.addQuickElement('atom:link', '', {
                 'rel': 'self',
@@ -879,7 +879,7 @@ class Feed(MappedClass):
     """
     class __mongometa__:
         session = project_orm_session
-        name = str('artifact_feed')
+        name = 'artifact_feed'
         indexes = [
             'pubdate',
             ('artifact_ref.project_id', 'artifact_ref.mount_point'),
@@ -924,7 +924,7 @@ class Feed(MappedClass):
 
     @classmethod
     def from_username(cls, username):
-        return cls.query.find({'author_link': "/u/{}/".format(username)}).all()
+        return cls.query.find({'author_link': f"/u/{username}/"}).all()
 
     @classmethod
     def has_access(cls, artifact):
@@ -1020,7 +1020,7 @@ class VotableArtifact(MappedClass):
 
     class __mongometa__:
         session = main_orm_session
-        name = str('vote')
+        name = 'vote'
 
     query: 'Query[VotableArtifact]'
 
@@ -1150,7 +1150,7 @@ class MovedArtifact(Artifact):
 
     class __mongometa__:
         session = artifact_orm_session
-        name = str('moved_artifact')
+        name = 'moved_artifact'
 
     query: 'Query[MovedArtifact]'
 
@@ -1164,7 +1164,7 @@ class MovedArtifact(Artifact):
 class SpamCheckResult(MappedClass):
     class __mongometa__:
         session = main_orm_session
-        name = str('spam_check_result')
+        name = 'spam_check_result'
         indexes = [
             ('project_id', 'result'),
             ('user_id', 'result'),
diff --git a/Allura/allura/model/attachments.py b/Allura/allura/model/attachments.py
index 4a46dbc..b5a3b34 100644
--- a/Allura/allura/model/attachments.py
+++ b/Allura/allura/model/attachments.py
@@ -35,7 +35,7 @@ class BaseAttachment(File):
     ArtifactType = None
 
     class __mongometa__:
-        name = str('attachment')
+        name = 'attachment'
         polymorphic_on = 'attachment_type'
         polymorphic_identity = None
         session = project_orm_session
diff --git a/Allura/allura/model/auth.py b/Allura/allura/model/auth.py
index 8fe2ad6..f188962 100644
--- a/Allura/allura/model/auth.py
+++ b/Allura/allura/model/auth.py
@@ -69,14 +69,14 @@ class AlluraUserProperty(ForeignIdProperty):
     '''
 
     def __init__(self, **kwargs):
-        super(AlluraUserProperty, self).__init__('User', allow_none=True, **kwargs)
+        super().__init__('User', allow_none=True, **kwargs)
 
 
 class EmailAddress(MappedClass):
     re_format = re.compile(r'^.*\s+<(.*)>\s*$')
 
     class __mongometa__:
-        name = str('email_address')
+        name = 'email_address'
         session = main_orm_session
         indexes = ['nonce', ]
         unique_indexes = [('email', 'claimed_by_user_id'), ]
@@ -140,7 +140,7 @@ class EmailAddress(MappedClass):
         if '@' in addr:
             try:
                 user, domain = addr.strip().split('@')
-                return '{}@{}'.format(user, domain.lower())
+                return f'{user}@{domain.lower()}'
             except ValueError:
                 return addr.strip()
         else:
@@ -175,7 +175,7 @@ please visit the following URL:
 %s
 ''' % (self.email,
        self.claimed_by_user(include_pending=True).username,
-       h.absurl('/auth/verify_addr?a={}'.format(h.urlquote(self.nonce))),
+       h.absurl(f'/auth/verify_addr?a={h.urlquote(self.nonce)}'),
        )
         log.info('Verification email:\n%s', text)
         allura.tasks.mail_tasks.sendsimplemail.post(
@@ -189,7 +189,7 @@ please visit the following URL:
 
 class AuthGlobals(MappedClass):
     class __mongometa__:
-        name = str('auth_globals')
+        name = 'auth_globals'
         session = main_orm_session
 
     query: 'Query[AuthGlobals]'
@@ -240,7 +240,7 @@ class User(MappedClass, ActivityNode, ActivityObject, SearchIndexable):
     SALT_LEN = 8
 
     class __mongometa__:
-        name = str('user')
+        name = 'user'
         session = main_orm_session
         indexes = ['tool_data.AuthPasswordReset.hash']
         unique_indexes = ['username']
@@ -426,7 +426,7 @@ class User(MappedClass, ActivityNode, ActivityObject, SearchIndexable):
                            hash=hash,
                            hash_expiry=datetime.utcnow() +
                                        timedelta(seconds=int(config.get('auth.recovery_hash_expiry_period', 600))))
-        reset_url = h.absurl('/auth/forgotten_password/{}'.format(hash))
+        reset_url = h.absurl(f'/auth/forgotten_password/{hash}')
         return reset_url
 
     def can_send_user_message(self):
@@ -872,7 +872,7 @@ class ProjectRole(MappedClass):
 
     class __mongometa__:
         session = main_orm_session
-        name = str('project_role')
+        name = 'project_role'
         unique_indexes = [('user_id', 'project_id', 'name')]
         indexes = [
             ('user_id',),
@@ -893,7 +893,7 @@ class ProjectRole(MappedClass):
 
     def __init__(self, **kw):
         assert 'project_id' in kw, 'Project roles must specify a project id'
-        super(ProjectRole, self).__init__(**kw)
+        super().__init__(**kw)
 
     def display(self):
         if self.name:
@@ -1008,7 +1008,7 @@ class ProjectRole(MappedClass):
 class AuditLog(MappedClass):
     class __mongometa__:
         session = main_orm_session
-        name = str('audit_log')
+        name = 'audit_log'
         indexes = [
             'project_id',
             'user_id',
@@ -1038,7 +1038,7 @@ class AuditLog(MappedClass):
         )
         with_br = h.nl2br_jinja_filter(self.message)
         message_bold = '<br>\n'.join([
-            line if line.startswith(standard_metadata_prefixes) else '<strong>{}</strong>'.format(line)
+            line if line.startswith(standard_metadata_prefixes) else f'<strong>{line}</strong>'
             for line in
             with_br.split('<br>\n')
         ])
@@ -1081,7 +1081,7 @@ class AuditLog(MappedClass):
 
     @classmethod
     def comment_user(cls, by, message, *args, **kwargs):
-        message = 'Comment by {}: {}'.format(by.username, message)
+        message = f'Comment by {by.username}: {message}'
         return cls.log_user(message, *args, **kwargs)
 
 
@@ -1093,7 +1093,7 @@ class UserLoginDetails(MappedClass):
     """
 
     class __mongometa__:
-        name = str('user_login_details')
+        name = 'user_login_details'
         session = main_explicitflush_orm_session
         indexes = ['user_id']
         unique_indexes = [('user_id', 'ip', 'ua'),  # DuplicateKeyError checked in add_login_detail
diff --git a/Allura/allura/model/discuss.py b/Allura/allura/model/discuss.py
index 3392be3..e6433f5 100644
--- a/Allura/allura/model/discuss.py
+++ b/Allura/allura/model/discuss.py
@@ -43,8 +43,6 @@ from .attachments import BaseAttachment
 from .auth import User, ProjectRole, AlluraUserProperty
 from .timeline import ActivityObject
 from .types import MarkdownCache
-from six.moves import range
-from six.moves import map
 
 if typing.TYPE_CHECKING:
     from ming.odm.mapper import Query
@@ -55,7 +53,7 @@ log = logging.getLogger(__name__)
 class Discussion(Artifact, ActivityObject):
 
     class __mongometa__:
-        name = str('discussion')
+        name = 'discussion'
 
     query: 'Query[Discussion]'
 
@@ -133,7 +131,7 @@ class Discussion(Artifact, ActivityObject):
         self.thread_class().query.remove(dict(discussion_id=self._id))
         self.post_class().query.remove(dict(discussion_id=self._id))
         self.attachment_class().remove(dict(discussion_id=self._id))
-        super(Discussion, self).delete()
+        super().delete()
 
     def find_posts(self, **kw):
         q = dict(kw, discussion_id=self._id, deleted=False)
@@ -143,7 +141,7 @@ class Discussion(Artifact, ActivityObject):
 class Thread(Artifact, ActivityObject):
 
     class __mongometa__:
-        name = str('thread')
+        name = 'thread'
         indexes = [
             ('artifact_id',),
             ('ref_id',),
@@ -462,7 +460,7 @@ class Thread(Artifact, ActivityObject):
         for p in self.post_class().query.find(dict(thread_id=self._id)):
             p.delete()
         self.attachment_class().remove(dict(thread_id=self._id))
-        super(Thread, self).delete()
+        super().delete()
 
     def spam(self):
         """Mark this thread as spam."""
@@ -473,7 +471,7 @@ class Thread(Artifact, ActivityObject):
 class PostHistory(Snapshot):
 
     class __mongometa__:
-        name = str('post_history')
+        name = 'post_history'
 
     query: 'Query[PostHistory]'
 
@@ -489,7 +487,7 @@ class PostHistory(Snapshot):
     def shorthand_id(self):
         original = self.original()
         if original:
-            return '{}#{}'.format(original.shorthand_id(), self.version)
+            return f'{original.shorthand_id()}#{self.version}'
         else:
             return None
 
@@ -510,7 +508,7 @@ class PostHistory(Snapshot):
 class Post(Message, VersionedArtifact, ActivityObject, ReactableArtifact):
 
     class __mongometa__:
-        name = str('post')
+        name = 'post'
         history_class = PostHistory
         indexes = [
             # used in general lookups, last_post, etc
@@ -599,7 +597,7 @@ class Post(Message, VersionedArtifact, ActivityObject, ReactableArtifact):
         return d
 
     def index(self):
-        result = super(Post, self).index()
+        result = super().index()
         result.update(
             title='Post by {} on {}'.format(
                 self.author().username, self.subject),
@@ -728,12 +726,12 @@ class Post(Message, VersionedArtifact, ActivityObject, ReactableArtifact):
         slug = h.urlquote(self.slug)
         url = self.main_url()
         if page == 0:
-            return '{}?limit={}#{}'.format(url, limit, slug)
-        return '{}?limit={}&page={}#{}'.format(url, limit, page, slug)
+            return f'{url}?limit={limit}#{slug}'
+        return f'{url}?limit={limit}&page={page}#{slug}'
 
     def shorthand_id(self):
         if self.thread:
-            return '{}#{}'.format(self.thread.shorthand_id(), self.slug)
+            return f'{self.thread.shorthand_id()}#{self.slug}'
         else:  # pragma no cover
             return None
 
@@ -840,7 +838,7 @@ class DiscussionAttachment(BaseAttachment):
     thumbnail_size = (100, 100)
 
     class __mongometa__:
-        polymorphic_identity = str('DiscussionAttachment')
+        polymorphic_identity = 'DiscussionAttachment'
         indexes = ['filename', 'discussion_id', 'thread_id', 'post_id']
 
     query: 'Query[DiscussionAttachment]'
diff --git a/Allura/allura/model/filesystem.py b/Allura/allura/model/filesystem.py
index 798c0a1..6f8ab7d 100644
--- a/Allura/allura/model/filesystem.py
+++ b/Allura/allura/model/filesystem.py
@@ -30,7 +30,6 @@ from ming.orm.declarative import MappedClass
 
 from .session import project_orm_session
 from allura.lib import utils
-from io import open
 
 if typing.TYPE_CHECKING:
     from ming.odm.mapper import Query
@@ -52,7 +51,7 @@ class File(MappedClass):
 
     class __mongometa__:
         session = project_orm_session
-        name = str('fs')
+        name = 'fs'
         indexes = ['filename']
 
     query: 'Query[File]'
@@ -63,7 +62,7 @@ class File(MappedClass):
     content_type = FieldProperty(str)
 
     def __init__(self, **kw):
-        super(File, self).__init__(**kw)
+        super().__init__(**kw)
         if self.content_type is None:
             self.content_type = utils.guess_mime_type(self.filename)
 
@@ -112,7 +111,7 @@ class File(MappedClass):
 
     def delete(self):
         self._fs().delete(self.file_id)
-        super(File, self).delete()
+        super().delete()
 
     def rfile(self):
         return self._fs().get(self.file_id)
@@ -189,7 +188,7 @@ class File(MappedClass):
 
         try:
             image = PIL.Image.open(fp)
-        except IOError as e:
+        except OSError as e:
             log.error('Error opening image %s %s', filename, e, exc_info=True)
             return None, None
 
diff --git a/Allura/allura/model/index.py b/Allura/allura/model/index.py
index cc4747c..34bf0c0 100644
--- a/Allura/allura/model/index.py
+++ b/Allura/allura/model/index.py
@@ -50,7 +50,7 @@ log = logging.getLogger(__name__)
 class ArtifactReference(MappedClass):
     class __mongometa__:
         session = main_orm_session
-        name = str('artifact_reference')
+        name = 'artifact_reference'
         indexes = [
             'references',
             'artifact_reference.project_id',  # used in ReindexCommand
@@ -92,7 +92,7 @@ class ArtifactReference(MappedClass):
         '''Look up the artifact referenced'''
         aref = self.artifact_reference
         try:
-            cls = loads(six.binary_type(aref.cls))
+            cls = loads(bytes(aref.cls))
             with h.push_context(aref.project_id):
                 return cls.query.get(_id=aref.artifact_id)
         except Exception:
@@ -105,7 +105,7 @@ class Shortlink(MappedClass):
 
     class __mongometa__:
         session = main_orm_session
-        name = str('shortlink')
+        name = 'shortlink'
         indexes = [
             'ref_id',  # for from_artifact() and index_tasks.py:del_artifacts
             ('project_id', 'link',)  # used by from_links()  More helpful to have project_id first, for other queries
@@ -190,7 +190,7 @@ class Shortlink(MappedClass):
             matches_by_artifact = {
                 link: list(matches)
                 for link, matches in groupby(q, key=lambda s: unquote(s.link))}
-            for link, d in six.iteritems(parsed_links):
+            for link, d in parsed_links.items():
                 matches = matches_by_artifact.get(unquote(d['artifact']), [])
                 matches = (
                     m for m in matches
diff --git a/Allura/allura/model/monq_model.py b/Allura/allura/model/monq_model.py
index bc1b66f..911dd64 100644
--- a/Allura/allura/model/monq_model.py
+++ b/Allura/allura/model/monq_model.py
@@ -69,7 +69,7 @@ class MonQTask(MappedClass):
 
     class __mongometa__:
         session = task_orm_session
-        name = str('monq_task')
+        name = 'monq_task'
         indexes = [
             [
                 # used in MonQTask.get() method
diff --git a/Allura/allura/model/multifactor.py b/Allura/allura/model/multifactor.py
index bd6a73e..df9a2d2 100644
--- a/Allura/allura/model/multifactor.py
+++ b/Allura/allura/model/multifactor.py
@@ -37,7 +37,7 @@ class TotpKey(MappedClass):
 
     class __mongometa__:
         session = main_orm_session
-        name = str('multifactor_totp')
+        name = 'multifactor_totp'
         unique_indexes = ['user_id']
 
     query: 'Query[TotpKey]'
@@ -54,7 +54,7 @@ class RecoveryCode(MappedClass):
 
     class __mongometa__:
         session = main_orm_session
-        name = str('multifactor_recovery_code')
+        name = 'multifactor_recovery_code'
         indexes = ['user_id']
 
     query: 'Query[RecoveryCode]'
diff --git a/Allura/allura/model/neighborhood.py b/Allura/allura/model/neighborhood.py
index 8114b8c..1c39798 100644
--- a/Allura/allura/model/neighborhood.py
+++ b/Allura/allura/model/neighborhood.py
@@ -70,7 +70,7 @@ class Neighborhood(MappedClass):
     '''
     class __mongometa__:
         session = main_orm_session
-        name = str('neighborhood')
+        name = 'neighborhood'
         unique_indexes = ['url_prefix']
 
     query: 'Query[Neighborhood]'
diff --git a/Allura/allura/model/notification.py b/Allura/allura/model/notification.py
index 5519682..4c0250a 100644
--- a/Allura/allura/model/notification.py
+++ b/Allura/allura/model/notification.py
@@ -58,7 +58,6 @@ import allura.tasks.mail_tasks
 from .session import main_orm_session
 from .auth import User, AlluraUserProperty
 import six
-from six.moves import filter
 
 if typing.TYPE_CHECKING:
     from ming.odm.mapper import Query
@@ -77,7 +76,7 @@ class Notification(MappedClass):
 
     class __mongometa__:
         session = main_orm_session
-        name = str('notification')
+        name = 'notification'
         indexes = ['project_id']
 
     query: 'Query[Notification]'
@@ -393,7 +392,7 @@ class Mailbox(MappedClass):
 
     class __mongometa__:
         session = main_orm_session
-        name = str('mailbox')
+        name = 'mailbox'
         unique_indexes = [
             ('user_id', 'project_id', 'app_config_id',
              'artifact_index_id', 'topic', 'is_flash'),
@@ -660,7 +659,7 @@ class Mailbox(MappedClass):
                         'Error sending notification: %s to mbox %s (user %s)',
                         n._id, self._id, self.user_id)
             # Accumulate messages from same address with same subject
-            for (subject, from_address, reply_to_address, author_id), ns in six.iteritems(ngroups):
+            for (subject, from_address, reply_to_address, author_id), ns in ngroups.items():
                 try:
                     if len(ns) == 1:
                         ns[0].send_direct(self.user_id)
@@ -684,7 +683,7 @@ class Mailbox(MappedClass):
                 notifications)
 
 
-class MailFooter(object):
+class MailFooter:
     view = jinja2.Environment(
         loader=jinja2.PackageLoader('allura', 'templates'),
         auto_reload=asbool(config.get('auto_reload_templates', True)),
@@ -719,7 +718,7 @@ class SiteNotification(MappedClass):
 
     class __mongometa__:
         session = main_orm_session
-        name = str('site_notification')
+        name = 'site_notification'
         indexes = [
             ('active', '_id'),
         ]
diff --git a/Allura/allura/model/oauth.py b/Allura/allura/model/oauth.py
index f142796..a5e390f 100644
--- a/Allura/allura/model/oauth.py
+++ b/Allura/allura/model/oauth.py
@@ -47,7 +47,7 @@ class OAuthToken(MappedClass):
 
     class __mongometa__:
         session = main_orm_session
-        name = str('oauth_token')
+        name = 'oauth_token'
         indexes = ['api_key']
         polymorphic_on = 'type'
         polymorphic_identity = None
@@ -70,8 +70,8 @@ class OAuthToken(MappedClass):
 class OAuthConsumerToken(OAuthToken):
 
     class __mongometa__:
-        polymorphic_identity = str('consumer')
-        name = str('oauth_consumer_token')
+        polymorphic_identity = 'consumer'
+        name = 'oauth_consumer_token'
         unique_indexes = [('name', 'user_id')]
 
     query: 'Query[OAuthConsumerToken]'
@@ -117,7 +117,7 @@ class OAuthConsumerToken(OAuthToken):
 class OAuthRequestToken(OAuthToken):
 
     class __mongometa__:
-        polymorphic_identity = str('request')
+        polymorphic_identity = 'request'
 
     query: 'Query[OAuthRequestToken]'
 
@@ -133,7 +133,7 @@ class OAuthRequestToken(OAuthToken):
 class OAuthAccessToken(OAuthToken):
 
     class __mongometa__:
-        polymorphic_identity = str('access')
+        polymorphic_identity = 'access'
 
     query: 'Query[OAuthAccessToken]'
 
diff --git a/Allura/allura/model/project.py b/Allura/allura/model/project.py
index 8b6cdd1..b994c60 100644
--- a/Allura/allura/model/project.py
+++ b/Allura/allura/model/project.py
@@ -1,4 +1,3 @@
-# coding=utf-8
 #       Licensed to the Apache Software Foundation (ASF) under one
 #       or more contributor license agreements.  See the NOTICE file
 #       distributed with this work for additional information
@@ -65,7 +64,6 @@ from .monq_model import MonQTask
 
 from .filesystem import File
 import six
-from six.moves import map
 
 if typing.TYPE_CHECKING:
     from ming.odm.mapper import Query
@@ -97,7 +95,7 @@ class ProjectCategory(MappedClass):
 
     class __mongometa__:
         session = main_orm_session
-        name = str('project_category')
+        name = 'project_category'
 
     query: 'Query[ProjectCategory]'
 
@@ -132,7 +130,7 @@ class TroveCategory(MappedClass):
 
     class __mongometa__:
         session = main_orm_session
-        name = str('trove_category')
+        name = 'trove_category'
         extensions = [TroveCategoryMapperExtension]
         indexes = ['trove_cat_id', 'trove_parent_id', 'shortname', 'fullpath']
 
@@ -197,7 +195,7 @@ class ProjectNameFieldProperty(FieldProperty):
             owning_user = instance.user_project_of
             if owning_user:
                 return owning_user.username
-        return super(ProjectNameFieldProperty, self).__get__(instance, cls)
+        return super().__get__(instance, cls)
 
 
 class Project(SearchIndexable, MappedClass, ActivityNode, ActivityObject):
@@ -211,7 +209,7 @@ class Project(SearchIndexable, MappedClass, ActivityNode, ActivityObject):
 
     class __mongometa__:
         session = main_orm_session
-        name = str('project')
+        name = 'project'
         indexes = [
             'name',
             'neighborhood_id',
@@ -407,7 +405,7 @@ class Project(SearchIndexable, MappedClass, ActivityNode, ActivityObject):
                 file_sha256 = sha256(file_bytes).hexdigest()
                 self.set_tool_data('allura', icon_sha256=file_sha256)
             except Exception as ex:
-                log.exception('Failed to calculate sha256 for icon file for {}'.format(self.shortname))
+                log.exception(f'Failed to calculate sha256 for icon file for {self.shortname}')
             return True
         return False
 
@@ -419,11 +417,11 @@ class Project(SearchIndexable, MappedClass, ActivityNode, ActivityObject):
     def icon_sized(self, w):
         allowed_sizes = list(map(int, aslist(config.get('project_icon_sizes', '16 24 32 48 64 72 96'))))
         if w not in allowed_sizes:
-            raise ValueError('Width must be one of {} (see project_icon_sizes in your .ini file)'.format(allowed_sizes))
+            raise ValueError(f'Width must be one of {allowed_sizes} (see project_icon_sizes in your .ini file)')
         if w == DEFAULT_ICON_WIDTH:
             icon_cat_name = 'icon'
         else:
-            icon_cat_name = 'icon-{}'.format(w)
+            icon_cat_name = f'icon-{w}'
         sized = ProjectFile.query.get(project_id=self._id, category=icon_cat_name)
         if not sized and w != DEFAULT_ICON_WIDTH:
             orig = self.icon_original
@@ -663,7 +661,7 @@ class Project(SearchIndexable, MappedClass, ActivityNode, ActivityObject):
         i = 0
         new_tools = []
         if not self.is_nbhd_project:
-            for tool, label in six.iteritems(anchored_tools):
+            for tool, label in anchored_tools.items():
                 if (tool not in installed_tools) and (self.app_instance(tool) is None):
                     try:
                         new_tools.append(
@@ -777,8 +775,7 @@ class Project(SearchIndexable, MappedClass, ActivityNode, ActivityObject):
         yield self
         pp = self.parent_project
         if pp:
-            for p in pp.parent_iter():
-                yield p
+            yield from pp.parent_iter()
 
     @property
     def subprojects(self):
@@ -848,7 +845,7 @@ class Project(SearchIndexable, MappedClass, ActivityNode, ActivityObject):
         try:
             return opt.validate(value)
         except fe.Invalid as e:
-            raise exceptions.ToolError('{}: {}'.format(opt.name, str(e)))
+            raise exceptions.ToolError(f'{opt.name}: {str(e)}')
 
     def last_ordinal_value(self):
         last_menu_item = self.ordered_mounts(include_hidden=True)[-1]
@@ -888,7 +885,7 @@ class Project(SearchIndexable, MappedClass, ActivityNode, ActivityObject):
         options['mount_label'] = mount_label or App.default_mount_label or mount_point
         options['ordinal'] = int(ordinal)
         options_on_install = {o.name: o for o in App.options_on_install()}
-        for o, val in six.iteritems(override_options):
+        for o, val in override_options.items():
             if o in options_on_install:
                 val = self._validate_tool_option(options_on_install[o], val)
             options[o] = val
@@ -1193,7 +1190,7 @@ class Project(SearchIndexable, MappedClass, ActivityNode, ActivityObject):
         try:
             _private = self.private
         except Exception:
-            log.warn('Error getting self.private on project {}'.format(self.shortname), exc_info=True)
+            log.warn(f'Error getting self.private on project {self.shortname}', exc_info=True)
             _private = False
         fields = dict(id=self.index_id(),
                       title='Project %s' % self.name,
@@ -1372,7 +1369,7 @@ class AppConfig(MappedClass, ActivityObject):
 
     class __mongometa__:
         session = project_orm_session
-        name = str('config')
+        name = 'config'
         indexes = [
             'project_id',
             'options.import_id',
diff --git a/Allura/allura/model/repo_refresh.py b/Allura/allura/model/repo_refresh.py
index b497105..f270ec2 100644
--- a/Allura/allura/model/repo_refresh.py
+++ b/Allura/allura/model/repo_refresh.py
@@ -105,11 +105,11 @@ def refresh_repo(repo, all_commits=False, notify=True, new_clone=False, commits_
         from allura.webhooks import RepoPushWebhookSender
         by_branches, by_tags = _group_commits(repo, commit_ids)
         params = []
-        for b, commits in six.iteritems(by_branches):
-            ref = 'refs/heads/{}'.format(b) if b != '__default__' else None
+        for b, commits in by_branches.items():
+            ref = f'refs/heads/{b}' if b != '__default__' else None
             params.append(dict(commit_ids=commits, ref=ref))
-        for t, commits in six.iteritems(by_tags):
-            ref = 'refs/tags/{}'.format(t)
+        for t, commits in by_tags.items():
+            ref = f'refs/tags/{t}'
             params.append(dict(commit_ids=commits, ref=ref))
         if params:
             RepoPushWebhookSender().send(params)
@@ -246,7 +246,7 @@ def send_notifications(repo, commit_ids):
 
     if commit_msgs:
         if len(commit_msgs) > 1:
-            subject = "{} new commits to {}".format(len(commit_msgs), repo.app.config.options.mount_label)
+            subject = f"{len(commit_msgs)} new commits to {repo.app.config.options.mount_label}"
         else:
             commit = commit_msgs[0]
             subject = 'New commit {} by {}'.format(commit['shorthand_id'], commit['author'])
diff --git a/Allura/allura/model/repository.py b/Allura/allura/model/repository.py
index 34013a7..ddedc1c 100644
--- a/Allura/allura/model/repository.py
+++ b/Allura/allura/model/repository.py
@@ -60,8 +60,6 @@ from .monq_model import MonQTask
 from .project import AppConfig
 from .session import main_doc_session
 from .session import repository_orm_session
-from io import open
-from six.moves import range
 
 if typing.TYPE_CHECKING:
     from ming.odm.mapper import Query
@@ -107,7 +105,7 @@ PYPELINE_EXTENSIONS = frozenset(utils.MARKDOWN_EXTENSIONS + ['.rst', '.textile',
 DIFF_SIMILARITY_THRESHOLD = .5  # used for determining file renames
 
 
-class RepositoryImplementation(object):
+class RepositoryImplementation:
 
     # Repository-specific code
     def init(self):  # pragma no cover
@@ -201,7 +199,7 @@ class RepositoryImplementation(object):
 
     def url_for_commit(self, commit, url_type='ci'):
         'return an URL, given either a commit or object id'
-        if isinstance(commit, six.string_types):
+        if isinstance(commit, str):
             object_id = commit
         else:
             object_id = commit._id
@@ -351,7 +349,7 @@ class Repository(Artifact, ActivityObject):
     BATCH_SIZE = 100
 
     class __mongometa__:
-        name = str('generic-repository')
+        name = 'generic-repository'
         indexes = ['upstream_repo.name']
 
     query: 'Query[Repository]'
@@ -382,7 +380,7 @@ class Repository(Artifact, ActivityObject):
                 kw['fs_path'] = self.default_fs_path(c.project, kw['tool'])
             if kw.get('url_path') is None:
                 kw['url_path'] = self.default_url_path(c.project, kw['tool'])
-        super(Repository, self).__init__(**kw)
+        super().__init__(**kw)
 
     @property
     def activity_name(self):
@@ -422,7 +420,7 @@ class Repository(Artifact, ActivityObject):
     def tarball_filename(self, revision, path=None):
         shortname = c.project.shortname.replace('/', '-')
         mount_point = c.app.config.options.mount_point
-        filename = '{}-{}-{}'.format(shortname, mount_point, revision)
+        filename = f'{shortname}-{mount_point}-{revision}'
         return filename
 
     def tarball_url(self, revision, path=None):
@@ -603,14 +601,14 @@ class Repository(Artifact, ActivityObject):
 
     @property
     def email_address(self):
-        return 'noreply@{}{}'.format(self.email_domain, config.common_suffix)
+        return f'noreply@{self.email_domain}{config.common_suffix}'
 
     def index(self):
         result = Artifact.index(self)
         result.update(
             name_s=self.name,
             type_s=self.type_s,
-            title='{} {} repository'.format(self.project.name, self.app.tool_label))
+            title=f'{self.project.name} {self.app.tool_label} repository')
         return result
 
     @property
@@ -623,7 +621,7 @@ class Repository(Artifact, ActivityObject):
             projname = owning_user.username
         else:
             projname = c.project.shortname.replace('/', '-')
-        return '{}-{}'.format(projname, self.name)
+        return f'{projname}-{self.name}'
 
     def clone_url(self, category, username=''):
         '''Return a URL string suitable for copy/paste that describes _this_ repo,
@@ -632,7 +630,7 @@ class Repository(Artifact, ActivityObject):
         if self.app.config.options.get('external_checkout_url', None):
             tpl = string.Template(self.app.config.options.external_checkout_url)
         else:
-            tpl = string.Template(tg.config.get('scm.host.{}.{}'.format(category, self.tool)))
+            tpl = string.Template(tg.config.get(f'scm.host.{category}.{self.tool}'))
         url = tpl.substitute(dict(username=username, path=self.url_path + self.name))
         # this is an svn option, but keeps clone_*() code from diverging
         url += self.app.config.options.get('checkout_url', '')
@@ -653,7 +651,7 @@ class Repository(Artifact, ActivityObject):
         '''
         if not username and c.user not in (None, User.anonymous()):
             username = c.user.username
-        tpl = string.Template(tg.config.get('scm.clone.{}.{}'.format(category, self.tool)) or
+        tpl = string.Template(tg.config.get(f'scm.clone.{category}.{self.tool}') or
                               tg.config.get('scm.clone.%s' % self.tool))
         return tpl.substitute(dict(username=username,
                                    source_url=self.clone_url(category, username),
@@ -673,7 +671,7 @@ class Repository(Artifact, ActivityObject):
         conf = tg.config.get('scm.clonechoices{}.{}'.format('_anon' if anon else '', self.tool))
         if not conf and anon:
             # check for a non-anon config
-            conf = tg.config.get('scm.clonechoices.{}'.format(self.tool))
+            conf = tg.config.get(f'scm.clonechoices.{self.tool}')
         if conf:
             return json.loads(conf)
         elif anon:
@@ -803,7 +801,7 @@ class MergeRequest(VersionedArtifact, ActivityObject):
     statuses = ['open', 'merged', 'rejected']
 
     class __mongometa__:
-        name = str('merge-request')
+        name = 'merge-request'
         indexes = ['commit_id', 'creator_id']
         unique_indexes = [('app_config_id', 'request_number')]
 
@@ -915,7 +913,7 @@ class MergeRequest(VersionedArtifact, ActivityObject):
             return False
         if self.status != 'open':
             return False
-        if asbool(tg.config.get('scm.merge.{}.disabled'.format(self.app.config.tool_name))):
+        if asbool(tg.config.get(f'scm.merge.{self.app.config.tool_name}.disabled')):
             return False
         if not h.has_access(self.app, 'write', user):
             return False
@@ -930,7 +928,7 @@ class MergeRequest(VersionedArtifact, ActivityObject):
         """
         source_hash = self.downstream.commit_id
         target_hash = self.app.repo.commit(self.target_branch)._id
-        key = '{}-{}'.format(source_hash, target_hash)
+        key = f'{source_hash}-{target_hash}'
         return key
 
     def get_can_merge_cache(self):
@@ -996,7 +994,7 @@ class MergeRequest(VersionedArtifact, ActivityObject):
         self.discussion_thread.add_post(text=message, is_meta=True, ignore_security=True)
 
 
-class RepoObject(object):
+class RepoObject:
 
     def __repr__(self):  # pragma no cover
         return '<{} {}>'.format(
@@ -1034,7 +1032,7 @@ class RepoObject(object):
 # this is duplicative with the Commit model
 # would be nice to get rid of this "doc" based view, but it is used a lot
 CommitDoc = collection(
-    str('repo_ci'), main_doc_session,
+    'repo_ci', main_doc_session,
     Field('_id', str),
     Field('tree_id', str),
     Field('committed', SUser),
@@ -1051,7 +1049,7 @@ class Commit(MappedClass, RepoObject, ActivityObject):
 
     class __mongometa__:
         session = repository_orm_session
-        name = str('repo_ci')
+        name = 'repo_ci'
         indexes = [
             'parent_ids',
             'child_ids',
@@ -1074,7 +1072,7 @@ class Commit(MappedClass, RepoObject, ActivityObject):
     repo = None
 
     def __init__(self, **kw):
-        for k, v in six.iteritems(kw):
+        for k, v in kw.items():
             setattr(self, k, v)
 
     @property
@@ -1321,7 +1319,7 @@ class Tree(MappedClass, RepoObject):
     # Basic tree information
     class __mongometa__:
         session = repository_orm_session
-        name = str('repo_tree')
+        name = 'repo_tree'
         indexes = [
         ]
 
@@ -1479,7 +1477,7 @@ class Tree(MappedClass, RepoObject):
         return Blob(self, name, x.id)
 
 
-class Blob(object):
+class Blob:
 
     '''Lightweight object representing a file in the repo'''
 
@@ -1576,7 +1574,7 @@ class EmptyBlob(Blob):
 # this is duplicative with the LastCommit model
 # would be nice to get rid of this "doc" based view, but it is used a lot
 LastCommitDoc = collection(
-    str('repo_last_commit'), main_doc_session,
+    'repo_last_commit', main_doc_session,
     Field('_id', S.ObjectId()),
     Field('commit_id', str),
     Field('path', str),
@@ -1590,7 +1588,7 @@ class LastCommit(MappedClass, RepoObject):
     # Information about the last commit to touch a tree
     class __mongometa__:
         session = repository_orm_session
-        name = str('repo_last_commit')
+        name = 'repo_last_commit'
         indexes = [
             ('commit_id', 'path'),
         ]
@@ -1606,7 +1604,7 @@ class LastCommit(MappedClass, RepoObject):
     )])
 
     def __repr__(self):
-        return '<LastCommit /{!r} {}>'.format(self.path, self.commit_id)
+        return f'<LastCommit /{self.path!r} {self.commit_id}>'
 
     @classmethod
     def _last_commit_id(cls, commit, path):
@@ -1682,13 +1680,13 @@ class LastCommit(MappedClass, RepoObject):
                 entries = {}
             # paths are fully-qualified; shorten them back to just node names
             entries = {
-                os.path.basename(path): commit_id for path, commit_id in six.iteritems(entries)}
+                os.path.basename(path): commit_id for path, commit_id in entries.items()}
         # update with the nodes changed in this tree's commit
         entries.update({node: tree.commit._id for node in changed})
         # convert to a list of dicts, since mongo doesn't handle arbitrary keys
         # well (i.e., . and $ not allowed)
         entries = [{'name': name, 'commit_id': value}
-                   for name, value in six.iteritems(entries)]
+                   for name, value in entries.items()]
         lcd = cls(
             commit_id=tree.commit._id,
             path=path,
@@ -1702,7 +1700,7 @@ class LastCommit(MappedClass, RepoObject):
         return {n.name: n.commit_id for n in self.entries}
 
 
-class ModelCache(object):
+class ModelCache:
 
     '''
     Cache model instances based on query params passed to get.  LRU cache.
@@ -1862,7 +1860,7 @@ class ModelCache(object):
             self.set(cls, keys, result)
 
 
-class GitLikeTree(object):
+class GitLikeTree:
 
     '''
     A tree node similar to that which is used in git
@@ -1919,10 +1917,10 @@ class GitLikeTree(object):
 
     def __repr__(self):
         # this can't change, is used in hex() above
-        lines = ['t {} {}'.format(t.hex(), h.really_unicode(name))
-                 for name, t in six.iteritems(self.trees)]
-        lines += ['b {} {}'.format(oid, h.really_unicode(name))
-                  for name, oid in six.iteritems(self.blobs)]
+        lines = [f't {t.hex()} {h.really_unicode(name)}'
+                 for name, t in self.trees.items()]
+        lines += [f'b {oid} {h.really_unicode(name)}'
+                  for name, oid in self.blobs.items()]
         return six.ensure_str('\n'.join(sorted(lines)))
 
     def __unicode__(self):
@@ -1933,9 +1931,9 @@ class GitLikeTree(object):
         lines = [' ' * indent + 't %s %s' %
                  (name, '\n' + t.unicode_full_tree(indent + 2, show_id=show_id)
                   if recurse else t.hex())
-                 for name, t in sorted(six.iteritems(self.trees))]
+                 for name, t in sorted(self.trees.items())]
         lines += [' ' * indent + 'b {} {}'.format(name, oid if show_id else '')
-                  for name, oid in sorted(six.iteritems(self.blobs))]
+                  for name, oid in sorted(self.blobs.items())]
         output = h.really_unicode('\n'.join(lines)).encode('utf-8')
         return output
 
diff --git a/Allura/allura/model/session.py b/Allura/allura/model/session.py
index 51a79ec..e3e5863 100644
--- a/Allura/allura/model/session.py
+++ b/Allura/allura/model/session.py
@@ -78,7 +78,7 @@ class IndexerSessionExtension(ManagedSessionExtension):
     def _objects_by_types(self, obj_list):
         result = defaultdict(list)
         for obj in obj_list:
-            class_path = '{}.{}'.format(type(obj).__module__, type(obj).__name__)
+            class_path = f'{type(obj).__module__}.{type(obj).__name__}'
             result[class_path].append(obj)
         return result
 
@@ -104,12 +104,12 @@ class IndexerSessionExtension(ManagedSessionExtension):
         for obj_list, action in actions:
             if obj_list:
                 types_objects_map = self._objects_by_types(obj_list)
-                for class_path, obj_list in six.iteritems(types_objects_map):
+                for class_path, obj_list in types_objects_map.items():
                     tasks = self.TASKS.get(class_path)
                     if tasks:
                         self._index_action(tasks, obj_list, action)
 
-        super(IndexerSessionExtension, self).after_flush(obj)
+        super().after_flush(obj)
 
 
 class ArtifactSessionExtension(ManagedSessionExtension):
@@ -135,7 +135,7 @@ class ArtifactSessionExtension(ManagedSessionExtension):
                 log.exception(
                     "Failed to update artifact references. Is this a borked project migration?")
             self.update_index(self.objects_deleted, arefs)
-        super(ArtifactSessionExtension, self).after_flush(obj)
+        super().after_flush(obj)
 
     def update_index(self, objects_deleted, arefs):
         # Post delete and add indexing operations
diff --git a/Allura/allura/model/stats.py b/Allura/allura/model/stats.py
index 731ed59..2092c89 100644
--- a/Allura/allura/model/stats.py
+++ b/Allura/allura/model/stats.py
@@ -31,7 +31,6 @@ import difflib
 
 from allura.model.session import main_orm_session
 from allura.lib import helpers as h
-from six.moves import range
 from functools import reduce
 
 if typing.TYPE_CHECKING:
@@ -41,7 +40,7 @@ if typing.TYPE_CHECKING:
 class Stats(MappedClass):
 
     class __mongometa__:
-        name = str('basestats')
+        name = 'basestats'
         session = main_orm_session
         unique_indexes = ['_id']
 
diff --git a/Allura/allura/model/timeline.py b/Allura/allura/model/timeline.py
index b1d7ed3..9c4da0e 100644
--- a/Allura/allura/model/timeline.py
+++ b/Allura/allura/model/timeline.py
@@ -44,7 +44,7 @@ class Director(ActivityDirector):
             return
 
         from allura.model.project import Project
-        super(Director, self).create_activity(actor, verb, obj,
+        super().create_activity(actor, verb, obj,
                                               target=target,
                                               related_nodes=related_nodes,
                                               tags=tags)
@@ -65,7 +65,7 @@ class ActivityNode(NodeBase):
 
     @property
     def node_id(self):
-        return "{}:{}".format(self.__class__.__name__, self._id)
+        return f"{self.__class__.__name__}:{self._id}"
 
 
 class ActivityObject(ActivityObjectBase):
@@ -76,7 +76,7 @@ class ActivityObject(ActivityObjectBase):
     @property
     def activity_name(self):
         """Override this for each Artifact type."""
-        return "{} {}".format(self.__mongometa__.name.capitalize(), self._id)
+        return f"{self.__mongometa__.name.capitalize()} {self._id}"
 
     @property
     def activity_url(self):
@@ -94,7 +94,7 @@ class ActivityObject(ActivityObjectBase):
         """Return a string which uniquely identifies this object and which can
         be used to retrieve the object from mongo.
         """
-        return "{}:{}".format(self.__class__.__name__, self._id)
+        return f"{self.__class__.__name__}:{self._id}"
 
     def has_activity_access(self, perm, user, activity):
         """Return True if user has perm access to this object, otherwise
diff --git a/Allura/allura/model/types.py b/Allura/allura/model/types.py
index 833e685..8a7c15c 100644
--- a/Allura/allura/model/types.py
+++ b/Allura/allura/model/types.py
@@ -24,7 +24,7 @@ EVERYONE, ALL_PERMISSIONS = None, '*'
 class MarkdownCache(S.Object):
 
     def __init__(self, **kw):
-        super(MarkdownCache, self).__init__(
+        super().__init__(
             fields=dict(
                 md5=S.String(),
                 fix7528=S.Anything,
@@ -50,7 +50,7 @@ class ACE(S.Object):
             permission = S.String()
         else:
             permission = S.OneOf('*', *permissions)
-        super(ACE, self).__init__(
+        super().__init__(
             fields=dict(
                 access=S.OneOf(self.ALLOW, self.DENY),
                 reason=S.String(),
@@ -88,7 +88,7 @@ class ACL(S.Array):
     '''
 
     def __init__(self, permissions=None, **kwargs):
-        super(ACL, self).__init__(
+        super().__init__(
             field_type=ACE(permissions), **kwargs)
 
     @classmethod
diff --git a/Allura/allura/model/webhook.py b/Allura/allura/model/webhook.py
index f67b406..34d39ac 100644
--- a/Allura/allura/model/webhook.py
+++ b/Allura/allura/model/webhook.py
@@ -33,7 +33,7 @@ if typing.TYPE_CHECKING:
 
 class Webhook(Artifact):
     class __mongometa__:
-        name = str('webhook')
+        name = 'webhook'
         unique_indexes = [('app_config_id', 'type', 'hook_url')]
 
     query: 'Query[Webhook]'
@@ -46,7 +46,7 @@ class Webhook(Artifact):
     def url(self):
         app = self.app_config.load()
         app = app(self.app_config.project, self.app_config)
-        return '{}webhooks/{}/{}'.format(app.admin_url, self.type, self._id)
+        return f'{app.admin_url}webhooks/{self.type}/{self._id}'
 
     def enforce_limit(self):
         '''Returns False if limit is reached, otherwise True'''
@@ -71,9 +71,9 @@ class Webhook(Artifact):
 
     def __json__(self):
         return {
-            '_id': six.text_type(self._id),
+            '_id': str(self._id),
             'url': h.absurl('/rest' + self.url()),
-            'type': six.text_type(self.type),
-            'hook_url': six.text_type(self.hook_url),
+            'type': str(self.type),
+            'hook_url': str(self.hook_url),
             'mod_date': self.mod_date,
         }
diff --git a/Allura/allura/scripts/create_sitemap_files.py b/Allura/allura/scripts/create_sitemap_files.py
index 3bcbc4c..d501fa0 100644
--- a/Allura/allura/scripts/create_sitemap_files.py
+++ b/Allura/allura/scripts/create_sitemap_files.py
@@ -42,8 +42,6 @@ from tg import config
 from allura import model as M
 from allura.lib import security, utils
 from allura.scripts import ScriptTask
-from io import open
-from six.moves import range
 
 
 MAX_SITEMAP_URLS = 50000
diff --git a/Allura/allura/scripts/delete_projects.py b/Allura/allura/scripts/delete_projects.py
index 6fe2566..3f29685 100644
--- a/Allura/allura/scripts/delete_projects.py
+++ b/Allura/allura/scripts/delete_projects.py
@@ -57,7 +57,7 @@ class DeleteProjects(ScriptTask):
     @classmethod
     def get_project(cls, proj):
         n, p = proj.split('/', 1)
-        n = M.Neighborhood.query.get(url_prefix='/{}/'.format(n))
+        n = M.Neighborhood.query.get(url_prefix=f'/{n}/')
         if not n:
             log.warn("Can't find neighborhood for %s", proj)
             return
diff --git a/Allura/allura/scripts/refresh_last_commits.py b/Allura/allura/scripts/refresh_last_commits.py
index 75487b0..72495e0 100644
--- a/Allura/allura/scripts/refresh_last_commits.py
+++ b/Allura/allura/scripts/refresh_last_commits.py
@@ -42,7 +42,7 @@ class RefreshLastCommits(ScriptTask):
                 repo_type = repo_type.strip()
                 if repo_type not in ['git', 'hg']:
                     raise argparse.ArgumentTypeError(
-                        '{} is not a valid repo type.'.format(repo_type))
+                        f'{repo_type} is not a valid repo type.')
                 repo_types.append(repo_type)
             return repo_types
         parser = argparse.ArgumentParser(description='Using existing commit data, '
diff --git a/Allura/allura/scripts/refreshrepo.py b/Allura/allura/scripts/refreshrepo.py
index 271fb36..2336a56 100644
--- a/Allura/allura/scripts/refreshrepo.py
+++ b/Allura/allura/scripts/refreshrepo.py
@@ -128,7 +128,7 @@ class RefreshRepo(ScriptTask):
                 repo_type = repo_type.strip()
                 if repo_type not in ['svn', 'git', 'hg']:
                     raise argparse.ArgumentTypeError(
-                        '{} is not a valid repo type.'.format(repo_type))
+                        f'{repo_type} is not a valid repo type.')
                 repo_types.append(repo_type)
             return repo_types
 
diff --git a/Allura/allura/scripts/scripttask.py b/Allura/allura/scripts/scripttask.py
index f136a1c..f466610 100644
--- a/Allura/allura/scripts/scripttask.py
+++ b/Allura/allura/scripts/scripttask.py
@@ -65,7 +65,7 @@ class MetaParserDocstring(type):
         return task(type.__new__(meta, classname, bases, classDict))
 
 
-class ScriptTask(six.with_metaclass(MetaParserDocstring, object)):
+class ScriptTask(metaclass=MetaParserDocstring):
 
     """Base class for a command-line script that is also executable as a task."""
 
diff --git a/Allura/allura/scripts/trac_export.py b/Allura/allura/scripts/trac_export.py
index cc8f846..cb131a5 100644
--- a/Allura/allura/scripts/trac_export.py
+++ b/Allura/allura/scripts/trac_export.py
@@ -70,7 +70,7 @@ Export ticket data from a Trac instance''')
     return options, args
 
 
-class TracExport(object):
+class TracExport:
 
     PAGE_SIZE = 100
     TICKET_URL = 'ticket/%s'
@@ -105,7 +105,7 @@ class TracExport(object):
     def remap_fields(self, dict):
         "Remap fields to adhere to standard taxonomy."
         out = {}
-        for k, v in six.iteritems(dict):
+        for k, v in dict.items():
             key = self.match_pattern(r'\W*(\w+)\W*', k)
             out[self.FIELD_MAP.get(key, key)] = v
 
@@ -136,7 +136,7 @@ class TracExport(object):
     @staticmethod
     def match_pattern(regexp, string):
         m = re.match(regexp, string)
-        assert m, "'{}' didn't match '{}'".format(regexp, string)
+        assert m, f"'{regexp}' didn't match '{string}'"
         for grp in m.groups():
             if grp is not None:
                 return grp
@@ -180,10 +180,10 @@ class TracExport(object):
                 r'.* by ', '', comment.find('h3', 'change').text).strip()
             c['date'] = self.trac2z_date(
                 comment.find('a', 'timeline')['title'].replace(' in Timeline', '').replace('See timeline at ', ''))
-            changes = six.text_type(comment.find('ul', 'changes') or '')
+            changes = str(comment.find('ul', 'changes') or '')
             body = comment.find('div', 'comment')
             body = body.renderContents('utf8').decode('utf8') if body else ''
-            body = body.replace('href="{}'.format(relative_base_url), 'href="')  # crude way to rewrite ticket links
+            body = body.replace(f'href="{relative_base_url}', 'href="')  # crude way to rewrite ticket links
             c['comment'] = html2text.html2text(changes + body)
             c['class'] = 'COMMENT'
             comments.append(c)
diff --git a/Allura/allura/tasks/activity_tasks.py b/Allura/allura/tasks/activity_tasks.py
index f6bf8b6..9928c32 100644
--- a/Allura/allura/tasks/activity_tasks.py
+++ b/Allura/allura/tasks/activity_tasks.py
@@ -29,7 +29,7 @@ def create_timelines(node_id):
 @task
 def change_user_name(user_id, new_name):
     Activity.query.update(
-        {'actor.node_id': "User:{}".format(user_id)},
+        {'actor.node_id': f"User:{user_id}"},
         {'$set': {
             "actor.activity_name": new_name,
         }},
diff --git a/Allura/allura/tasks/event_tasks.py b/Allura/allura/tasks/event_tasks.py
index eae9a97..3bed9a0 100644
--- a/Allura/allura/tasks/event_tasks.py
+++ b/Allura/allura/tasks/event_tasks.py
@@ -32,6 +32,6 @@ def event(event_type, *args, **kwargs):
             exceptions.append(sys.exc_info())
     if exceptions:
         if len(exceptions) == 1:
-            six.reraise(exceptions[0][0], exceptions[0][1], exceptions[0][2])
+            raise exceptions[0][1].with_traceback(exceptions[0][2])
         else:
             raise CompoundError(*exceptions)
diff --git a/Allura/allura/tasks/export_tasks.py b/Allura/allura/tasks/export_tasks.py
index cfba554..90b0369 100644
--- a/Allura/allura/tasks/export_tasks.py
+++ b/Allura/allura/tasks/export_tasks.py
@@ -46,7 +46,7 @@ def bulk_export(tools, filename=None, send_email=True, with_attachments=False):
     return BulkExport().process(c.project, tools, c.user, filename, send_email, with_attachments)
 
 
-class BulkExport(object):
+class BulkExport:
 
     def process(self, project, tools, user, filename=None, send_email=True, with_attachments=False):
         export_filename = filename or project.bulk_export_filename()
diff --git a/Allura/allura/tasks/index_tasks.py b/Allura/allura/tasks/index_tasks.py
index 8a834f6..7d71284 100644
--- a/Allura/allura/tasks/index_tasks.py
+++ b/Allura/allura/tasks/index_tasks.py
@@ -126,7 +126,7 @@ def add_artifacts(ref_ids, update_solr=True, update_refs=True, solr_hosts=None):
         __get_solr(solr_hosts).add(solr_updates)
 
     if len(exceptions) == 1:
-        six.reraise(exceptions[0][0], exceptions[0][1], exceptions[0][2])
+        raise exceptions[0][1].with_traceback(exceptions[0][2])
     if exceptions:
         raise CompoundError(*exceptions)
     check_for_dirty_ming_records('add_artifacts task')
@@ -153,7 +153,7 @@ def commit():
 
 @task
 def solr_del_tool(project_id, mount_point_s):
-    g.solr.delete(q='project_id_s:"{}" AND mount_point_s:"{}"'.format(project_id, mount_point_s))
+    g.solr.delete(q=f'project_id_s:"{project_id}" AND mount_point_s:"{mount_point_s}"')
 
 @contextmanager
 def _indexing_disabled(session):
diff --git a/Allura/allura/tasks/mail_tasks.py b/Allura/allura/tasks/mail_tasks.py
index 7ce5767..541f471 100644
--- a/Allura/allura/tasks/mail_tasks.py
+++ b/Allura/allura/tasks/mail_tasks.py
@@ -153,7 +153,7 @@ def sendmail(fromaddr, destinations, text, reply_to, subject,
     addrs_multi = []
     if fromaddr is None:
         fromaddr = g.noreply
-    elif not isinstance(fromaddr, six.string_types) or '@' not in fromaddr:
+    elif not isinstance(fromaddr, str) or '@' not in fromaddr:
         log.warning('Looking up user with fromaddr: %s', fromaddr)
         user = M.User.query.get(_id=ObjectId(fromaddr), disabled=False, pending=False)
         if not user:
@@ -222,7 +222,7 @@ def sendsimplemail(
     from allura import model as M
     if fromaddr is None:
         fromaddr = g.noreply
-    elif not isinstance(fromaddr, six.string_types) or '@' not in fromaddr:
+    elif not isinstance(fromaddr, str) or '@' not in fromaddr:
         log.warning('Looking up user with fromaddr: %s', fromaddr)
         user = M.User.query.get(_id=ObjectId(fromaddr), disabled=False, pending=False)
         if not user:
@@ -231,7 +231,7 @@ def sendsimplemail(
         else:
             fromaddr = user.email_address_header()
 
-    if not isinstance(toaddr, six.string_types) or '@' not in toaddr:
+    if not isinstance(toaddr, str) or '@' not in toaddr:
         log.warning('Looking up user with toaddr: %s', toaddr)
         user = M.User.query.get(_id=ObjectId(toaddr), disabled=False, pending=False)
         if not user:
@@ -255,7 +255,7 @@ def send_system_mail_to_user(user_or_emailaddr, subject, text):
     :param subject: subject of the email
     :param text: text of the email (markdown)
     '''
-    if isinstance(user_or_emailaddr, six.string_types):
+    if isinstance(user_or_emailaddr, str):
         toaddr = user_or_emailaddr
     else:
         toaddr = user_or_emailaddr._id
@@ -266,8 +266,8 @@ def send_system_mail_to_user(user_or_emailaddr, subject, text):
             config['site_name'],
             config['forgemail.return_path']
         ),
-        'sender': six.text_type(config['forgemail.return_path']),
-        'reply_to': six.text_type(config['forgemail.return_path']),
+        'sender': str(config['forgemail.return_path']),
+        'reply_to': str(config['forgemail.return_path']),
         'message_id': h.gen_message_id(),
         'subject': subject,
         'text': text,
diff --git a/Allura/allura/templates/__init__.py b/Allura/allura/templates/__init__.py
index 4ab9cb7..7b9f3fc 100644
--- a/Allura/allura/templates/__init__.py
+++ b/Allura/allura/templates/__init__.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-
 #       Licensed to the Apache Software Foundation (ASF) under one
 #       or more contributor license agreements.  See the NOTICE file
 #       distributed with this work for additional information
@@ -19,4 +17,3 @@
 
 """Templates package for the application."""
 
-from __future__ import unicode_literals
\ No newline at end of file
diff --git a/Allura/allura/templates_responsive/responsive_overrides.py b/Allura/allura/templates_responsive/responsive_overrides.py
index 0478620..062be75 100644
--- a/Allura/allura/templates_responsive/responsive_overrides.py
+++ b/Allura/allura/templates_responsive/responsive_overrides.py
@@ -16,7 +16,7 @@
 #       under the License.
 
 
-class ResponsiveOverrides(object):
+class ResponsiveOverrides:
     '''
     Placeholder to trigger usage of template overrides in the /override/ dir
     Could put specific rules here if needed, but we don't need it.
diff --git a/Allura/allura/tests/__init__.py b/Allura/allura/tests/__init__.py
index 8047c6b..a64deb3 100644
--- a/Allura/allura/tests/__init__.py
+++ b/Allura/allura/tests/__init__.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-
 #       Licensed to the Apache Software Foundation (ASF) under one
 #       or more contributor license agreements.  See the NOTICE file
 #       distributed with this work for additional information
diff --git a/Allura/allura/tests/decorators.py b/Allura/allura/tests/decorators.py
index 1a28721..a53a714 100644
--- a/Allura/allura/tests/decorators.py
+++ b/Allura/allura/tests/decorators.py
@@ -92,7 +92,7 @@ with_wiki = with_tool('test', 'Wiki', 'wiki')
 with_url = with_tool('test', 'ShortUrl', 'url')
 
 
-class raises(object):
+class raises:
 
     '''
     Test helper in the form of a context manager, to assert that something raises an exception.
@@ -129,7 +129,7 @@ def without_module(*module_names):
     return _without_module
 
 
-class patch_middleware_config(object):
+class patch_middleware_config:
 
     '''
     Context manager that patches the configuration used during middleware
@@ -169,7 +169,7 @@ def audits(*messages, **kwargs):
         actor = kwargs.get('actor', '.*')
         ip_addr = kwargs.get('ip_addr', '.*')
         user_agent = kwargs.get('user_agent', '.*')
-        preamble = '(Done by user: {}\n)?IP Address: {}\nUser-Agent: {}\n'.format(actor, ip_addr, user_agent)
+        preamble = f'(Done by user: {actor}\n)?IP Address: {ip_addr}\nUser-Agent: {user_agent}\n'
     else:
         preamble = ''
 
@@ -182,7 +182,7 @@ def audits(*messages, **kwargs):
                 hints += '\nin these AuditLog messages:\n\t' + '\n\t'.join(a.message for a in all)
             if message != re.escape(message):
                 hints += '\nYou may need to escape the regex chars in the text you are matching'
-            raise AssertionError('Could not find "{}"{}'.format(message, hints))
+            raise AssertionError(f'Could not find "{message}"{hints}')
 
 
 @contextlib.contextmanager
@@ -199,7 +199,7 @@ def out_audits(*messages, **kwargs):
     if kwargs.get('user'):
         actor = kwargs.get('actor', '.*')
         ip_addr = kwargs.get('ip_addr', '.*')
-        preamble = '(Done by user: {}\n)?IP Address: {}\n'.format(actor, ip_addr)
+        preamble = f'(Done by user: {actor}\n)?IP Address: {ip_addr}\n'
     else:
         preamble = ''
     for message in messages:
@@ -218,7 +218,7 @@ def assert_logmsg_and_no_warnings_or_errors(logs, msg):
         if msg in r.getMessage():
             found_msg = True
         if r.levelno > logging.INFO:
-            raise AssertionError('unexpected log {} {}'.format(r.levelname, r.getMessage()))
+            raise AssertionError(f'unexpected log {r.levelname} {r.getMessage()}')
     assert found_msg, 'Did not find {} in logs: {}'.format(msg, '\n'.join([r.getMessage() for r in logs.records]))
 
 
diff --git a/Allura/allura/tests/functional/__init__.py b/Allura/allura/tests/functional/__init__.py
index 9055977..f4a7b12 100644
--- a/Allura/allura/tests/functional/__init__.py
+++ b/Allura/allura/tests/functional/__init__.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-
 #       Licensed to the Apache Software Foundation (ASF) under one
 #       or more contributor license agreements.  See the NOTICE file
 #       distributed with this work for additional information
@@ -19,4 +17,3 @@
 
 """Functional test suite for the controllers of the application."""
 
-from __future__ import unicode_literals
\ No newline at end of file
diff --git a/Allura/allura/tests/functional/test_admin.py b/Allura/allura/tests/functional/test_admin.py
index 37ce2d5..287e121 100644
--- a/Allura/allura/tests/functional/test_admin.py
+++ b/Allura/allura/tests/functional/test_admin.py
@@ -1,4 +1,3 @@
-# coding=utf-8
 #       Licensed to the Apache Software Foundation (ASF) under one
 #       or more contributor license agreements.  See the NOTICE file
 #       distributed with this work for additional information
@@ -21,7 +20,6 @@ from datetime import datetime
 import pkg_resources
 from io import BytesIO
 import logging
-from io import open
 
 import tg
 import PIL
@@ -813,7 +811,7 @@ class TestProjectAdmin(TestController):
         # make sure can still access homepage after one of user's roles were
         # deleted
         r = self.app.get('/p/test/wiki/',
-                         extra_environ=dict(username=str('test-user'))).follow()
+                         extra_environ=dict(username='test-user')).follow()
         assert r.status == '200 OK'
 
     def test_change_perms(self):
@@ -906,7 +904,7 @@ class TestProjectAdmin(TestController):
 
     def test_admin_extension_sidebar(self):
 
-        class FooSettingsController(object):
+        class FooSettingsController:
 
             @expose()
             def index(self, *a, **kw):
@@ -946,7 +944,7 @@ class TestProjectAdmin(TestController):
 class TestExport(TestController):
 
     def setUp(self):
-        super(TestExport, self).setUp()
+        super().setUp()
         self.setup_with_tools()
 
     @td.with_wiki
@@ -963,17 +961,17 @@ class TestExport(TestController):
 
     def test_access(self):
         r = self.app.get('/admin/export',
-                         extra_environ={'username': str('*anonymous')}).follow()
+                         extra_environ={'username': '*anonymous'}).follow()
         assert_equals(r.request.url,
                       'http://localhost/auth/?return_to=%2Fadmin%2Fexport')
         self.app.get('/admin/export',
-                     extra_environ={'username': str('test-user')},
+                     extra_environ={'username': 'test-user'},
                      status=403)
         r = self.app.post('/admin/export',
-                          extra_environ={'username': str('*anonymous')}).follow()
+                          extra_environ={'username': '*anonymous'}).follow()
         assert_equals(r.request.url, 'http://localhost/auth/')
         self.app.post('/admin/export',
-                      extra_environ={'username': str('test-user')},
+                      extra_environ={'username': 'test-user'},
                       status=403)
 
     def test_ini_option(self):
@@ -1047,13 +1045,13 @@ class TestExport(TestController):
     def test_bulk_export_filename_for_user_project(self):
         project = M.Project.query.get(shortname='u/test-user')
         filename = project.bulk_export_filename()
-        assert filename.startswith('test-user-backup-{}-'.format(datetime.utcnow().year))
+        assert filename.startswith(f'test-user-backup-{datetime.utcnow().year}-')
         assert filename.endswith('.zip')
 
     def test_bulk_export_filename_for_nbhd(self):
         project = M.Project.query.get(name='Home Project for Projects')
         filename = project.bulk_export_filename()
-        assert filename.startswith('p-backup-{}-'.format(datetime.utcnow().year))
+        assert filename.startswith(f'p-backup-{datetime.utcnow().year}-')
         assert filename.endswith('.zip')
 
     def test_bulk_export_path_for_nbhd(self):
@@ -1255,7 +1253,7 @@ class TestRestInstallTool(TestRestApiBase):
             'mount_label': 'wiki_label1'
         }
         r = self.app.post('/rest/p/test/admin/install_tool/',
-                          extra_environ={'username': str('*anonymous')},
+                          extra_environ={'username': '*anonymous'},
                           status=401,
                           params=data)
         assert_equals(r.status, '401 Unauthorized')
diff --git a/Allura/allura/tests/functional/test_auth.py b/Allura/allura/tests/functional/test_auth.py
index 72ece15..99a16a5 100644
--- a/Allura/allura/tests/functional/test_auth.py
+++ b/Allura/allura/tests/functional/test_auth.py
@@ -51,7 +51,6 @@ from allura.lib import plugin
 from allura.lib import helpers as h
 from allura.lib.multifactor import TotpService, RecoveryCodeService
 import six
-from six.moves import range
 
 
 def unentity(s):
@@ -84,7 +83,7 @@ class TestAuth(TestController):
         r = self.app.post('/auth/do_login', antispam=True, params=dict(
             username='test-user', password='foo', honey1='robot',  # bad honeypot value
             _session_id=self.app.cookies['_session_id']),
-                          extra_environ={'regular_antispam_err_handling_even_when_tests': str('true')},
+                          extra_environ={'regular_antispam_err_handling_even_when_tests': 'true'},
                           status=302)
         wf = json.loads(self.webflash(r))
         assert_equal(wf['status'], 'error')
@@ -102,18 +101,18 @@ class TestAuth(TestController):
         assert 'Invalid login' in str(r), r.showbrowser()
 
     def test_login_invalid_username(self):
-        extra = {'username': str('*anonymous')}
+        extra = {'username': '*anonymous'}
         r = self.app.get('/auth/', extra_environ=extra)
         f = r.forms[0]
         encoded = self.app.antispam_field_names(f)
         f[encoded['username']] = 'test@user.com'
         f[encoded['password']] = 'foo'
-        r = f.submit(extra_environ={'username': str('*anonymous')})
+        r = f.submit(extra_environ={'username': '*anonymous'})
         r.mustcontain('Usernames only include small letters, ')
 
     def test_login_diff_ips_ok(self):
         # exercises AntiSpam.validate methods
-        extra = {'username': str('*anonymous'), 'REMOTE_ADDR': str('11.22.33.44')}
+        extra = {'username': '*anonymous', 'REMOTE_ADDR': '11.22.33.44'}
         r = self.app.get('/auth/', extra_environ=extra)
 
         f = r.forms[0]
@@ -121,19 +120,19 @@ class TestAuth(TestController):
         f[encoded['username']] = 'test-user'
         f[encoded['password']] = 'foo'
         with audits('Successful login', user=True):
-            r = f.submit(extra_environ={'username': str('*anonymous'), 'REMOTE_ADDR': str('11.22.33.99')})
+            r = f.submit(extra_environ={'username': '*anonymous', 'REMOTE_ADDR': '11.22.33.99'})
 
     def test_login_diff_ips_bad(self):
         # exercises AntiSpam.validate methods
-        extra = {'username': str('*anonymous'), 'REMOTE_ADDR': str('24.52.32.123')}
+        extra = {'username': '*anonymous', 'REMOTE_ADDR': '24.52.32.123'}
         r = self.app.get('/auth/', extra_environ=extra)
 
         f = r.forms[0]
         encoded = self.app.antispam_field_names(f)
         f[encoded['username']] = 'test-user'
         f[encoded['password']] = 'foo'
-        r = f.submit(extra_environ={'username': str('*anonymous'), 'REMOTE_ADDR': str('11.22.33.99'),
-                                    'regular_antispam_err_handling_even_when_tests': str('true')},
+        r = f.submit(extra_environ={'username': '*anonymous', 'REMOTE_ADDR': '11.22.33.99',
+                                    'regular_antispam_err_handling_even_when_tests': 'true'},
                      status=302)
         wf = json.loads(self.webflash(r))
         assert_equal(wf['status'], 'error')
@@ -143,7 +142,7 @@ class TestAuth(TestController):
     @patch('allura.tasks.mail_tasks.sendsimplemail')
     def test_login_hibp_compromised_password_untrusted_client(self, sendsimplemail):
         # first & only login by this user, so won't have any trusted previous logins
-        self.app.extra_environ = {'disable_auth_magic': str('True')}
+        self.app.extra_environ = {'disable_auth_magic': 'True'}
         r = self.app.get('/auth/')
         f = r.forms[0]
         encoded = self.app.antispam_field_names(f)
@@ -165,7 +164,7 @@ class TestAuth(TestController):
 
     @patch('allura.tasks.mail_tasks.sendsimplemail')
     def test_login_hibp_compromised_password_trusted_client(self, sendsimplemail):
-        self.app.extra_environ = {'disable_auth_magic': str('True')}
+        self.app.extra_environ = {'disable_auth_magic': 'True'}
 
         # regular login first, so IP address will be recorded and then trusted
         r = self.app.get('/auth/')
@@ -201,43 +200,43 @@ class TestAuth(TestController):
     def test_login_disabled(self):
         u = M.User.query.get(username='test-user')
         u.disabled = True
-        r = self.app.get('/auth/', extra_environ={'username': str('*anonymous')})
+        r = self.app.get('/auth/', extra_environ={'username': '*anonymous'})
         f = r.forms[0]
         encoded = self.app.antispam_field_names(f)
         f[encoded['username']] = 'test-user'
         f[encoded['password']] = 'foo'
         with audits('Failed login', user=True):
-            r = f.submit(extra_environ={'username': str('*anonymous')})
+            r = f.submit(extra_environ={'username': '*anonymous'})
 
     def test_login_pending(self):
         u = M.User.query.get(username='test-user')
         u.pending = True
-        r = self.app.get('/auth/', extra_environ={'username': str('*anonymous')})
+        r = self.app.get('/auth/', extra_environ={'username': '*anonymous'})
         f = r.forms[0]
         encoded = self.app.antispam_field_names(f)
         f[encoded['username']] = 'test-user'
         f[encoded['password']] = 'foo'
         with audits('Failed login', user=True):
-            r = f.submit(extra_environ={'username': str('*anonymous')})
+            r = f.submit(extra_environ={'username': '*anonymous'})
 
     def test_login_overlay(self):
-        r = self.app.get('/auth/login_fragment/', extra_environ={'username': str('*anonymous')})
+        r = self.app.get('/auth/login_fragment/', extra_environ={'username': '*anonymous'})
         f = r.forms[0]
         encoded = self.app.antispam_field_names(f)
         f[encoded['username']] = 'test-user'
         f[encoded['password']] = 'foo'
         with audits('Successful login', user=True):
-            r = f.submit(extra_environ={'username': str('*anonymous')})
+            r = f.submit(extra_environ={'username': '*anonymous'})
 
     def test_logout(self):
-        self.app.extra_environ = {'disable_auth_magic': str('True')}
+        self.app.extra_environ = {'disable_auth_magic': 'True'}
         nav_pattern = ('nav', {'class': 'nav-main'})
         r = self.app.get('/auth/')
 
         r = self.app.post('/auth/do_login', params=dict(
             username='test-user', password='foo',
             _session_id=self.app.cookies['_session_id']),
-            extra_environ={'REMOTE_ADDR': str('127.0.0.1')},
+            extra_environ={'REMOTE_ADDR': '127.0.0.1'},
             antispam=True).follow().follow()
 
         logged_in_session = r.session['_id']
@@ -258,8 +257,8 @@ class TestAuth(TestController):
 
         self.app.get('/').follow()  # establish session
         self.app.post('/auth/do_login',
-                      headers={str('User-Agent'): str('browser')},
-                      extra_environ={'REMOTE_ADDR': str('127.0.0.1')},
+                      headers={'User-Agent': 'browser'},
+                      extra_environ={'REMOTE_ADDR': '127.0.0.1'},
                       params=dict(
                           username='test-user',
                           password='foo',
@@ -316,7 +315,7 @@ class TestAuth(TestController):
                           'password': 'foo',
                           '_session_id': self.app.cookies['_session_id'],
                       },
-                      extra_environ=dict(username=str('test-admin')))
+                      extra_environ=dict(username='test-admin'))
 
         assert M.EmailAddress.find(dict(email=email_address, claimed_by_user_id=user._id)).count() == 1
         r = self.app.post('/auth/preferences/update_emails',
@@ -328,7 +327,7 @@ class TestAuth(TestController):
                               'password': 'foo',
                               '_session_id': self.app.cookies['_session_id'],
                           },
-                          extra_environ=dict(username=str('test-admin')))
+                          extra_environ=dict(username='test-admin'))
 
         assert json.loads(self.webflash(r))['status'] == 'error', self.webflash(r)
         assert M.EmailAddress.find(dict(email=email_address, claimed_by_user_id=user._id)).count() == 1
@@ -362,7 +361,7 @@ class TestAuth(TestController):
                               'password': 'foo',
                               '_session_id': self.app.cookies['_session_id'],
                           },
-                          extra_environ=dict(username=str('test-admin')))
+                          extra_environ=dict(username='test-admin'))
 
         assert json.loads(self.webflash(r))['status'] == 'ok'
         assert json.loads(self.webflash(r))['message'] == 'A verification email has been sent.  ' \
@@ -405,7 +404,7 @@ class TestAuth(TestController):
                               'password': 'foo',
                               '_session_id': self.app.cookies['_session_id'],
                           },
-                          extra_environ=dict(username=str('test-user-1')))
+                          extra_environ=dict(username='test-user-1'))
 
         assert json.loads(self.webflash(r))['status'] == 'ok'
         assert json.loads(self.webflash(r))['message'] == 'A verification email has been sent.  ' \
@@ -429,7 +428,7 @@ class TestAuth(TestController):
                                   'password': 'foo',
                                   '_session_id': self.app.cookies['_session_id'],
                               },
-                              extra_environ=dict(username=str('test-user-1')))
+                              extra_environ=dict(username='test-user-1'))
             assert json.loads(self.webflash(r))['status'] == 'ok'
 
             r = self.app.post('/auth/preferences/update_emails',
@@ -441,7 +440,7 @@ class TestAuth(TestController):
                                   'password': 'foo',
                                   '_session_id': self.app.cookies['_session_id'],
                               },
-                              extra_environ=dict(username=str('test-user-1')))
+                              extra_environ=dict(username='test-user-1'))
 
             assert json.loads(self.webflash(r))['status'] == 'error'
             assert json.loads(self.webflash(r))['message'] == 'You cannot claim more than 2 email addresses.'
@@ -467,7 +466,7 @@ class TestAuth(TestController):
 
         r = self.app.post('/auth/send_verification_link',
                           params=dict(a=email_address, _session_id=self.app.cookies['_session_id']),
-                          extra_environ=dict(username=str('test-user-1'), _session_id=self.app.cookies['_session_id']))
+                          extra_environ=dict(username='test-user-1', _session_id=self.app.cookies['_session_id']))
 
         assert json.loads(self.webflash(r))['status'] == 'ok'
         assert json.loads(self.webflash(r))['message'] == 'Verification link sent'
@@ -493,7 +492,7 @@ class TestAuth(TestController):
         self.app.post('/auth/send_verification_link',
                       params=dict(a=email_address,
                                   _session_id=self.app.cookies['_session_id']),
-                      extra_environ=dict(username=str('test-user')))
+                      extra_environ=dict(username='test-user'))
 
         user1 = M.User.query.get(username='test-user-1')
         user1.claim_address(email_address)
@@ -502,7 +501,7 @@ class TestAuth(TestController):
         ThreadLocalORMSession.flush_all()
         # Verify first email with the verification link
         r = self.app.get('/auth/verify_addr', params=dict(a=email.nonce),
-                         extra_environ=dict(username=str('test-user')))
+                         extra_environ=dict(username='test-user'))
 
         assert json.loads(self.webflash(r))['status'] == 'error'
         email = M.EmailAddress.find(dict(email=email_address, claimed_by_user_id=user._id)).first()
@@ -524,23 +523,23 @@ class TestAuth(TestController):
         self.app.post('/auth/send_verification_link',
                       params=dict(a=email_address,
                                   _session_id=self.app.cookies['_session_id']),
-                      extra_environ=dict(username=str('test-user')))
+                      extra_environ=dict(username='test-user'))
 
         # logged out, gets redirected to login page
         r = self.app.get('/auth/verify_addr', params=dict(a=email.nonce),
-                         extra_environ=dict(username=str('*anonymous')))
+                         extra_environ=dict(username='*anonymous'))
         assert_in('/auth/?return_to=%2Fauth%2Fverify_addr', r.location)
 
         # logged in as someone else
         r = self.app.get('/auth/verify_addr', params=dict(a=email.nonce),
-                         extra_environ=dict(username=str('test-admin')))
+                         extra_environ=dict(username='test-admin'))
         assert_in('/auth/?return_to=%2Fauth%2Fverify_addr', r.location)
         assert_equal('You must be logged in to the correct account', json.loads(self.webflash(r))['message'])
         assert_equal('warning', json.loads(self.webflash(r))['status'])
 
         # logged in as correct user
         r = self.app.get('/auth/verify_addr', params=dict(a=email.nonce),
-                         extra_environ=dict(username=str('test-user')))
+                         extra_environ=dict(username='test-user'))
         assert_in('confirmed', json.loads(self.webflash(r))['message'])
         assert_equal('ok', json.loads(self.webflash(r))['status'])
 
@@ -605,7 +604,7 @@ class TestAuth(TestController):
         self.app.get('/').follow()  # establish session
         change_params['_session_id'] = self.app.cookies['_session_id']
         self.app.post('/auth/preferences/update_emails',
-                      extra_environ=dict(username=str('test-admin')),
+                      extra_environ=dict(username='test-admin'),
                       params=change_params)
 
         u = M.User.by_username('test-admin')
@@ -623,7 +622,7 @@ class TestAuth(TestController):
         # Change password
         with audits('Password changed', user=True):
             self.app.post('/auth/preferences/change_password',
-                          extra_environ=dict(username=str('test-admin')),
+                          extra_environ=dict(username='test-admin'),
                           params={
                               'oldpw': 'foo',
                               'pw': 'asdfasdf',
@@ -654,7 +653,7 @@ class TestAuth(TestController):
 
         # Attempt change password with weak pwd
         r = self.app.post('/auth/preferences/change_password',
-                          extra_environ=dict(username=str('test-admin')),
+                          extra_environ=dict(username='test-admin'),
                           params={
                               'oldpw': 'foo',
                               'pw': 'password',
@@ -665,7 +664,7 @@ class TestAuth(TestController):
         assert 'Unsafe' in str(r.headers)
 
         r = self.app.post('/auth/preferences/change_password',
-                          extra_environ=dict(username=str('test-admin')),
+                          extra_environ=dict(username='test-admin'),
                           params={
                               'oldpw': 'foo',
                               'pw': '3j84rhoirwnoiwrnoiw',
@@ -683,7 +682,7 @@ class TestAuth(TestController):
     @td.with_user_project('test-admin')
     def test_prefs(self, gen_message_id, sendsimplemail):
         r = self.app.get('/auth/preferences/',
-                         extra_environ=dict(username=str('test-admin')))
+                         extra_environ=dict(username='test-admin'))
         # check preconditions of test data
         assert 'test@example.com' not in r
         assert 'test-admin@users.localhost' in r
@@ -693,7 +692,7 @@ class TestAuth(TestController):
         # add test@example
         with td.audits('New email address: test@example.com', user=True):
             r = self.app.post('/auth/preferences/update_emails',
-                              extra_environ=dict(username=str('test-admin')),
+                              extra_environ=dict(username='test-admin'),
                               params={
                                   'new_addr.addr': 'test@example.com',
                                   'new_addr.claim': 'Claim Address',
@@ -710,7 +709,7 @@ class TestAuth(TestController):
         # remove test-admin@users.localhost
         with td.audits('Email address deleted: test-admin@users.localhost', user=True):
             r = self.app.post('/auth/preferences/update_emails',
-                              extra_environ=dict(username=str('test-admin')),
+                              extra_environ=dict(username='test-admin'),
                               params={
                                   'addr-1.ord': '1',
                                   'addr-1.delete': 'on',
@@ -738,7 +737,7 @@ class TestAuth(TestController):
                               params={'preferences.display_name': 'Admin',
                                       '_session_id': self.app.cookies['_session_id'],
                                       },
-                              extra_environ=dict(username=str('test-admin')))
+                              extra_environ=dict(username='test-admin'))
 
     @td.with_user_project('test-admin')
     @patch('allura.tasks.mail_tasks.sendsimplemail')
@@ -754,21 +753,21 @@ class TestAuth(TestController):
         }
         r = self.app.post('/auth/preferences/update_emails',
                           params=new_email_params,
-                          extra_environ=dict(username=str('test-admin')))
+                          extra_environ=dict(username='test-admin'))
         assert_in('You must provide your current password to claim new email', self.webflash(r))
         assert_not_in('test@example.com', r.follow())
         new_email_params['password'] = 'bad pass'
 
         r = self.app.post('/auth/preferences/update_emails',
                           params=new_email_params,
-                          extra_environ=dict(username=str('test-admin')))
+                          extra_environ=dict(username='test-admin'))
         assert_in('You must provide your current password to claim new email', self.webflash(r))
         assert_not_in('test@example.com', r.follow())
         new_email_params['password'] = 'foo'  # valid password
 
         r = self.app.post('/auth/preferences/update_emails',
                           params=new_email_params,
-                          extra_environ=dict(username=str('test-admin')))
+                          extra_environ=dict(username='test-admin'))
         assert_not_in('You must provide your current password to claim new email', self.webflash(r))
         assert_in('test@example.com', r.follow())
 
@@ -780,14 +779,14 @@ class TestAuth(TestController):
         }
         r = self.app.post('/auth/preferences/update_emails',
                           params=change_primary_params,
-                          extra_environ=dict(username=str('test-admin')))
+                          extra_environ=dict(username='test-admin'))
         assert_in('You must provide your current password to change primary address', self.webflash(r))
         assert_equal(M.User.by_username('test-admin').get_pref('email_address'), 'test-admin@users.localhost')
         change_primary_params['password'] = 'bad pass'
 
         r = self.app.post('/auth/preferences/update_emails',
                           params=change_primary_params,
-                          extra_environ=dict(username=str('test-admin')))
+                          extra_environ=dict(username='test-admin'))
         assert_in('You must provide your current password to change primary address', self.webflash(r))
         assert_equal(M.User.by_username('test-admin').get_pref('email_address'), 'test-admin@users.localhost')
         change_primary_params['password'] = 'foo'  # valid password
@@ -795,7 +794,7 @@ class TestAuth(TestController):
         self.app.get('/auth/preferences/')  # let previous 'flash' message cookie get used up
         r = self.app.post('/auth/preferences/update_emails',
                           params=change_primary_params,
-                          extra_environ=dict(username=str('test-admin')))
+                          extra_environ=dict(username='test-admin'))
         assert_not_in('You must provide your current password to change primary address', self.webflash(r))
         assert_equal(M.User.by_username('test-admin').get_pref('email_address'), 'test@example.com')
 
@@ -815,26 +814,26 @@ class TestAuth(TestController):
         }
         r = self.app.post('/auth/preferences/update_emails',
                           params=remove_email_params,
-                          extra_environ=dict(username=str('test-admin')))
+                          extra_environ=dict(username='test-admin'))
         assert_in('You must provide your current password to delete an email', self.webflash(r))
         assert_in('test@example.com', r.follow())
         remove_email_params['password'] = 'bad pass'
         r = self.app.post('/auth/preferences/update_emails',
                           params=remove_email_params,
-                          extra_environ=dict(username=str('test-admin')))
+                          extra_environ=dict(username='test-admin'))
         assert_in('You must provide your current password to delete an email', self.webflash(r))
         assert_in('test@example.com', r.follow())
         remove_email_params['password'] = 'foo'  # vallid password
         r = self.app.post('/auth/preferences/update_emails',
                           params=remove_email_params,
-                          extra_environ=dict(username=str('test-admin')))
+                          extra_environ=dict(username='test-admin'))
         assert_not_in('You must provide your current password to delete an email', self.webflash(r))
         assert_not_in('test@example.com', r.follow())
 
     @td.with_user_project('test-admin')
     def test_prefs_subscriptions(self):
         r = self.app.get('/auth/subscriptions/',
-                         extra_environ=dict(username=str('test-admin')))
+                         extra_environ=dict(username='test-admin'))
         subscriptions = M.Mailbox.query.find(dict(
             user_id=c.user._id, is_flash=False)).all()
         # make sure page actually lists all the user's subscriptions
@@ -869,7 +868,7 @@ class TestAuth(TestController):
 
     def _find_subscriptions_form(self, r):
         form = None
-        for f in six.itervalues(r.forms):
+        for f in r.forms.values():
             if f.action == 'update_subscriptions':
                 form = f
                 break
@@ -878,7 +877,7 @@ class TestAuth(TestController):
 
     def _find_subscriptions_field(self, form, subscribed=False):
         field_name = None
-        for k, v in six.iteritems(form.fields):
+        for k, v in form.fields.items():
             if subscribed:
                 check = v and v[0].value == 'on'
             else:
@@ -891,7 +890,7 @@ class TestAuth(TestController):
     @td.with_user_project('test-admin')
     def test_prefs_subscriptions_subscribe(self):
         resp = self.app.get('/auth/subscriptions/',
-                            extra_environ=dict(username=str('test-admin')))
+                            extra_environ=dict(username='test-admin'))
         form = self._find_subscriptions_form(resp)
         # find not subscribed tool, subscribe and verify
         field_name = self._find_subscriptions_field(form, subscribed=False)
@@ -907,7 +906,7 @@ class TestAuth(TestController):
     @td.with_user_project('test-admin')
     def test_prefs_subscriptions_unsubscribe(self):
         resp = self.app.get('/auth/subscriptions/',
-                            extra_environ=dict(username=str('test-admin')))
+                            extra_environ=dict(username='test-admin'))
         form = self._find_subscriptions_form(resp)
         field_name = self._find_subscriptions_field(form, subscribed=True)
         s_id = ObjectId(form.fields[field_name + '.subscription_id'][0].value)
@@ -1070,7 +1069,7 @@ class TestAuth(TestController):
             dict(user_id=user._id, project_id=p._id)).count() == 0
 
         self.app.get('/p/test/admin/permissions',
-                     extra_environ=dict(username=str('aaa')), status=403)
+                     extra_environ=dict(username='aaa'), status=403)
         assert M.ProjectRole.query.find(
             dict(user_id=user._id, project_id=p._id)).count() <= 1
 
@@ -1083,7 +1082,7 @@ class TestAuth(TestController):
         sess = session(user)
         assert not user.disabled
         r = self.app.get('/p/test/admin/',
-                         extra_environ={'username': str('test-admin')})
+                         extra_environ={'username': 'test-admin'})
         assert_equal(r.status_int, 200, 'Redirect to %s' % r.location)
         user.disabled = True
         sess.save(user)
@@ -1091,7 +1090,7 @@ class TestAuth(TestController):
         user = M.User.query.get(username='test-admin')
         assert user.disabled
         r = self.app.get('/p/test/admin/',
-                         extra_environ={'username': str('test-admin')})
+                         extra_environ={'username': 'test-admin'})
         assert_equal(r.status_int, 302)
         assert_equal(r.location, 'http://localhost/auth/?return_to=%2Fp%2Ftest%2Fadmin%2F')
 
@@ -1526,9 +1525,9 @@ class TestPasswordReset(TestController):
     test_primary_email = 'testprimaryaddr@mail.com'
 
     def setUp(self):
-        super(TestPasswordReset, self).setUp()
+        super().setUp()
         # so test-admin isn't automatically logged in for all requests
-        self.app.extra_environ = {'disable_auth_magic': str('True')}
+        self.app.extra_environ = {'disable_auth_magic': 'True'}
 
     @patch('allura.model.User.send_password_reset_email')
     @patch('allura.lib.plugin.LocalAuthenticationProvider.resend_verification_link')
@@ -1797,7 +1796,7 @@ class TestOAuth(TestController):
                                   }, status=302)
         r = self.app.get('/auth/oauth/')
         assert_equal(r.forms[1].action, 'generate_access_token')
-        r = r.forms[1].submit(extra_environ={'username': str('test-user')})  # not the right user
+        r = r.forms[1].submit(extra_environ={'username': 'test-user'})  # not the right user
         assert_in("Invalid app ID", self.webflash(r))                   # gets an error
         r = self.app.get('/auth/oauth/')                                # do it again
         r = r.forms[1].submit()                                         # as correct user
@@ -2115,7 +2114,7 @@ class TestDisableAccount(TestController):
     def test_not_authenticated(self):
         r = self.app.get(
             '/auth/disable/',
-            extra_environ={'username': str('*anonymous')})
+            extra_environ={'username': '*anonymous'})
         assert_equal(r.status_int, 302)
         assert_equal(r.location,
                      'http://localhost/auth/?return_to=%2Fauth%2Fdisable%2F')
@@ -2157,21 +2156,21 @@ class TestDisableAccount(TestController):
 
 class TestPasswordExpire(TestController):
     def login(self, username='test-user', pwd='foo', query_string=''):
-        extra = {'username': str('*anonymous'), 'REMOTE_ADDR': str('127.0.0.1')}
+        extra = {'username': '*anonymous', 'REMOTE_ADDR': '127.0.0.1'}
         r = self.app.get('/auth/' + query_string, extra_environ=extra)
 
         f = r.forms[0]
         encoded = self.app.antispam_field_names(f)
         f[encoded['username']] = username
         f[encoded['password']] = pwd
-        return f.submit(extra_environ={'username': str('*anonymous')})
+        return f.submit(extra_environ={'username': '*anonymous'})
 
     def assert_redirects(self, where='/'):
-        resp = self.app.get(where, extra_environ={'username': str('test-user')}, status=302)
+        resp = self.app.get(where, extra_environ={'username': 'test-user'}, status=302)
         assert_equal(resp.location, 'http://localhost/auth/pwd_expired?' + urlencode({'return_to': where}))
 
     def assert_not_redirects(self, where='/neighborhood'):
-        self.app.get(where, extra_environ={'username': str('test-user')}, status=200)
+        self.app.get(where, extra_environ={'username': 'test-user'}, status=200)
 
     def test_disabled(self):
         r = self.login()
@@ -2223,7 +2222,7 @@ class TestPasswordExpire(TestController):
             r = self.login()
             assert_true(self.expired(r))
             self.assert_redirects()
-            r = self.app.get('/auth/logout', extra_environ={'username': str('test-user')})
+            r = self.app.get('/auth/logout', extra_environ={'username': 'test-user'})
             assert_false(self.expired(r))
             self.assert_not_redirects()
 
@@ -2237,12 +2236,12 @@ class TestPasswordExpire(TestController):
             user = M.User.by_username('test-user')
             old_update_time = user.last_password_updated
             old_password = user.password
-            r = self.app.get('/auth/pwd_expired', extra_environ={'username': str('test-user')})
+            r = self.app.get('/auth/pwd_expired', extra_environ={'username': 'test-user'})
             f = r.forms[0]
             f['oldpw'] = 'foo'
             f['pw'] = 'qwerty'
             f['pw2'] = 'qwerty'
-            r = f.submit(extra_environ={'username': str('test-user')}, status=302)
+            r = f.submit(extra_environ={'username': 'test-user'}, status=302)
             assert_equal(r.location, 'http://localhost/')
             assert_false(self.expired(r))
             user = M.User.by_username('test-user')
@@ -2277,12 +2276,12 @@ class TestPasswordExpire(TestController):
             session(user).flush(user)
 
             # Change expired password
-            r = self.app.get('/auth/pwd_expired', extra_environ={'username': str('test-user')})
+            r = self.app.get('/auth/pwd_expired', extra_environ={'username': 'test-user'})
             f = r.forms[0]
             f['oldpw'] = 'foo'
             f['pw'] = 'qwerty'
             f['pw2'] = 'qwerty'
-            r = f.submit(extra_environ={'username': str('test-user')}, status=302)
+            r = f.submit(extra_environ={'username': 'test-user'}, status=302)
             assert_equal(r.location, 'http://localhost/')
 
             user = M.User.by_username('test-user')
@@ -2296,12 +2295,12 @@ class TestPasswordExpire(TestController):
         user = M.User.by_username('test-user')
         old_update_time = user.last_password_updated
         old_password = user.password
-        r = self.app.get('/auth/pwd_expired', extra_environ={'username': str('test-user')})
+        r = self.app.get('/auth/pwd_expired', extra_environ={'username': 'test-user'})
         f = r.forms[0]
         f['oldpw'] = oldpw
         f['pw'] = pw
         f['pw2'] = pw2
-        r = f.submit(extra_environ={'username': str('test-user')})
+        r = f.submit(extra_environ={'username': 'test-user'})
         assert_true(self.expired(r))
         user = M.User.by_username('test-user')
         assert_equal(user.last_password_updated, old_update_time)
@@ -2340,20 +2339,20 @@ class TestPasswordExpire(TestController):
             # but if user tries to go directly there anyway, intercept and redirect back
             self.assert_redirects(where=return_to)
 
-            r = self.app.get('/auth/pwd_expired', extra_environ={'username': str('test-user')})
+            r = self.app.get('/auth/pwd_expired', extra_environ={'username': 'test-user'})
             f = r.forms[0]
             f['oldpw'] = 'foo'
             f['pw'] = 'qwerty'
             f['pw2'] = 'qwerty'
             f['return_to'] = return_to
-            r = f.submit(extra_environ={'username': str('test-user')}, status=302)
+            r = f.submit(extra_environ={'username': 'test-user'}, status=302)
             assert_equal(r.location, 'http://localhost/p/test/tickets/?milestone=1.0&page=2')
 
 
 class TestCSRFProtection(TestController):
     def test_blocks_invalid(self):
         # so test-admin isn't automatically logged in for all requests
-        self.app.extra_environ = {'disable_auth_magic': str('True'), 'REMOTE_ADDR': str('127.0.0.1')}
+        self.app.extra_environ = {'disable_auth_magic': 'True', 'REMOTE_ADDR': '127.0.0.1'}
 
         # regular login
         r = self.app.get('/auth/')
@@ -2581,7 +2580,7 @@ class TestTwoFactor(TestController):
         self._init_totp()
 
         # so test-admin isn't automatically logged in for all requests
-        self.app.extra_environ = {'disable_auth_magic': str('True')}
+        self.app.extra_environ = {'disable_auth_magic': 'True'}
 
         # regular login
         r = self.app.get('/auth/?return_to=/p/foo')
@@ -2618,7 +2617,7 @@ class TestTwoFactor(TestController):
         self._init_totp()
 
         # so test-admin isn't automatically logged in for all requests
-        self.app.extra_environ = {'disable_auth_magic': str('True')}
+        self.app.extra_environ = {'disable_auth_magic': 'True'}
 
         # regular login
         r = self.app.get('/auth/?return_to=/p/foo')
@@ -2649,7 +2648,7 @@ class TestTwoFactor(TestController):
         self._init_totp()
 
         # so test-admin isn't automatically logged in for all requests
-        self.app.extra_environ = {'disable_auth_magic': str('True')}
+        self.app.extra_environ = {'disable_auth_magic': 'True'}
 
         # regular login
         r = self.app.get('/auth/')
@@ -2679,7 +2678,7 @@ class TestTwoFactor(TestController):
         self._init_totp()
 
         # so test-admin isn't automatically logged in for all requests
-        self.app.extra_environ = {'disable_auth_magic': str('True')}
+        self.app.extra_environ = {'disable_auth_magic': 'True'}
 
         # regular login
         r = self.app.get('/auth/?return_to=/p/foo')
@@ -2726,7 +2725,7 @@ class TestTwoFactor(TestController):
         self._init_totp()
 
         # so test-admin isn't automatically logged in for all requests
-        self.app.extra_environ = {'disable_auth_magic': str('True')}
+        self.app.extra_environ = {'disable_auth_magic': 'True'}
 
         # regular login
         r = self.app.get('/auth/?return_to=/p/foo')
diff --git a/Allura/allura/tests/functional/test_discuss.py b/Allura/allura/tests/functional/test_discuss.py
index cc3b669..948a7af 100644
--- a/Allura/allura/tests/functional/test_discuss.py
+++ b/Allura/allura/tests/functional/test_discuss.py
@@ -25,8 +25,6 @@ from allura.tests import TestController
 from allura import model as M
 from allura.lib import helpers as h
 from tg import config
-from io import open
-from six.moves import range
 
 
 class TestDiscussBase(TestController):
@@ -81,8 +79,8 @@ class TestDiscuss(TestDiscussBase):
                 params[field['name']] = field.get('value') or ''
         params[f.find('textarea')['name']] = text
         r = self.app.post(f['action'], params=params,
-                          headers={str('Referer'): str(thread_link)},
-                          extra_environ=dict(username=str('root')))
+                          headers={'Referer': str(thread_link)},
+                          extra_environ=dict(username='root'))
         r = r.follow()
         return r
 
@@ -107,7 +105,7 @@ class TestDiscuss(TestDiscussBase):
         params[post_form.find('textarea')['name']] = 'This is a new post'
         r = self.app.post(post_link,
                           params=params,
-                          headers={str('Referer'): str(thread_link)})
+                          headers={'Referer': str(thread_link)})
         r = r.follow()
         assert 'This is a new post' in r, r
         r = self.app.get(post_link)
@@ -121,7 +119,7 @@ class TestDiscuss(TestDiscussBase):
         params[post_form.find('textarea')['name']] = 'Tis a reply'
         r = self.app.post(post_link + 'reply',
                           params=params,
-                          headers={str('Referer'): str(post_link)})
+                          headers={'Referer': str(post_link)})
         r = self.app.get(thread_link)
         assert 'Tis a reply' in r, r
         permalinks = [post.find('form')['action']
@@ -135,7 +133,7 @@ class TestDiscuss(TestDiscussBase):
     def test_rate_limit_comments(self):
         with h.push_config(config, **{'allura.rate_limits_per_user': '{"3600": 2}'}):
             for i in range(0, 2):
-                r = self._make_post('This is a post {}'.format(i))
+                r = self._make_post(f'This is a post {i}')
                 assert 'rate limit exceeded' not in r.text
 
             r = self._make_post('This is a post that should fail.')
@@ -163,14 +161,14 @@ class TestDiscuss(TestDiscussBase):
         ]
 
         self.app.get(thread_url, status=200,  # ok
-                     extra_environ=dict(username=str('test-admin')))
+                     extra_environ=dict(username='test-admin'))
         self.app.get(thread_url, status=403,  # forbidden
                      extra_environ=dict(username=str(non_admin)))
 
     def test_spam_link(self):
         r = self._make_post('Test post')
         assert '<span><i class="fa fa-exclamation" aria-hidden="true"></i></span>' in r
-        r = self.app.get('/wiki/Home/', extra_environ={'username': str('test-user-1')})
+        r = self.app.get('/wiki/Home/', extra_environ={'username': 'test-user-1'})
         assert '<span><i class="fa fa-exclamation" aria-hidden="true"></i></span>' not in r, 'User without moderate perm must not see Spam link'
 
     @patch('allura.controllers.discuss.g.spam_checker.submit_spam')
@@ -214,7 +212,7 @@ class TestDiscuss(TestDiscussBase):
             update_link,
             params={
                 'text': '- [x] checkbox'},
-            extra_environ=dict(username=str('*anonymous')))
+            extra_environ=dict(username='*anonymous'))
         assert response.json['status'] == 'no_permission'
 
     def test_comment_post_reaction_new(self):
@@ -239,14 +237,14 @@ class TestDiscuss(TestDiscussBase):
             react_link,
             params={
                 'r': ':+1:'},
-            extra_environ=dict(username=str('*anonymous')))
+            extra_environ=dict(username='*anonymous'))
         assert response.json['error'] == 'no_permission'
         # even anon can't send invalid reactions
         response = self.app.post(
             react_link,
             params={
                 'r': 'invalid'},
-            extra_environ=dict(username=str('*anonymous')))
+            extra_environ=dict(username='*anonymous'))
         assert response.json['error'] == 'no_permission'
 
     def test_comment_post_reaction_change(self):
@@ -401,7 +399,7 @@ class TestDiscuss(TestDiscussBase):
 class TestAttachment(TestDiscussBase):
 
     def setUp(self):
-        super(TestAttachment, self).setUp()
+        super().setUp()
         self.thread_link = self._thread_link()
         thread = self.app.get(self.thread_link)
         for f in thread.html.findAll('form'):
@@ -415,7 +413,7 @@ class TestAttachment(TestDiscussBase):
                 params[field['name']] = field.get('value') or ''
         params[f.find('textarea')['name']] = 'Test Post'
         r = self.app.post(f['action'], params=params,
-                          headers={str('Referer'): str(self.thread_link)})
+                          headers={'Referer': str(self.thread_link)})
         r = r.follow()
         self.post_link = str(
             r.html.find('div', {'class': 'edit_post_form reply'}).find('form')['action'])
@@ -491,15 +489,15 @@ class TestAttachment(TestDiscussBase):
         self.app.get(thumblink, status=200)
         _, slug = self.post_link.rstrip('/').rsplit('/', 1)
         post = M.Post.query.get(slug=slug)
-        assert post, 'Could not find post for {} {}'.format(slug, self.post_link)
+        assert post, f'Could not find post for {slug} {self.post_link}'
         post.deleted = True
         session(post).flush(post)
         self.app.get(alink, status=404)
         self.app.get(thumblink, status=404)
 
     def test_unmoderated_post_attachments(self):
-        ordinary_user = {'username': str('test-user')}
-        moderator = {'username': str('test-admin')}
+        ordinary_user = {'username': 'test-user'}
+        moderator = {'username': 'test-admin'}
         # set up attachment
         f = os.path.join(os.path.dirname(__file__), '..', 'data', 'user.png')
         with open(f, 'rb') as f:
@@ -519,7 +517,7 @@ class TestAttachment(TestDiscussBase):
         # make post unmoderated
         _, slug = self.post_link.rstrip('/').rsplit('/', 1)
         post = M.Post.query.get(slug=slug)
-        assert post, 'Could not find post for {} {}'.format(slug, self.post_link)
+        assert post, f'Could not find post for {slug} {self.post_link}'
         post.status = 'pending'
         session(post).flush(post)
         # ... make sure attachment is not visible to ordinary user
diff --git a/Allura/allura/tests/functional/test_feeds.py b/Allura/allura/tests/functional/test_feeds.py
index b5de861..3cbbc9f 100644
--- a/Allura/allura/tests/functional/test_feeds.py
+++ b/Allura/allura/tests/functional/test_feeds.py
@@ -91,7 +91,7 @@ class TestFeeds(TestController):
             summary='This is a new ticket',
             status='unread',
             milestone='',
-            description='This is another description'), extra_environ=dict(username=str('root')))
+            description='This is another description'), extra_environ=dict(username='root'))
         r = self.app.get('/bugs/1/feed.atom')
         assert '=&amp;gt' in r
         assert '\n+' in r
diff --git a/Allura/allura/tests/functional/test_home.py b/Allura/allura/tests/functional/test_home.py
index fe02ffb..0a0a5fc 100644
--- a/Allura/allura/tests/functional/test_home.py
+++ b/Allura/allura/tests/functional/test_home.py
@@ -18,7 +18,6 @@
 import json
 import re
 import os
-from io import open
 
 from tg import tmpl_context as c
 from alluratest.tools import assert_equal, assert_not_in, assert_in
@@ -28,8 +27,6 @@ import allura
 from allura.tests import TestController
 from allura.tests import decorators as td
 from allura import model as M
-from six.moves import range
-from six.moves import zip
 
 
 class TestProjectHome(TestController):
@@ -224,13 +221,13 @@ class TestProjectHome(TestController):
 
     def test_members_anonymous(self):
         r = self.app.get('/p/test/_members/',
-                         extra_environ=dict(username=str('*anonymous')))
+                         extra_environ=dict(username='*anonymous'))
         assert '<td>Test Admin</td>' in r
         assert '<td><a href="/u/test-admin/">test-admin</a></td>' in r
         assert '<td>Admin</td>' in r
 
     def test_toolaccess_before_subproject(self):
-        self.app.extra_environ = {'username': str('test-admin')}
+        self.app.extra_environ = {'username': 'test-admin'}
         # Add the subproject with a wiki.
         self.app.post('/p/test/admin/update_mounts', params={
             'new.install': 'install',
@@ -261,7 +258,7 @@ class TestProjectHome(TestController):
         })
 
         # Try to access the  installed tool as anon.
-        r = self.app.get('/p/test/test-mount/test-sub/', extra_environ=dict(username=str('*anonymous')), status=404)
+        r = self.app.get('/p/test/test-mount/test-sub/', extra_environ=dict(username='*anonymous'), status=404)
 
         # Try to access the installed tool as Admin.
         r = self.app.get('/p/test/test-mount/test-sub/').follow()
diff --git a/Allura/allura/tests/functional/test_nav.py b/Allura/allura/tests/functional/test_nav.py
index cde61cd..3477981 100644
--- a/Allura/allura/tests/functional/test_nav.py
+++ b/Allura/allura/tests/functional/test_nav.py
@@ -32,7 +32,7 @@ class TestNavigation(TestController):
     """
 
     def setUp(self):
-        super(TestNavigation, self).setUp()
+        super().setUp()
         self.logo_pattern = ('div', {'class': 'nav-logo'})
         self.global_nav_pattern = ('nav', {'class': 'nav-left'})
         self.nav_data = {
@@ -102,7 +102,7 @@ class TestNavigation(TestController):
         width = self.width % self.logo_data["width"]
         height = self.height % self.logo_data["height"]
         assert nav_logo.find(
-            'img', style='{} {}'.format(width, height)) is not None
+            'img', style=f'{width} {height}') is not None
 
     def test_missing_logo_width(self):
         self.logo_data = {
diff --git a/Allura/allura/tests/functional/test_neighborhood.py b/Allura/allura/tests/functional/test_neighborhood.py
index 41f2659..82ab2a0 100644
--- a/Allura/allura/tests/functional/test_neighborhood.py
+++ b/Allura/allura/tests/functional/test_neighborhood.py
@@ -1,4 +1,3 @@
-# coding=utf-8
 #       Licensed to the Apache Software Foundation (ASF) under one
 #       or more contributor license agreements.  See the NOTICE file
 #       distributed with this work for additional information
@@ -22,7 +21,6 @@ from io import BytesIO
 import six.moves.urllib.parse
 import six.moves.urllib.request
 import six.moves.urllib.error
-from io import open
 
 import PIL
 from mock import patch
@@ -39,7 +37,6 @@ from allura.tests import decorators as td
 from allura.lib import helpers as h
 from allura.lib import utils
 from alluratest.controller import setup_trove_categories
-from six.moves import map
 
 
 class TestNeighborhood(TestController):
@@ -49,13 +46,13 @@ class TestNeighborhood(TestController):
         r = r.follow()
         assert 'This is the "Adobe" neighborhood' in str(r), str(r)
         r = self.app.get(
-            '/adobe/admin/', extra_environ=dict(username=str('test-user')),
+            '/adobe/admin/', extra_environ=dict(username='test-user'),
             status=403)
 
     def test_redirect(self):
         r = self.app.post('/adobe/_admin/update',
                           params=dict(redirect='wiki/Home/'),
-                          extra_environ=dict(username=str('root')))
+                          extra_environ=dict(username='root'))
         r = self.app.get('/adobe/')
         assert r.location.endswith('/adobe/wiki/Home/')
 
@@ -65,25 +62,25 @@ class TestNeighborhood(TestController):
         assert 'This is the "Adobe" neighborhood' in str(r), str(r)
 
     def test_admin(self):
-        r = self.app.get('/adobe/_admin/', extra_environ=dict(username=str('root')))
+        r = self.app.get('/adobe/_admin/', extra_environ=dict(username='root'))
         r = self.app.get('/adobe/_admin/overview',
-                         extra_environ=dict(username=str('root')))
+                         extra_environ=dict(username='root'))
         r = self.app.get('/adobe/_admin/accolades',
-                         extra_environ=dict(username=str('root')))
+                         extra_environ=dict(username='root'))
         neighborhood = M.Neighborhood.query.get(name='Adobe')
         neighborhood.features['google_analytics'] = True
         r = self.app.post('/adobe/_admin/update',
                           params=dict(name='Mozq1', css='',
                                       homepage='# MozQ1!', tracking_id='U-123456'),
-                          extra_environ=dict(username=str('root')))
+                          extra_environ=dict(username='root'))
         r = self.app.post('/adobe/_admin/update',
                           params=dict(name='Mozq1', css='',
                                       homepage='# MozQ1!\n[Root]'),
-                          extra_environ=dict(username=str('root')))
+                          extra_environ=dict(username='root'))
         # make sure project_template is validated as proper json
         r = self.app.post('/adobe/_admin/update',
                           params=dict(project_template='{'),
-                          extra_environ=dict(username=str('root')))
+                          extra_environ=dict(username='root'))
         assert 'Invalid JSON' in r
 
     def test_admin_overview_audit_log(self):
@@ -109,7 +106,7 @@ class TestNeighborhood(TestController):
 
         }
         self.app.post('/p/_admin/update', params=params,
-                      extra_environ=dict(username=str('root')))
+                      extra_environ=dict(username='root'))
         # must get as many log records as many values are updated
         assert M.AuditLog.query.find().count() == len(params)
 
@@ -131,9 +128,9 @@ class TestNeighborhood(TestController):
         self.app.post('/p/_admin/update',
                       params=dict(name='Projects',
                                   prohibited_tools='wiki, tickets'),
-                      extra_environ=dict(username=str('root')))
+                      extra_environ=dict(username='root'))
 
-        r = self.app.get('/p/_admin/overview', extra_environ=dict(username=str('root')))
+        r = self.app.get('/p/_admin/overview', extra_environ=dict(username='root'))
         assert 'wiki, tickets' in r
 
         c.user = M.User.query.get(username='root')
@@ -146,7 +143,7 @@ class TestNeighborhood(TestController):
         r = self.app.post('/p/_admin/update',
                           params=dict(name='Projects',
                                       prohibited_tools='wiki, test'),
-                          extra_environ=dict(username=str('root')))
+                          extra_environ=dict(username='root'))
         assert 'error' in self.webflash(r), self.webflash(r)
 
     @td.with_wiki
@@ -156,26 +153,26 @@ class TestNeighborhood(TestController):
         r = self.app.post('/p/_admin/update',
                           params=dict(name='Projects',
                                       anchored_tools='wiki:Wiki, tickets:Ticket'),
-                          extra_environ=dict(username=str('root')))
+                          extra_environ=dict(username='root'))
         assert 'error' not in self.webflash(r)
         r = self.app.post('/p/_admin/update',
                           params=dict(name='Projects',
                                       anchored_tools='w!iki:Wiki, tickets:Ticket'),
-                          extra_environ=dict(username=str('root')))
+                          extra_environ=dict(username='root'))
         assert 'error' in self.webflash(r)
         assert_equal(neighborhood.anchored_tools, 'wiki:Wiki, tickets:Ticket')
 
         r = self.app.post('/p/_admin/update',
                           params=dict(name='Projects',
                                       anchored_tools='wiki:Wiki,'),
-                          extra_environ=dict(username=str('root')))
+                          extra_environ=dict(username='root'))
         assert 'error' in self.webflash(r)
         assert_equal(neighborhood.anchored_tools, 'wiki:Wiki, tickets:Ticket')
 
         r = self.app.post('/p/_admin/update',
                           params=dict(name='Projects',
                                       anchored_tools='badname,'),
-                          extra_environ=dict(username=str('root')))
+                          extra_environ=dict(username='root'))
         assert 'error' in self.webflash(r)
         assert_equal(neighborhood.anchored_tools, 'wiki:Wiki, tickets:Ticket')
 
@@ -195,7 +192,7 @@ class TestNeighborhood(TestController):
 
     def test_show_title(self):
         r = self.app.get('/adobe/_admin/overview',
-                         extra_environ=dict(username=str('root')))
+                         extra_environ=dict(username='root'))
         neighborhood = M.Neighborhood.query.get(name='Adobe')
         # if not set show_title must be True
         assert neighborhood.show_title
@@ -206,17 +203,17 @@ class TestNeighborhood(TestController):
                                       homepage='# MozQ1!',
                                       tracking_id='U-123456',
                                       show_title='false'),
-                          extra_environ=dict(username=str('root')))
+                          extra_environ=dict(username='root'))
         # no title now
-        r = self.app.get('/adobe/', extra_environ=dict(username=str('root')))
+        r = self.app.get('/adobe/', extra_environ=dict(username='root'))
         assert 'class="project_title"' not in str(r)
         r = self.app.get('/adobe/wiki/Home/',
-                         extra_environ=dict(username=str('root')))
+                         extra_environ=dict(username='root'))
         assert 'class="project_title"' not in str(r)
 
         # title must be present on project page
         r = self.app.get('/adobe/adobe-1/admin/',
-                         extra_environ=dict(username=str('root')))
+                         extra_environ=dict(username='root'))
         assert 'class="project_title"' in str(r)
 
     def test_admin_stats_del_count(self):
@@ -225,7 +222,7 @@ class TestNeighborhood(TestController):
         proj.deleted = True
         ThreadLocalORMSession.flush_all()
         r = self.app.get('/adobe/_admin/stats/',
-                         extra_environ=dict(username=str('root')))
+                         extra_environ=dict(username='root'))
         assert 'Deleted: 1' in r
         assert 'Private: 0' in r
 
@@ -236,7 +233,7 @@ class TestNeighborhood(TestController):
         proj.private = True
         ThreadLocalORMSession.flush_all()
         r = self.app.get('/adobe/_admin/stats/',
-                         extra_environ=dict(username=str('root')))
+                         extra_environ=dict(username='root'))
         assert 'Deleted: 0' in r
         assert 'Private: 1' in r
 
@@ -246,7 +243,7 @@ class TestNeighborhood(TestController):
         proj.private = False
         ThreadLocalORMSession.flush_all()
         r = self.app.get('/adobe/_admin/stats/adminlist',
-                         extra_environ=dict(username=str('root')))
+                         extra_environ=dict(username='root'))
         pq = M.Project.query.find(
             dict(neighborhood_id=neighborhood._id, deleted=False))
         pq.sort('name')
@@ -270,11 +267,11 @@ class TestNeighborhood(TestController):
         file_data = open(file_path, 'rb').read()
         upload = ('icon', file_name, file_data)
 
-        r = self.app.get('/adobe/_admin/', extra_environ=dict(username=str('root')))
+        r = self.app.get('/adobe/_admin/', extra_environ=dict(username='root'))
         r = self.app.post('/adobe/_admin/update',
                           params=dict(name='Mozq1', css='',
                                       homepage='# MozQ1'),
-                          extra_environ=dict(username=str('root')), upload_files=[upload])
+                          extra_environ=dict(username='root'), upload_files=[upload])
         r = self.app.get('/adobe/icon')
         image = PIL.Image.open(BytesIO(r.body))
         assert image.size == (48, 48)
@@ -286,33 +283,33 @@ class TestNeighborhood(TestController):
         neighborhood = M.Neighborhood.query.get(name='Adobe')
         neighborhood.features['google_analytics'] = True
         r = self.app.get('/adobe/_admin/overview',
-                         extra_environ=dict(username=str('root')))
+                         extra_environ=dict(username='root'))
         assert 'Google Analytics ID' in r
         r = self.app.get('/adobe/adobe-1/admin/overview',
-                         extra_environ=dict(username=str('root')))
+                         extra_environ=dict(username='root'))
         assert 'Google Analytics ID' in r
         r = self.app.post('/adobe/_admin/update',
                           params=dict(name='Adobe', css='',
                                       homepage='# MozQ1', tracking_id='U-123456'),
-                          extra_environ=dict(username=str('root')), status=302)
+                          extra_environ=dict(username='root'), status=302)
         r = self.app.post('/adobe/adobe-1/admin/update',
                           params=dict(tracking_id='U-654321'),
-                          extra_environ=dict(username=str('root')), status=302)
+                          extra_environ=dict(username='root'), status=302)
         r = self.app.get('/adobe/adobe-1/admin/overview',
-                         extra_environ=dict(username=str('root')))
+                         extra_environ=dict(username='root'))
         assert "_add_tracking('nbhd', 'U-123456');" in r, r
         assert "_add_tracking('proj', 'U-654321');" in r
         # analytics not allowed
         neighborhood = M.Neighborhood.query.get(name='Adobe')
         neighborhood.features['google_analytics'] = False
         r = self.app.get('/adobe/_admin/overview',
-                         extra_environ=dict(username=str('root')))
+                         extra_environ=dict(username='root'))
         assert 'Google Analytics ID' not in r
         r = self.app.get('/adobe/adobe-1/admin/overview',
-                         extra_environ=dict(username=str('root')))
+                         extra_environ=dict(username='root'))
         assert 'Google Analytics ID' not in r
         r = self.app.get('/adobe/adobe-1/admin/overview',
-                         extra_environ=dict(username=str('root')))
+                         extra_environ=dict(username='root'))
         assert "_add_tracking('nbhd', 'U-123456');" not in r
         assert "_add_tracking('proj', 'U-654321');" not in r
 
@@ -326,7 +323,7 @@ class TestNeighborhood(TestController):
         r = self.app.get('/adobe/')
         assert test_css not in r
         r = self.app.get('/adobe/_admin/overview',
-                         extra_environ=dict(username=str('root')))
+                         extra_environ=dict(username='root'))
         assert custom_css not in r
 
         neighborhood = M.Neighborhood.query.get(name='Adobe')
@@ -336,7 +333,7 @@ class TestNeighborhood(TestController):
             r = r.follow()
         assert test_css in r
         r = self.app.get('/adobe/_admin/overview',
-                         extra_environ=dict(username=str('root')))
+                         extra_environ=dict(username='root'))
         assert custom_css in r
 
         neighborhood = M.Neighborhood.query.get(name='Adobe')
@@ -346,7 +343,7 @@ class TestNeighborhood(TestController):
             r = r.follow()
         assert test_css in r
         r = self.app.get('/adobe/_admin/overview',
-                         extra_environ=dict(username=str('root')))
+                         extra_environ=dict(username='root'))
         assert custom_css in r
 
     def test_picker_css(self):
@@ -354,7 +351,7 @@ class TestNeighborhood(TestController):
         neighborhood.features['css'] = 'picker'
 
         r = self.app.get('/adobe/_admin/overview',
-                         extra_environ=dict(username=str('root')))
+                         extra_environ=dict(username='root'))
         assert 'Project title, font' in r
         assert 'Project title, color' in r
         assert 'Bar on top' in r
@@ -370,7 +367,7 @@ class TestNeighborhood(TestController):
                                   'css-barontop': '#555555',
                                   'css-titlebarbackground': '#333',
                                   'css-titlebarcolor': '#444'},
-                          extra_environ=dict(username=str('root')), upload_files=[])
+                          extra_environ=dict(username='root'), upload_files=[])
         neighborhood = M.Neighborhood.query.get(name='Adobe')
         assert '/*projecttitlefont*/.project_title{font-family:arial,sans-serif;}' in neighborhood.css
         assert '/*projecttitlecolor*/.project_title{color:green;}' in neighborhood.css
@@ -387,7 +384,7 @@ class TestNeighborhood(TestController):
                               project_unixname='maxproject1', project_name='Max project1',
                               project_description='', neighborhood='Projects'),
                           antispam=True,
-                          extra_environ=dict(username=str('root')), status=302)
+                          extra_environ=dict(username='root'), status=302)
         assert '/p/maxproject1/admin' in r.location
 
         # Set max value to 0
@@ -398,7 +395,7 @@ class TestNeighborhood(TestController):
                               project_unixname='maxproject2', project_name='Max project2',
                               project_description='', neighborhood='Projects'),
                           antispam=True,
-                          extra_environ=dict(username=str('root')))
+                          extra_environ=dict(username='root'))
         while isinstance(r.response, HTTPFound):
             r = r.follow()
         assert 'You have exceeded the maximum number of projects' in r
@@ -411,7 +408,7 @@ class TestNeighborhood(TestController):
                                   project_unixname='rateproject1', project_name='Rate project1',
                                   project_description='', neighborhood='Projects'),
                               antispam=True,
-                              extra_environ=dict(username=str('test-user-1')), status=302)
+                              extra_environ=dict(username='test-user-1'), status=302)
             assert '/p/rateproject1/admin' in r.location
 
         # Set rate limit to 1 in first hour of user account
@@ -421,7 +418,7 @@ class TestNeighborhood(TestController):
                                   project_unixname='rateproject2', project_name='Rate project2',
                                   project_description='', neighborhood='Projects'),
                               antispam=True,
-                              extra_environ=dict(username=str('test-user-1')))
+                              extra_environ=dict(username='test-user-1'))
             while isinstance(r.response, HTTPFound):
                 r = r.follow()
             assert 'Project creation rate limit exceeded.  Please try again later.' in r
@@ -434,7 +431,7 @@ class TestNeighborhood(TestController):
                                   project_unixname='rateproject1', project_name='Rate project1',
                                   project_description='', neighborhood='Projects'),
                               antispam=True,
-                              extra_environ=dict(username=str('root')), status=302)
+                              extra_environ=dict(username='root'), status=302)
             assert '/p/rateproject1/admin' in r.location
 
         # Set rate limit to 1 in first hour of user account
@@ -444,71 +441,71 @@ class TestNeighborhood(TestController):
                                   project_unixname='rateproject2', project_name='Rate project2',
                                   project_description='', neighborhood='Projects'),
                               antispam=True,
-                              extra_environ=dict(username=str('root')))
+                              extra_environ=dict(username='root'))
             assert '/p/rateproject2/admin' in r.location
 
     def test_invite(self):
         p_nbhd_id = str(M.Neighborhood.query.get(name='Projects')._id)
         r = self.app.get('/adobe/_moderate/',
-                         extra_environ=dict(username=str('root')))
+                         extra_environ=dict(username='root'))
         r = self.app.post('/adobe/_moderate/invite',
                           params=dict(pid='adobe-1', invite='on',
                                       neighborhood_id=p_nbhd_id),
-                          extra_environ=dict(username=str('root')))
-        r = self.app.get(r.location, extra_environ=dict(username=str('root')))
+                          extra_environ=dict(username='root'))
+        r = self.app.get(r.location, extra_environ=dict(username='root'))
         assert 'error' in r
         r = self.app.post('/adobe/_moderate/invite',
                           params=dict(pid='no_such_user',
                                       invite='on', neighborhood_id=p_nbhd_id),
-                          extra_environ=dict(username=str('root')))
-        r = self.app.get(r.location, extra_environ=dict(username=str('root')))
+                          extra_environ=dict(username='root'))
+        r = self.app.get(r.location, extra_environ=dict(username='root'))
         assert 'error' in r
         r = self.app.post('/adobe/_moderate/invite',
                           params=dict(pid='test', invite='on',
                                       neighborhood_id=p_nbhd_id),
-                          extra_environ=dict(username=str('root')))
-        r = self.app.get(r.location, extra_environ=dict(username=str('root')))
+                          extra_environ=dict(username='root'))
+        r = self.app.get(r.location, extra_environ=dict(username='root'))
         assert 'invited' in r, r
         assert 'warning' not in r
         r = self.app.post('/adobe/_moderate/invite',
                           params=dict(pid='test', invite='on',
                                       neighborhood_id=p_nbhd_id),
-                          extra_environ=dict(username=str('root')))
-        r = self.app.get(r.location, extra_environ=dict(username=str('root')))
+                          extra_environ=dict(username='root'))
+        r = self.app.get(r.location, extra_environ=dict(username='root'))
         assert 'warning' in r
         r = self.app.post('/adobe/_moderate/invite',
                           params=dict(pid='test', uninvite='on',
                                       neighborhood_id=p_nbhd_id),
-                          extra_environ=dict(username=str('root')))
-        r = self.app.get(r.location, extra_environ=dict(username=str('root')))
+                          extra_environ=dict(username='root'))
+        r = self.app.get(r.location, extra_environ=dict(username='root'))
         assert 'uninvited' in r
         assert 'warning' not in r
         r = self.app.post('/adobe/_moderate/invite',
                           params=dict(pid='test', uninvite='on',
                                       neighborhood_id=p_nbhd_id),
-                          extra_environ=dict(username=str('root')))
-        r = self.app.get(r.location, extra_environ=dict(username=str('root')))
+                          extra_environ=dict(username='root'))
+        r = self.app.get(r.location, extra_environ=dict(username='root'))
         assert 'warning' in r
         r = self.app.post('/adobe/_moderate/invite',
                           params=dict(pid='test', invite='on',
                                       neighborhood_id=p_nbhd_id),
-                          extra_environ=dict(username=str('root')))
-        r = self.app.get(r.location, extra_environ=dict(username=str('root')))
+                          extra_environ=dict(username='root'))
+        r = self.app.get(r.location, extra_environ=dict(username='root'))
         assert 'invited' in r
         assert 'warning' not in r
 
     def test_evict(self):
         r = self.app.get('/adobe/_moderate/',
-                         extra_environ=dict(username=str('root')))
+                         extra_environ=dict(username='root'))
         r = self.app.post('/adobe/_moderate/evict',
                           params=dict(pid='test'),
-                          extra_environ=dict(username=str('root')))
-        r = self.app.get(r.location, extra_environ=dict(username=str('root')))
+                          extra_environ=dict(username='root'))
+        r = self.app.get(r.location, extra_environ=dict(username='root'))
         assert 'error' in r
         r = self.app.post('/adobe/_moderate/evict',
                           params=dict(pid='adobe-1'),
-                          extra_environ=dict(username=str('root')))
-        r = self.app.get(r.location, extra_environ=dict(username=str('root')))
+                          extra_environ=dict(username='root'))
+        r = self.app.get(r.location, extra_environ=dict(username='root'))
         assert 'adobe-1 evicted to Projects' in r
 
     def test_home(self):
@@ -521,7 +518,7 @@ class TestNeighborhood(TestController):
                               project_unixname='', project_name='Nothing',
                               project_description='', neighborhood='Adobe'),
                           antispam=True,
-                          extra_environ=dict(username=str('root')))
+                          extra_environ=dict(username='root'))
         assert r.html.find('div', {'class': 'error'}
                            ).string == 'Please use 3-15 small letters, numbers, and dashes.'
         r = self.app.post('/adobe/register',
@@ -529,14 +526,14 @@ class TestNeighborhood(TestController):
                               project_unixname='mymoz', project_name='My Moz',
                               project_description='', neighborhood='Adobe'),
                           antispam=True,
-                          extra_environ=dict(username=str('*anonymous')),
+                          extra_environ=dict(username='*anonymous'),
                           status=302)
         r = self.app.post('/adobe/register',
                           params=dict(
                               project_unixname='foo.mymoz', project_name='My Moz',
                               project_description='', neighborhood='Adobe'),
                           antispam=True,
-                          extra_environ=dict(username=str('root')))
+                          extra_environ=dict(username='root'))
         assert r.html.find('div', {'class': 'error'}
                            ).string == 'Please use 3-15 small letters, numbers, and dashes.'
         r = self.app.post('/p/register',
@@ -544,7 +541,7 @@ class TestNeighborhood(TestController):
                               project_unixname='test', project_name='Tester',
                               project_description='', neighborhood='Projects'),
                           antispam=True,
-                          extra_environ=dict(username=str('root')))
+                          extra_environ=dict(username='root'))
         assert r.html.find('div', {'class': 'error'}
                            ).string == 'This project name is taken.'
         r = self.app.post('/adobe/register',
@@ -552,7 +549,7 @@ class TestNeighborhood(TestController):
                               project_unixname='mymoz', project_name='My Moz',
                               project_description='', neighborhood='Adobe'),
                           antispam=True,
-                          extra_environ=dict(username=str('root')),
+                          extra_environ=dict(username='root'),
                           status=302)
 
     def test_register_private_fails_for_anon(self):
@@ -565,7 +562,7 @@ class TestNeighborhood(TestController):
                 neighborhood='Projects',
                 private_project='on'),
             antispam=True,
-            extra_environ=dict(username=str('*anonymous')),
+            extra_environ=dict(username='*anonymous'),
             status=302)
         assert config.get('auth.login_url', '/auth/') in r.location, r.location
 
@@ -579,14 +576,14 @@ class TestNeighborhood(TestController):
                 neighborhood='Projects',
                 private_project='on'),
             antispam=True,
-            extra_environ=dict(username=str('test-user')),
+            extra_environ=dict(username='test-user'),
             status=403)
 
     def test_register_private_fails_for_non_private_neighborhood(self):
         # Turn off private
         neighborhood = M.Neighborhood.query.get(name='Projects')
         neighborhood.features['private_projects'] = False
-        r = self.app.get('/p/add_project', extra_environ=dict(username=str('root')))
+        r = self.app.get('/p/add_project', extra_environ=dict(username='root'))
         assert 'private_project' not in r
 
         r = self.app.post(
@@ -598,7 +595,7 @@ class TestNeighborhood(TestController):
                 neighborhood='Projects',
                 private_project='on'),
             antispam=True,
-            extra_environ=dict(username=str('root')))
+            extra_environ=dict(username='root'))
         cookies = r.headers.getall('Set-Cookie')
         flash_msg_cookies = list(map(six.moves.urllib.parse.unquote, cookies))
 
@@ -611,7 +608,7 @@ class TestNeighborhood(TestController):
         # Turn on private
         neighborhood = M.Neighborhood.query.get(name='Projects')
         neighborhood.features['private_projects'] = True
-        r = self.app.get('/p/add_project', extra_environ=dict(username=str('root')))
+        r = self.app.get('/p/add_project', extra_environ=dict(username='root'))
         assert 'private_project' in r
 
         self.app.post(
@@ -623,7 +620,7 @@ class TestNeighborhood(TestController):
                 neighborhood='Projects',
                 private_project='on'),
             antispam=True,
-            extra_environ=dict(username=str('root')))
+            extra_environ=dict(username='root'))
 
         proj = M.Project.query.get(
             shortname='myprivate2', neighborhood_id=neighborhood._id)
@@ -640,21 +637,21 @@ class TestNeighborhood(TestController):
                 private_project='on',
                 tools='wiki'),
             antispam=True,
-            extra_environ=dict(username=str('root')),
+            extra_environ=dict(username='root'),
             status=302)
         assert config.get('auth.login_url',
                           '/auth/') not in r.location, r.location
         r = self.app.get(
             '/p/mymoz/wiki/',
-            extra_environ=dict(username=str('root'))).follow(extra_environ=dict(username=str('root')), status=200)
+            extra_environ=dict(username='root')).follow(extra_environ=dict(username='root'), status=200)
         r = self.app.get(
             '/p/mymoz/wiki/',
-            extra_environ=dict(username=str('*anonymous')),
+            extra_environ=dict(username='*anonymous'),
             status=302)
         assert config.get('auth.login_url', '/auth/') in r.location, r.location
         self.app.get(
             '/p/mymoz/wiki/',
-            extra_environ=dict(username=str('test-user')),
+            extra_environ=dict(username='test-user'),
             status=403)
 
     def test_project_template(self):
@@ -717,7 +714,7 @@ class TestNeighborhood(TestController):
                 },
                 "groups": %s
                 }""" % (icon_url, json.dumps(test_groups))),
-            extra_environ=dict(username=str('root')))
+            extra_environ=dict(username='root'))
         r = self.app.post(
             '/adobe/register',
             params=dict(
@@ -727,7 +724,7 @@ class TestNeighborhood(TestController):
                 neighborhood='Mozq1',
                 private_project='off'),
             antispam=True,
-            extra_environ=dict(username=str('root')),
+            extra_environ=dict(username='root'),
             status=302).follow()
         p = M.Project.query.get(shortname='testtemp')
         # make sure the correct tools got installed in the right order
@@ -743,10 +740,10 @@ class TestNeighborhood(TestController):
         # make sure project is private
         r = self.app.get(
             '/adobe/testtemp/wiki/',
-            extra_environ=dict(username=str('root'))).follow(extra_environ=dict(username=str('root')), status=200)
+            extra_environ=dict(username='root')).follow(extra_environ=dict(username='root'), status=200)
         r = self.app.get(
             '/adobe/testtemp/wiki/',
-            extra_environ=dict(username=str('*anonymous')),
+            extra_environ=dict(username='*anonymous'),
             status=302)
         # check the labels and trove cats
         r = self.app.get('/adobe/testtemp/admin/trove')
@@ -812,7 +809,7 @@ class TestNeighborhood(TestController):
                 "tool_order":["wiki","admin"],
 
                 }"""),
-                          extra_environ=dict(username=str('root')))
+                          extra_environ=dict(username='root'))
         neighborhood = M.Neighborhood.query.get(name='Adobe')
         neighborhood.anchored_tools = 'wiki:Wiki'
         r = self.app.post(
@@ -824,7 +821,7 @@ class TestNeighborhood(TestController):
                 neighborhood='Adobe',
                 private_project='off'),
             antispam=True,
-            extra_environ=dict(username=str('root')))
+            extra_environ=dict(username='root'))
         r = self.app.get('/adobe/testtemp/admin/overview')
         assert r.html.find('div', id='top_nav').find(
             'a', href='/adobe/testtemp/wiki/'), r.html
@@ -861,7 +858,7 @@ class TestNeighborhood(TestController):
                               project_unixname='test', project_name='Test again',
                               project_description='', neighborhood='Adobe', tools='wiki'),
                           antispam=True,
-                          extra_environ=dict(username=str('root')))
+                          extra_environ=dict(username='root'))
         assert r.status_int == 302, r.html.find(
             'div', {'class': 'error'}).string
         assert not r.location.endswith('/add_project'), self.webflash(r)
@@ -875,42 +872,42 @@ class TestNeighborhood(TestController):
         upload = ('icon', file_name, file_data)
 
         r = self.app.get('/adobe/_admin/awards',
-                         extra_environ=dict(username=str('root')))
+                         extra_environ=dict(username='root'))
         r = self.app.post('/adobe/_admin/awards/create',
                           params=dict(short='FOO', full='A basic foo award'),
-                          extra_environ=dict(username=str('root')), upload_files=[upload])
+                          extra_environ=dict(username='root'), upload_files=[upload])
         r = self.app.post('/adobe/_admin/awards/create',
                           params=dict(short='BAR',
                                       full='A basic bar award with no icon'),
-                          extra_environ=dict(username=str('root')))
+                          extra_environ=dict(username='root'))
         foo_id = str(M.Award.query.find(dict(short='FOO')).first()._id)
         bar_id = str(M.Award.query.find(dict(short='BAR')).first()._id)
         r = self.app.post('/adobe/_admin/awards/%s/update' % bar_id,
                           params=dict(short='BAR2',
                                       full='Updated description.'),
-                          extra_environ=dict(username=str('root'))).follow().follow()
+                          extra_environ=dict(username='root')).follow().follow()
         assert 'BAR2' in r
         assert 'Updated description.' in r
         r = self.app.get('/adobe/_admin/awards/%s' %
-                         foo_id, extra_environ=dict(username=str('root')))
+                         foo_id, extra_environ=dict(username='root'))
         r = self.app.get('/adobe/_admin/awards/%s/icon' %
-                         foo_id, extra_environ=dict(username=str('root')))
+                         foo_id, extra_environ=dict(username='root'))
         image = PIL.Image.open(BytesIO(r.body))
         assert image.size == (48, 48)
         self.app.post('/adobe/_admin/awards/grant',
                       params=dict(grant='FOO', recipient='adobe-1',
                                   url='http://award.org', comment='Winner!'),
-                      extra_environ=dict(username=str('root')))
+                      extra_environ=dict(username='root'))
         r = self.app.get('/adobe/_admin/accolades',
-                         extra_environ=dict(username=str('root')))
+                         extra_environ=dict(username='root'))
         assert_in('Winner!', r)
         assert_in('http://award.org', r)
         self.app.get('/adobe/_admin/awards/%s/adobe-1' %
-                     foo_id, extra_environ=dict(username=str('root')))
+                     foo_id, extra_environ=dict(username='root'))
         self.app.post('/adobe/_admin/awards/%s/adobe-1/revoke' % foo_id,
-                      extra_environ=dict(username=str('root')))
+                      extra_environ=dict(username='root'))
         self.app.post('/adobe/_admin/awards/%s/delete' % foo_id,
-                      extra_environ=dict(username=str('root')))
+                      extra_environ=dict(username='root'))
 
     def test_add_a_project_link(self):
         from tg import tmpl_context as c
@@ -921,24 +918,24 @@ class TestNeighborhood(TestController):
                 p.install_app('home', 'home', 'Home', ordinal=0)
         r = self.app.get('/p/')
         assert 'Add a Project' in r
-        r = self.app.get('/u/', extra_environ=dict(username=str('test-user')))
+        r = self.app.get('/u/', extra_environ=dict(username='test-user'))
         assert 'Add a Project' not in r
-        r = self.app.get('/adobe/', extra_environ=dict(username=str('test-user')))
+        r = self.app.get('/adobe/', extra_environ=dict(username='test-user'))
         assert 'Add a Project' not in r
-        r = self.app.get('/u/', extra_environ=dict(username=str('root')))
+        r = self.app.get('/u/', extra_environ=dict(username='root'))
         assert 'Add a Project' in r
-        r = self.app.get('/adobe/', extra_environ=dict(username=str('root')))
+        r = self.app.get('/adobe/', extra_environ=dict(username='root'))
         assert 'Add a Project' in r
 
     def test_help(self):
         r = self.app.get('/p/_admin/help/',
-                         extra_environ=dict(username=str('root')))
+                         extra_environ=dict(username='root'))
         assert 'macro' in r
 
     @td.with_user_project('test-user')
     def test_profile_tools(self):
         r = self.app.get('/u/test-user/',
-                         extra_environ=dict(username=str('test-user'))).follow()
+                         extra_environ=dict(username='test-user')).follow()
         assert r.html.select('div.profile-section.tools a[href="/u/test-user/profile/"]'), r.html
 
     def test_user_project_creates_on_demand(self):
@@ -951,9 +948,9 @@ class TestNeighborhood(TestController):
         self.app.get('/u/donald-duck/')  # assert it's there
         M.User.query.update(dict(username='donald-duck'),
                             {'$set': {'disabled': True}})
-        self.app.get('/u/donald-duck/', status=404, extra_environ={'username': str('*anonymous')})
-        self.app.get('/u/donald-duck/', status=404, extra_environ={'username': str('test-user')})
-        self.app.get('/u/donald-duck/', status=302, extra_environ={'username': str('test-admin')})  # site admin user
+        self.app.get('/u/donald-duck/', status=404, extra_environ={'username': '*anonymous'})
+        self.app.get('/u/donald-duck/', status=404, extra_environ={'username': 'test-user'})
+        self.app.get('/u/donald-duck/', status=302, extra_environ={'username': 'test-admin'})  # site admin user
 
     def test_more_projects_link(self):
         r = self.app.get('/adobe/adobe-1/admin/')
@@ -1091,7 +1088,7 @@ class TestPhoneVerificationOnProjectRegistration(TestController):
                     project_name='Phone Test',
                     project_description='',
                     neighborhood='Projects'),
-                extra_environ=dict(username=str('test-user')),
+                extra_environ=dict(username='test-user'),
                 antispam=True)
             overlay = r.html.find('div', {'id': 'phone_verification_overlay'})
             assert_not_equal(overlay, None)
diff --git a/Allura/allura/tests/functional/test_personal_dashboard.py b/Allura/allura/tests/functional/test_personal_dashboard.py
index 6db35c5..4117041 100644
--- a/Allura/allura/tests/functional/test_personal_dashboard.py
+++ b/Allura/allura/tests/functional/test_personal_dashboard.py
@@ -30,7 +30,6 @@ from allura.tests import TestController
 from allura.tests import decorators as td
 from alluratest.controller import setup_global_objects, setup_unit_test
 from forgetracker.tests.functional.test_root import TrackerTestController
-from six.moves import map
 
 
 class TestPersonalDashboard(TestController):
@@ -87,7 +86,7 @@ class TestTicketsSection(TrackerTestController):
 class TestMergeRequestsSection(TestController):
 
     def setUp(self):
-        super(TestMergeRequestsSection, self).setUp()
+        super().setUp()
         setup_unit_test()
         self.setup_with_tools()
         mr= self.merge_request
diff --git a/Allura/allura/tests/functional/test_rest.py b/Allura/allura/tests/functional/test_rest.py
index fd46bd9..f61a93b 100644
--- a/Allura/allura/tests/functional/test_rest.py
+++ b/Allura/allura/tests/functional/test_rest.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-
 #       Licensed to the Apache Software Foundation (ASF) under one
 #       or more contributor license agreements.  See the NOTICE file
 #       distributed with this work for additional information
@@ -141,7 +139,7 @@ class TestRestHome(TestRestApiBase):
         ThreadLocalODMSession.flush_all()
         token = access_token.api_key
         request.headers = {
-            'Authorization': 'Bearer {}'.format(token)
+            'Authorization': f'Bearer {token}'
         }
         request.scheme = 'https'
         r = self.api_post('/rest/p/test/wiki', access_token='foo', status=200)
@@ -205,7 +203,7 @@ class TestRestHome(TestRestApiBase):
 
         # anonymous sees only non-private tool
         r = self.app.get('/rest/p/test/',
-                         extra_environ={'username': str('*anonymous')})
+                         extra_environ={'username': '*anonymous'})
         assert_equal(r.json['shortname'], 'test')
         tool_mounts = [t['mount_point'] for t in r.json['tools']]
         assert_in('bugs', tool_mounts)
@@ -316,7 +314,7 @@ class TestRestHome(TestRestApiBase):
         self.app.post(
             h.urlquote('/wiki/tést/update'),
             params={
-                'title': 'tést'.encode('utf-8'),
+                'title': 'tést'.encode(),
                 'text': 'sometext',
                 'labels': '',
                 })
@@ -337,10 +335,10 @@ class TestRestHome(TestRestApiBase):
         if auth_read_perm in acl:
             acl.remove(auth_read_perm)
         self.app.get('/rest/p/test/wiki/Home/',
-                     extra_environ={'username': str('*anonymous')},
+                     extra_environ={'username': '*anonymous'},
                      status=401)
         self.app.get('/rest/p/test/wiki/Home/',
-                     extra_environ={'username': str('test-user-0')},
+                     extra_environ={'username': 'test-user-0'},
                      status=403)
 
     def test_index(self):
@@ -372,7 +370,7 @@ class TestRestHome(TestRestApiBase):
     @td.with_wiki
     def test_cors_POST_req_blocked_by_csrf(self):
         # so test-admin isn't automatically logged in for all requests
-        self.app.extra_environ = {'disable_auth_magic': str('True')}
+        self.app.extra_environ = {'disable_auth_magic': 'True'}
 
         # regular login to get a session cookie set up
         r = self.app.get('/auth/')
@@ -384,14 +382,14 @@ class TestRestHome(TestRestApiBase):
         # simulate CORS ajax request withCredentials (cookie headers)
         # make sure we don't allow the cookies to authorize the request (else could be a CSRF attack vector)
         assert self.app.cookies['allura']
-        self.app.post('/rest/p/test/wiki/NewPage', headers={'Origin': str('http://bad.com/')},
+        self.app.post('/rest/p/test/wiki/NewPage', headers={'Origin': 'http://bad.com/'},
                       status=401)
 
     @mock.patch('allura.lib.plugin.ThemeProvider._get_site_notification')
     def test_notification(self, _get_site_notification):
         user = M.User.by_username('test-admin')
         note = M.SiteNotification()
-        cookie = '{}-1-False'.format(note._id)
+        cookie = f'{note._id}-1-False'
         g.theme._get_site_notification = mock.Mock(return_value=(note, cookie))
 
         r = self.app.get('/rest/notification?url=test_url&cookie=test_cookie&tool_name=test_tool')
@@ -414,7 +412,7 @@ class TestRestHome(TestRestApiBase):
 class TestRestNbhdAddProject(TestRestApiBase):
 
     def setUp(self):
-        super(TestRestNbhdAddProject, self).setUp()
+        super().setUp()
         # create some troves we'll need
         M.TroveCategory(fullname="Root", trove_cat_id=1, trove_parent_id=0)
         M.TroveCategory(fullname="License", trove_cat_id=2, trove_parent_id=1)
@@ -608,7 +606,7 @@ class TestDoap(TestRestApiBase):
 
         # anonymous sees only non-private tool
         r = self.app.get('/rest/p/test?doap',
-                         extra_environ={'username': str('*anonymous')})
+                         extra_environ={'username': '*anonymous'})
         p = r.xml.find(self.ns + 'Project')
         tools = p.findall(self.ns_sf + 'feature')
         tools = [(t.find(self.ns_sf + 'Feature').find(self.ns + 'name').text,
diff --git a/Allura/allura/tests/functional/test_root.py b/Allura/allura/tests/functional/test_root.py
index 91e160f..ddfaf98 100644
--- a/Allura/allura/tests/functional/test_root.py
+++ b/Allura/allura/tests/functional/test_root.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-
 #       Licensed to the Apache Software Foundation (ASF) under one
 #       or more contributor license agreements.  See the NOTICE file
 #       distributed with this work for additional information
@@ -48,7 +46,7 @@ from alluratest.controller import setup_trove_categories
 class TestRootController(TestController):
 
     def setUp(self):
-        super(TestRootController, self).setUp()
+        super().setUp()
         n_adobe = M.Neighborhood.query.get(name='Adobe')
         assert n_adobe
         u_admin = M.User.query.get(username='test-admin')
@@ -56,7 +54,7 @@ class TestRootController(TestController):
         n_adobe.register_project('adobe-2', u_admin)
 
     def test_index(self):
-        response = self.app.get('/', extra_environ=dict(username=str('*anonymous')))
+        response = self.app.get('/', extra_environ=dict(username='*anonymous'))
         assert_equal(response.location, 'http://localhost/neighborhood')
 
         response = self.app.get('/')
@@ -196,12 +194,12 @@ class TestRootController(TestController):
 class TestRootWithSSLPattern(TestController):
     def setUp(self):
         with td.patch_middleware_config({'force_ssl.pattern': '^/auth'}):
-            super(TestRootWithSSLPattern, self).setUp()
+            super().setUp()
 
     def test_no_weird_ssl_redirect_for_error_document(self):
         # test a 404, same functionality as a 500 from an error
         r = self.app.get('/auth/asdfasdf',
-                         extra_environ={'wsgi.url_scheme': str('https')},
+                         extra_environ={'wsgi.url_scheme': 'https'},
                          status=404)
         assert '302 Found' not in r.text, r.text
         assert '301 Moved Permanently' not in r.text, r.text
diff --git a/Allura/allura/tests/functional/test_search.py b/Allura/allura/tests/functional/test_search.py
index ddc4a6c..3a1a72b 100644
--- a/Allura/allura/tests/functional/test_search.py
+++ b/Allura/allura/tests/functional/test_search.py
@@ -69,6 +69,6 @@ class TestSearch(TestController):
         resp.mustcontain('Welcome to your wiki! This is the default page')
         resp.mustcontain('Sample wiki comment')
 
-        resp = self.app.get('/p/test2/search/', params=dict(q='wiki'), extra_environ=dict(username=str('*anonymous')))
+        resp = self.app.get('/p/test2/search/', params=dict(q='wiki'), extra_environ=dict(username='*anonymous'))
         resp.mustcontain(no='Welcome to your wiki! This is the default page')
         resp.mustcontain(no='Sample wiki comment')
diff --git a/Allura/allura/tests/functional/test_site_admin.py b/Allura/allura/tests/functional/test_site_admin.py
index a66770f..bd58ca7 100644
--- a/Allura/allura/tests/functional/test_site_admin.py
+++ b/Allura/allura/tests/functional/test_site_admin.py
@@ -1,4 +1,3 @@
-# coding: utf-8
 #       Licensed to the Apache Software Foundation (ASF) under one
 #       or more contributor license agreements.  See the NOTICE file
 #       distributed with this work for additional information
@@ -40,21 +39,21 @@ class TestSiteAdmin(TestController):
 
     def test_access(self):
         r = self.app.get('/nf/admin/', extra_environ=dict(
-            username=str('test-user')), status=403)
+            username='test-user'), status=403)
 
         r = self.app.get('/nf/admin/', extra_environ=dict(
-            username=str('*anonymous')), status=302)
+            username='*anonymous'), status=302)
         r = r.follow()
         assert 'Login' in r
 
     def test_home(self):
         r = self.app.get('/nf/admin/', extra_environ=dict(
-            username=str('root')))
+            username='root'))
         assert 'Site Admin Home' in r
 
     def test_stats(self):
         r = self.app.get('/nf/admin/stats/', extra_environ=dict(
-            username=str('root')))
+            username='root'))
         assert 'Forge Site Admin' in r.html.find(
             'h2', {'class': 'dark title'}).contents[0]
         stats_table = r.html.find('table')
@@ -63,18 +62,18 @@ class TestSiteAdmin(TestController):
 
     def test_tickets_access(self):
         self.app.get('/nf/admin/api_tickets', extra_environ=dict(
-            username=str('test-user')), status=403)
+            username='test-user'), status=403)
 
     def test_new_projects_access(self):
         self.app.get('/nf/admin/new_projects', extra_environ=dict(
-            username=str('test_user')), status=403)
+            username='test_user'), status=403)
         r = self.app.get('/nf/admin/new_projects', extra_environ=dict(
-            username=str('*anonymous')), status=302).follow()
+            username='*anonymous'), status=302).follow()
         assert 'Login' in r
 
     def test_new_projects(self):
         r = self.app.get('/nf/admin/new_projects', extra_environ=dict(
-            username=str('root')))
+            username='root'))
         headers = r.html.find('table').findAll('th')
         assert headers[1].contents[0] == 'Created'
         assert headers[2].contents[0] == 'Shortname'
@@ -87,18 +86,18 @@ class TestSiteAdmin(TestController):
     def test_new_projects_deleted_projects(self):
         '''Deleted projects should not be visible here'''
         r = self.app.get('/nf/admin/new_projects', extra_environ=dict(
-            username=str('root')))
+            username='root'))
         count = len(r.html.find('table').findAll('tr'))
         p = M.Project.query.get(shortname='test')
         p.deleted = True
         ThreadLocalORMSession.flush_all()
         r = self.app.get('/nf/admin/new_projects', extra_environ=dict(
-            username=str('root')))
+            username='root'))
         assert_equal(len(r.html.find('table').findAll('tr')), count - 1)
 
     def test_new_projects_daterange_filtering(self):
         r = self.app.get('/nf/admin/new_projects', extra_environ=dict(
-            username=str('root')))
+            username='root'))
         count = len(r.html.find('table').findAll('tr'))
         assert_equal(count, 7)
 
@@ -111,7 +110,7 @@ class TestSiteAdmin(TestController):
 
     def test_reclone_repo_access(self):
         r = self.app.get('/nf/admin/reclone_repo', extra_environ=dict(
-            username=str('*anonymous')), status=302).follow()
+            username='*anonymous'), status=302).follow()
         assert 'Login' in r
 
     def test_reclone_repo(self):
@@ -124,7 +123,7 @@ class TestSiteAdmin(TestController):
 
     def test_task_list(self):
         r = self.app.get('/nf/admin/task_manager',
-                         extra_environ=dict(username=str('*anonymous')), status=302)
+                         extra_environ=dict(username='*anonymous'), status=302)
         import math
         M.MonQTask.post(math.ceil, (12.5,))
         r = self.app.get('/nf/admin/task_manager?page_num=1')
@@ -135,13 +134,13 @@ class TestSiteAdmin(TestController):
         task = M.MonQTask.post(re.search, ('pattern', 'string'), {'flags': re.I})
         url = '/nf/admin/task_manager/view/%s' % task._id
         r = self.app.get(
-            url, extra_environ=dict(username=str('*anonymous')), status=302)
+            url, extra_environ=dict(username='*anonymous'), status=302)
         r = self.app.get(url)
         assert 're.search' in r, r
         assert '<td>pattern</td>' in r, r
         assert '<td>string</td>' in r, r
         assert '<th class="second-column-headers side-header">flags</th>' in r, r
-        assert '<td>{}</td>'.format(re.I) in r, r
+        assert f'<td>{re.I}</td>' in r, r
         assert 'ready' in r, r
 
         # test resubmit too
@@ -189,9 +188,9 @@ class TestSiteAdminNotifications(TestController):
 
     def test_site_notifications_access(self):
         self.app.get('/nf/admin/site_notifications', extra_environ=dict(
-            username=str('test_user')), status=403)
+            username='test_user'), status=403)
         r = self.app.get('/nf/admin/site_notifications', extra_environ=dict(
-            username=str('*anonymous')), status=302).follow()
+            username='*anonymous'), status=302).follow()
         assert 'Login' in r
 
     def test_site_notifications(self):
@@ -205,7 +204,7 @@ class TestSiteAdminNotifications(TestController):
         assert M.notification.SiteNotification.query.find().count() == 1
 
         r = self.app.get('/nf/admin/site_notifications/', extra_environ=dict(
-            username=str('root')))
+            username='root'))
         table = r.html.find('table')
         headers = table.findAll('th')
         row = table.findAll('td')
@@ -272,7 +271,7 @@ class TestSiteAdminNotifications(TestController):
                                                page_regex='test3',
                                                page_tool_type='test4')
         ThreadLocalORMSession().flush_all()
-        r = self.app.get('/nf/admin/site_notifications/{}/edit'.format(note._id))
+        r = self.app.get(f'/nf/admin/site_notifications/{note._id}/edit')
 
         assert r
         assert 'checked' in r.form['active'].attrs
@@ -305,7 +304,7 @@ class TestSiteAdminNotifications(TestController):
 
         count = M.notification.SiteNotification.query.find().count()
 
-        r = self.app.post('/nf/admin/site_notifications/{}/update'.format(note._id), params=dict(
+        r = self.app.post(f'/nf/admin/site_notifications/{note._id}/update', params=dict(
             active=active,
             impressions=impressions,
             content=content,
@@ -333,7 +332,7 @@ class TestSiteAdminNotifications(TestController):
 
         count = M.notification.SiteNotification.query.find().count()
 
-        self.app.post('/nf/admin/site_notifications/{}/delete'.format(note._id))
+        self.app.post(f'/nf/admin/site_notifications/{note._id}/delete')
         assert M.notification.SiteNotification.query.find().count() == count -1
         assert M.notification.SiteNotification.query.get(_id=bson.ObjectId(note._id)) is None
 
@@ -359,7 +358,7 @@ class TestProjectsSearch(TestController):
     }])
 
     def setUp(self):
-        super(TestProjectsSearch, self).setUp()
+        super().setUp()
         # Create project that matches TEST_HIT id
         _id = ObjectId('53ccf6e8100d2b0741746e9f')
         p = M.Project.query.get(_id=_id)
@@ -416,7 +415,7 @@ class TestUsersSearch(TestController):
         'username_s': 'darth'}])
 
     def setUp(self):
-        super(TestUsersSearch, self).setUp()
+        super().setUp()
         # Create user that matches TEST_HIT id
         _id = ObjectId('540efdf2100d2b1483155d39')
         u = M.User.query.get(_id=_id)
@@ -500,14 +499,14 @@ class TestUserDetails(TestController):
     def test_add_audit_trail_entry_access(self):
         self.app.get('/nf/admin/user/add_audit_log_entry', status=404)  # GET is not allowed
         r = self.app.post('/nf/admin/user/add_audit_log_entry',
-                          extra_environ={'username': str('*anonymous')},
+                          extra_environ={'username': '*anonymous'},
                           status=302)
         assert_equal(r.location, 'http://localhost/auth/')
 
     def test_add_comment(self):
         r = self.app.get('/nf/admin/user/test-user')
         assert_not_in('Comment by test-admin: I was hêre!', r)
-        form = [f for f in six.itervalues(r.forms) if f.action.endswith('add_audit_trail_entry')][0]
+        form = [f for f in r.forms.values() if f.action.endswith('add_audit_trail_entry')][0]
         assert_equal(form['username'].value, 'test-user')
         form['comment'] = 'I was hêre!'
         r = form.submit()
@@ -653,7 +652,7 @@ class TestUserDetails(TestController):
                 'new_addr.addr': 'test@example.com',
                 'new_addr.claim': 'Claim Address',
                 'primary_addr': 'test@example.com'},
-                extra_environ=dict(username=str('test-admin')))
+                extra_environ=dict(username='test-admin'))
         r = self.app.get('/nf/admin/user/test-user')
         assert_in('test@example.com', r)
         em = M.EmailAddress.get(email='test@example.com')
@@ -668,7 +667,7 @@ class TestUserDetails(TestController):
                 'new_addr.addr': 'test2@example.com',
                 'new_addr.claim': 'Claim Address',
                 'primary_addr': 'test@example.com'},
-                extra_environ=dict(username=str('test-admin')))
+                extra_environ=dict(username='test-admin'))
         r = self.app.get('/nf/admin/user/test-user')
         assert_in('test2@example.com', r)
         em = M.EmailAddress.get(email='test2@example.com')
@@ -682,7 +681,7 @@ class TestUserDetails(TestController):
                 'username': 'test-user',
                 'new_addr.addr': '',
                 'primary_addr': 'test2@example.com'},
-                extra_environ=dict(username=str('test-admin')))
+                extra_environ=dict(username='test-admin'))
         r = self.app.get('/nf/admin/user/test-user')
         user = M.User.query.get(username='test-user')
         assert_equal(user.get_pref('email_address'), 'test2@example.com')
@@ -697,7 +696,7 @@ class TestUserDetails(TestController):
                 'addr-3.delete': 'on',
                 'new_addr.addr': '',
                 'primary_addr': 'test2@example.com'},
-                extra_environ=dict(username=str('test-admin')))
+                extra_environ=dict(username='test-admin'))
         r = self.app.get('/nf/admin/user/test-user')
         user = M.User.query.get(username='test-user')
         # test@example.com set as primary since test2@example.com is deleted
diff --git a/Allura/allura/tests/functional/test_user_profile.py b/Allura/allura/tests/functional/test_user_profile.py
index 566c2dd..cd4b3da 100644
--- a/Allura/allura/tests/functional/test_user_profile.py
+++ b/Allura/allura/tests/functional/test_user_profile.py
@@ -23,7 +23,6 @@ from alluratest.controller import TestRestApiBase
 from allura.model import Project, User
 from allura.tests import decorators as td
 from allura.tests import TestController
-from six.moves import map
 
 
 class TestUserProfile(TestController):
@@ -180,7 +179,7 @@ class TestUserProfile(TestController):
     @td.with_user_project('test-user')
     def test_send_message_for_anonymous(self):
         r = self.app.get('/u/test-user/profile/send_message',
-                         extra_environ={'username': str('*anonymous')},
+                         extra_environ={'username': '*anonymous'},
                          status=302)
         assert 'You must be logged in to send user messages.' in self.webflash(r)
 
@@ -188,7 +187,7 @@ class TestUserProfile(TestController):
                           params={'subject': 'test subject',
                                   'message': 'test message',
                                   'cc': 'on'},
-                          extra_environ={'username': str('*anonymous')},
+                          extra_environ={'username': '*anonymous'},
                           status=302)
         assert 'You must be logged in to send user messages.' in self.webflash(r)
 
diff --git a/Allura/allura/tests/model/__init__.py b/Allura/allura/tests/model/__init__.py
index b7d0eab..e2b83dc 100644
--- a/Allura/allura/tests/model/__init__.py
+++ b/Allura/allura/tests/model/__init__.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-
 #       Licensed to the Apache Software Foundation (ASF) under one
 #       or more contributor license agreements.  See the NOTICE file
 #       distributed with this work for additional information
@@ -19,4 +17,3 @@
 
 """Model test suite for the models of the application."""
 
-from __future__ import unicode_literals
\ No newline at end of file
diff --git a/Allura/allura/tests/model/test_artifact.py b/Allura/allura/tests/model/test_artifact.py
index 3ff4271..530f7c7 100644
--- a/Allura/allura/tests/model/test_artifact.py
+++ b/Allura/allura/tests/model/test_artifact.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-
 #       Licensed to the Apache Software Foundation (ASF) under one
 #       or more contributor license agreements.  See the NOTICE file
 #       distributed with this work for additional information
@@ -44,13 +42,13 @@ from forgewiki import model as WM
 class Checkmessage(M.Message):
 
     class __mongometa__:
-        name = str('checkmessage')
+        name = 'checkmessage'
 
     def url(self):
         return ''
 
     def __init__(self, **kw):
-        super(Checkmessage, self).__init__(**kw)
+        super().__init__(**kw)
         if self.slug is not None and self.full_slug is None:
             self.full_slug = datetime.utcnow().strftime('%Y%m%d%H%M%S%f') + ':' + self.slug
 Mapper.compile_all()
diff --git a/Allura/allura/tests/model/test_auth.py b/Allura/allura/tests/model/test_auth.py
index fd3c3ad..90e3988 100644
--- a/Allura/allura/tests/model/test_auth.py
+++ b/Allura/allura/tests/model/test_auth.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-
 #       Licensed to the Apache Software Foundation (ASF) under one
 #       or more contributor license agreements.  See the NOTICE file
 #       distributed with this work for additional information
@@ -364,7 +362,7 @@ def test_user_track_active():
     assert_equal(c.user.last_access['session_ip'], None)
     assert_equal(c.user.last_access['session_ua'], None)
 
-    req = Mock(headers={'User-Agent': str('browser')}, remote_addr='addr')
+    req = Mock(headers={'User-Agent': 'browser'}, remote_addr='addr')
     c.user.track_active(req)
     c.user = M.User.by_username(c.user.username)
     assert_not_equal(c.user.last_access['session_date'], None)
@@ -470,7 +468,7 @@ def test_user_backfill_login_details():
     assert_equal(details[1].ua, 'TestBrowser/57')
 
 
-class TestAuditLog(object):
+class TestAuditLog:
 
     def test_message_html(self):
         al = h.auditlog_user('our message <script>alert(1)</script>')
diff --git a/Allura/allura/tests/model/test_discussion.py b/Allura/allura/tests/model/test_discussion.py
index c21363e..124c916 100644
--- a/Allura/allura/tests/model/test_discussion.py
+++ b/Allura/allura/tests/model/test_discussion.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-
 #       Licensed to the Apache Software Foundation (ASF) under one
 #       or more contributor license agreements.  See the NOTICE file
 #       distributed with this work for additional information
@@ -38,7 +36,6 @@ from allura import model as M
 from allura.lib import helpers as h
 from allura.tests import TestController
 from alluratest.controller import setup_global_objects
-from six.moves import range
 
 
 def setUp():
@@ -205,7 +202,7 @@ def test_attachment_methods():
     ThreadLocalORMSession.flush_all()
     n = M.Notification.query.get(
         subject='[test:wiki] Test comment notification')
-    url = h.absurl('{}attachment/{}'.format(p.url(), fs.filename))
+    url = h.absurl(f'{p.url()}attachment/{fs.filename}')
     assert_in(
         '\nAttachments:\n\n'
         '- [fake.txt]({}) (37 Bytes; text/plain)'.format(url),
@@ -270,7 +267,7 @@ def test_notification_two_attaches():
     ThreadLocalORMSession.flush_all()
     n = M.Notification.query.get(
         subject='[test:wiki] Test comment notification')
-    base_url = h.absurl('{}attachment/'.format(p.url()))
+    base_url = h.absurl(f'{p.url()}attachment/')
     assert_in(
         '\nAttachments:\n\n'
         '- [fake.txt]({0}fake.txt) (37 Bytes; text/plain)\n'
diff --git a/Allura/allura/tests/model/test_filesystem.py b/Allura/allura/tests/model/test_filesystem.py
index 7c742a9..60e78a8 100644
--- a/Allura/allura/tests/model/test_filesystem.py
+++ b/Allura/allura/tests/model/test_filesystem.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-
 #       Licensed to the Apache Software Foundation (ASF) under one
 #       or more contributor license agreements.  See the NOTICE file
 #       distributed with this work for additional information
@@ -30,7 +28,6 @@ from webob import Request, Response
 
 from allura import model as M
 from alluratest.controller import setup_unit_test
-from io import open
 
 
 class File(M.File):
@@ -86,7 +83,6 @@ class TestFile(TestCase):
         assert self.db.fs.chunks.count() >= 1
         assert f.filename == os.path.basename(path)
         text = f.rfile().read()
-        assert text.startswith(b'# -*-')
 
     def test_delete(self):
         f = File.from_data('test1.txt', b'test1')
diff --git a/Allura/allura/tests/model/test_neighborhood.py b/Allura/allura/tests/model/test_neighborhood.py
index 428ad0b..c5a0e5c 100644
--- a/Allura/allura/tests/model/test_neighborhood.py
+++ b/Allura/allura/tests/model/test_neighborhood.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-
 #       Licensed to the Apache Software Foundation (ASF) under one
 #       or more contributor license agreements.  See the NOTICE file
 #       distributed with this work for additional information
diff --git a/Allura/allura/tests/model/test_notification.py b/Allura/allura/tests/model/test_notification.py
index 2258f42..5ad0aa6 100644
--- a/Allura/allura/tests/model/test_notification.py
+++ b/Allura/allura/tests/model/test_notification.py
@@ -388,13 +388,13 @@ class TestSubscriptionTypes(unittest.TestCase):
 
             def __init__(self, factory=list, *a, **kw):
                 self._factory = factory
-                super(OrderedDefaultDict, self).__init__(*a, **kw)
+                super().__init__(*a, **kw)
 
             def __getitem__(self, key):
                 if key not in self:
                     value = self[key] = self._factory()
                 else:
-                    value = super(OrderedDefaultDict, self).__getitem__(key)
+                    value = super().__getitem__(key)
                 return value
 
         notifications = mocked_notification.query.find.return_value.all.return_value = [
diff --git a/Allura/allura/tests/model/test_oauth.py b/Allura/allura/tests/model/test_oauth.py
index 45a7eed..9f7b7f0 100644
--- a/Allura/allura/tests/model/test_oauth.py
+++ b/Allura/allura/tests/model/test_oauth.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-
 #       Licensed to the Apache Software Foundation (ASF) under one
 #       or more contributor license agreements.  See the NOTICE file
 #       distributed with this work for additional information
diff --git a/Allura/allura/tests/model/test_project.py b/Allura/allura/tests/model/test_project.py
index 16cc4e6..7dc696d 100644
--- a/Allura/allura/tests/model/test_project.py
+++ b/Allura/allura/tests/model/test_project.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-
 #       Licensed to the Apache Software Foundation (ASF) under one
 #       or more contributor license agreements.  See the NOTICE file
 #       distributed with this work for additional information
diff --git a/Allura/allura/tests/model/test_repo.py b/Allura/allura/tests/model/test_repo.py
index 2a31145..aa85e06 100644
--- a/Allura/allura/tests/model/test_repo.py
+++ b/Allura/allura/tests/model/test_repo.py
@@ -1,4 +1,3 @@
-# coding=utf-8
 #       Licensed to the Apache Software Foundation (ASF) under one
 #       or more contributor license agreements.  See the NOTICE file
 #       distributed with this work for additional information
@@ -32,7 +31,7 @@ from allura import model as M
 from allura.lib import helpers as h
 
 
-class TestGitLikeTree(object):
+class TestGitLikeTree:
     def test_set_blob(self):
         tree = M.GitLikeTree()
         tree.set_blob('/dir/dir2/file', 'file-oid')
@@ -63,12 +62,12 @@ class TestGitLikeTree(object):
 
     def test_hex_with_unicode(self):
         tree = M.GitLikeTree()
-        tree.set_blob(u'/dir/f•º£', 'file-oid')
+        tree.set_blob('/dir/f•º£', 'file-oid')
         # the hex() value shouldn't change, it's an important key
         assert_equal(tree.hex(), '51ce65bead2f6452da61d4f6f2e42f8648bf9e4b')
 
 
-class RepoImplTestBase(object):
+class RepoImplTestBase:
     pass
 
 
@@ -679,7 +678,7 @@ class TestModelCache(unittest.TestCase):
         session.return_value.expunge.assert_called_once_with(tree1)
 
 
-class TestMergeRequest(object):
+class TestMergeRequest:
 
     def setUp(self):
         setup_basic_test()
diff --git a/Allura/allura/tests/model/test_timeline.py b/Allura/allura/tests/model/test_timeline.py
index 1efb175..067b719 100644
--- a/Allura/allura/tests/model/test_timeline.py
+++ b/Allura/allura/tests/model/test_timeline.py
@@ -22,7 +22,7 @@ from allura.tests import decorators as td
 from alluratest.controller import setup_basic_test, setup_global_objects
 
 
-class TestActivityObject_Functional(object):
+class TestActivityObject_Functional:
     # NOTE not for unit tests, this class sets up all the junk
 
     def setUp(self):
diff --git a/Allura/allura/tests/scripts/test_create_sitemap_files.py b/Allura/allura/tests/scripts/test_create_sitemap_files.py
index de99290..31fa0bf 100644
--- a/Allura/allura/tests/scripts/test_create_sitemap_files.py
+++ b/Allura/allura/tests/scripts/test_create_sitemap_files.py
@@ -29,7 +29,7 @@ from allura.lib import helpers as h
 from allura.scripts.create_sitemap_files import CreateSitemapFiles
 
 
-class TestCreateSitemapFiles(object):
+class TestCreateSitemapFiles:
 
     def setUp(self):
         setup_basic_test()
diff --git a/Allura/allura/tests/scripts/test_delete_projects.py b/Allura/allura/tests/scripts/test_delete_projects.py
index 84f5ae6..6d3f1c3 100644
--- a/Allura/allura/tests/scripts/test_delete_projects.py
+++ b/Allura/allura/tests/scripts/test_delete_projects.py
@@ -1,4 +1,3 @@
-# coding=utf-8
 #       Licensed to the Apache Software Foundation (ASF) under one
 #       or more contributor license agreements.  See the NOTICE file
 #       distributed with this work for additional information
@@ -31,7 +30,7 @@ from allura.lib import plugin
 class TestDeleteProjects(TestController):
 
     def setUp(self):
-        super(TestDeleteProjects, self).setUp()
+        super().setUp()
         n = M.Neighborhood.query.get(name='Projects')
         admin = M.User.by_username('test-admin')
         self.p_shortname = 'test-delete'
@@ -59,7 +58,7 @@ class TestDeleteProjects(TestController):
     def test_project_is_deleted(self):
         p = M.Project.query.get(shortname=self.p_shortname)
         assert p is not None, 'Can not find project to delete'
-        self.run_script(['p/{}'.format(p.shortname)])
+        self.run_script([f'p/{p.shortname}'])
         session(p).expunge(p)
         p = M.Project.query.get(shortname=p.shortname)
         assert p is None, 'Project is not deleted'
@@ -68,7 +67,7 @@ class TestDeleteProjects(TestController):
         pid = M.Project.query.get(shortname=self.p_shortname)._id
         things = self.things_related_to_project(pid)
         assert len(things) > 0, 'No things related to project to begin with'
-        self.run_script(['p/{}'.format(self.p_shortname)])
+        self.run_script([f'p/{self.p_shortname}'])
         things = self.things_related_to_project(pid)
         assert len(things) == 0, 'Not all things are deleted: %s' % things
 
@@ -97,7 +96,7 @@ class TestDeleteProjects(TestController):
     @patch('allura.lib.plugin.solr_del_project_artifacts', autospec=True)
     def test_solr_index_is_deleted(self, del_solr):
         pid = M.Project.query.get(shortname=self.p_shortname)._id
-        self.run_script(['p/{}'.format(self.p_shortname)])
+        self.run_script([f'p/{self.p_shortname}'])
         del_solr.post.assert_called_once_with(pid)
 
     @with_user_project('test-user')
@@ -113,7 +112,7 @@ class TestDeleteProjects(TestController):
     @patch.object(plugin.g, 'post_event', autospec=True)
     def test_event_is_fired(self, post_event):
         pid = M.Project.query.get(shortname=self.p_shortname)._id
-        self.run_script(['p/{}'.format(self.p_shortname)])
+        self.run_script([f'p/{self.p_shortname}'])
         post_event.assert_called_once_with('project_deleted', project_id=pid, reason=None)
 
     @patch.object(plugin.g, 'post_event', autospec=True)
@@ -122,7 +121,7 @@ class TestDeleteProjects(TestController):
         p = M.Project.query.get(shortname=self.p_shortname)
         pid = p._id
         assert p is not None, 'Can not find project to delete'
-        self.run_script(['-r', 'The Reason¢¢', 'p/{}'.format(p.shortname)])
+        self.run_script(['-r', 'The Reason¢¢', f'p/{p.shortname}'])
         session(p).expunge(p)
         p = M.Project.query.get(shortname=p.shortname)
         assert p is None, 'Project is not deleted'
@@ -134,8 +133,8 @@ class TestDeleteProjects(TestController):
         self.proj.add_user(dev, ['Developer'])
         ThreadLocalODMSession.flush_all()
         g.credentials.clear()
-        proj = 'p/{}'.format(self.p_shortname)
-        msg = 'Account disabled because project /{} is deleted. Reason: The Reason'.format(proj)
+        proj = f'p/{self.p_shortname}'
+        msg = f'Account disabled because project /{proj} is deleted. Reason: The Reason'
         opts = ['-r', 'The Reason', proj]
         if disable:
             opts.insert(0, '--disable-users')
diff --git a/Allura/allura/tests/scripts/test_misc_scripts.py b/Allura/allura/tests/scripts/test_misc_scripts.py
index 6fb093a..a20206f 100644
--- a/Allura/allura/tests/scripts/test_misc_scripts.py
+++ b/Allura/allura/tests/scripts/test_misc_scripts.py
@@ -24,7 +24,7 @@ from allura import model as M
 from ming.odm import session
 
 
-class TestClearOldNotifications(object):
+class TestClearOldNotifications:
 
     def setUp(self):
         setup_basic_test()
diff --git a/Allura/allura/tests/scripts/test_reindexes.py b/Allura/allura/tests/scripts/test_reindexes.py
index db10d54..c2cdc2b 100644
--- a/Allura/allura/tests/scripts/test_reindexes.py
+++ b/Allura/allura/tests/scripts/test_reindexes.py
@@ -24,7 +24,7 @@ from alluratest.controller import setup_basic_test
 from allura import model as M
 
 
-class TestReindexProjects(object):
+class TestReindexProjects:
 
     def setUp(self):
         setup_basic_test()
@@ -48,7 +48,7 @@ class TestReindexProjects(object):
         assert_equal(M.MonQTask.query.find({'task_name': 'allura.tasks.index_tasks.add_projects'}).count(), 1)
 
 
-class TestReindexUsers(object):
+class TestReindexUsers:
 
     def setUp(self):
         setup_basic_test()
diff --git a/Allura/allura/tests/templates/jinja_master/test_lib.py b/Allura/allura/tests/templates/jinja_master/test_lib.py
index 4c649e7..9e73a0f 100644
--- a/Allura/allura/tests/templates/jinja_master/test_lib.py
+++ b/Allura/allura/tests/templates/jinja_master/test_lib.py
@@ -28,7 +28,7 @@ def strip_space(s):
     return ''.join(s.split())
 
 
-class TemplateTest(object):
+class TemplateTest:
     def setUp(self):
         setup_basic_test()
         self.jinja2_env = AlluraJinjaRenderer.create(config, g)['jinja'].jinja2_env
@@ -63,7 +63,7 @@ class TestRelatedArtifacts(TemplateTest):
 
     def test_non_artifact(self):
         # e.g. a commit
-        class CommitThing(object):
+        class CommitThing:
             type_s = 'Commit'
 
             def link_text(self):
diff --git a/Allura/allura/tests/test_app.py b/Allura/allura/tests/test_app.py
index a7fe5ca..ceca745 100644
--- a/Allura/allura/tests/test_app.py
+++ b/Allura/allura/tests/test_app.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-
 #       Licensed to the Apache Software Foundation (ASF) under one
 #       or more contributor license agreements.  See the NOTICE file
 #       distributed with this work for additional information
@@ -138,17 +136,17 @@ def test_handle_artifact_unicode(qg):
 
     a = app.Application(c.project, c.app.config)
 
-    msg = dict(payload='foo ƒ†©¥˙¨ˆ'.encode('utf-8'), message_id=1, headers={})
+    msg = dict(payload='foo ƒ†©¥˙¨ˆ'.encode(), message_id=1, headers={})
     a.handle_artifact_message(ticket, msg)
-    assert_equal(post.attach.call_args[0][1].getvalue(), 'foo ƒ†©¥˙¨ˆ'.encode('utf-8'))
+    assert_equal(post.attach.call_args[0][1].getvalue(), 'foo ƒ†©¥˙¨ˆ'.encode())
 
     msg = dict(payload=b'foo', message_id=1, headers={})
     a.handle_artifact_message(ticket, msg)
     assert_equal(post.attach.call_args[0][1].getvalue(), b'foo')
 
-    msg = dict(payload="\x94my quote\x94".encode('utf-8'), message_id=1, headers={})
+    msg = dict(payload="\x94my quote\x94".encode(), message_id=1, headers={})
     a.handle_artifact_message(ticket, msg)
-    assert_equal(post.attach.call_args[0][1].getvalue(), '\x94my quote\x94'.encode('utf-8'))
+    assert_equal(post.attach.call_args[0][1].getvalue(), '\x94my quote\x94'.encode())
 
     # assert against prod example
     msg_raw = """Message-Id: <15...@webmail.messagingengine.com>
diff --git a/Allura/allura/tests/test_commands.py b/Allura/allura/tests/test_commands.py
index 4f89c2e..edb50b1 100644
--- a/Allura/allura/tests/test_commands.py
+++ b/Allura/allura/tests/test_commands.py
@@ -36,13 +36,12 @@ from allura.command import base, script, set_neighborhood_features, \
 from allura import model as M
 from allura.lib.exceptions import InvalidNBFeatureValueError
 from allura.tests import decorators as td
-from six.moves import range
 
 test_config = pkg_resources.resource_filename(
     'allura', '../test.ini') + '#main'
 
 
-class EmptyClass(object):
+class EmptyClass:
     pass
 
 
@@ -183,7 +182,7 @@ def test_update_neighborhood():
     assert nb.has_home_tool is False
 
 
-class TestEnsureIndexCommand(object):
+class TestEnsureIndexCommand:
 
     def test_run(self):
         cmd = show_models.EnsureIndexCommand('ensure_index')
@@ -269,7 +268,7 @@ class TestEnsureIndexCommand(object):
         ])
 
 
-class TestTaskCommand(object):
+class TestTaskCommand:
 
     def tearDown(self):
         M.MonQTask.query.remove({})
@@ -335,7 +334,7 @@ class TestTaskCommand(object):
         assert_equal(M.MonQTask.query.find().count(), 0)
 
 
-class TestTaskdCleanupCommand(object):
+class TestTaskdCleanupCommand:
 
     def setUp(self):
         self.cmd_class = taskd_cleanup.TaskdCleanupCommand
@@ -449,24 +448,18 @@ def test_status_log_retries():
     assert cmd._taskd_status.mock_calls == expected_calls
 
 
-class TestShowModels(object):
+class TestShowModels:
 
     def test_show_models(self):
         cmd = show_models.ShowModelsCommand('models')
         with OutputCapture() as output:
             cmd.run([test_config])
-        if six.PY3:
-            assert_in('''allura.model.notification.SiteNotification
+        assert_in('''allura.model.notification.SiteNotification
          - <FieldProperty _id>
          - <FieldProperty content>
         ''', output.captured)
-        else:
-            # order of class fields are not preserved
-            assert_in('allura.model.notification.SiteNotification\n', output.captured)
-            assert_in('         - <FieldProperty _id>\n', output.captured)
-            assert_in('         - <FieldProperty content>\n', output.captured)
 
-class TestReindexAsTask(object):
+class TestReindexAsTask:
 
     cmd = 'allura.command.show_models.ReindexCommand'
     task_name = 'allura.command.base.run_command'
@@ -500,7 +493,7 @@ class TestReindexAsTask(object):
             M.MonQTask.query.remove()
 
 
-class TestReindexCommand(object):
+class TestReindexCommand:
 
     @patch('allura.command.show_models.g')
     def test_skip_solr_delete(self, g):
diff --git a/Allura/allura/tests/test_decorators.py b/Allura/allura/tests/test_decorators.py
index ccbb3b1..0935b7b 100644
--- a/Allura/allura/tests/test_decorators.py
+++ b/Allura/allura/tests/test_decorators.py
@@ -59,7 +59,7 @@ class TestTask(TestCase):
         func.post('test', foo=2, delay=1)
 
 
-class TestMemoize(object):
+class TestMemoize:
 
     def test_function(self):
         @memoize
@@ -81,7 +81,7 @@ class TestMemoize(object):
 
     def test_methods(self):
 
-        class Randomy(object):
+        class Randomy:
             @memoize
             def randomy(self, do_random):
                 if do_random:
@@ -122,7 +122,7 @@ class TestMemoize(object):
 
     def test_methods_garbage_collection(self):
 
-        class Randomy(object):
+        class Randomy:
             @memoize
             def randomy(self, do_random):
                 if do_random:
diff --git a/Allura/allura/tests/test_diff.py b/Allura/allura/tests/test_diff.py
index 926c7b1..854b945 100644
--- a/Allura/allura/tests/test_diff.py
+++ b/Allura/allura/tests/test_diff.py
@@ -1,4 +1,3 @@
-# -*- coding: utf-8 -*-
 #       Licensed to the Apache Software Foundation (ASF) under one
 #       or more contributor license agreements.  See the NOTICE file
 #       distributed with this work for additional information
diff --git a/Allura/allura/tests/test_globals.py b/Allura/allura/tests/test_globals.py
index 8ca1c00..47540c2 100644
--- a/Allura/allura/tests/test_globals.py
+++ b/Allura/allura/tests/test_globals.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-
 #       Licensed to the Apache Software Foundation (ASF) under one
 #       or more contributor license agreements.  See the NOTICE file
 #       distributed with this work for additional information
@@ -48,7 +46,6 @@ from allura.tests import decorators as td
 
 from forgewiki import model as WM
 from forgeblog import model as BM
-from io import open
 
 
 def squish_spaces(text):
@@ -350,7 +347,7 @@ def test_macro_embed(oembed_fetch):
 def test_macro_embed_video_gone():
     # this does a real fetch
     r = g.markdown_wiki.convert('[[embed url=https://www.youtube.com/watch?v=OWsFqPZ3v-0]]')
-    r = six.text_type(r)  # convert away from Markup, to get better assertion diff output
+    r = str(r)  # convert away from Markup, to get better assertion diff output
     # either of these could happen depending on the mood of youtube's oembed API:
     assert_in(r, [
         '<div class="markdown_content"><p>Video not available</p></div>',
@@ -553,7 +550,7 @@ def test_markdown_autolink():
     tgt = 'http://everything2.com/?node=nate+oostendorp'
     s = g.markdown.convert('This is %s' % tgt)
     assert_equal(
-        s, '<div class="markdown_content"><p>This is <a href="{}" rel="nofollow">{}</a></p></div>'.format(tgt, tgt))
+        s, f'<div class="markdown_content"><p>This is <a href="{tgt}" rel="nofollow">{tgt}</a></p></div>')
     assert '<a href=' in g.markdown.convert('This is http://domain.net')
     # beginning of doc
     assert_in('<a href=', g.markdown.convert('http://domain.net abc'))
@@ -745,7 +742,7 @@ def test_myprojects_macro():
     for p in c.user.my_projects():
         if p.deleted or p.is_nbhd_project:
             continue
-        proj_title = '<h2><a href="{}">{}</a></h2>'.format(p.url(), p.name)
+        proj_title = f'<h2><a href="{p.url()}">{p.name}</a></h2>'
         assert_in(proj_title, r)
 
     h.set_context('u/test-user-1', 'wiki', neighborhood='Users')
@@ -754,7 +751,7 @@ def test_myprojects_macro():
     for p in user.my_projects():
         if p.deleted or p.is_nbhd_project:
             continue
-        proj_title = '<h2><a href="{}">{}</a></h2>'.format(p.url(), p.name)
+        proj_title = f'<h2><a href="{p.url()}">{p.name}</a></h2>'
         assert_in(proj_title, r)
 
 
@@ -1073,7 +1070,7 @@ class TestHandlePaging(unittest.TestCase):
         self.assertEqual(g.handle_paging(10, 'asdf', 30), (10, 0, 0))
 
 
-class TestIconRender(object):
+class TestIconRender:
 
     def setUp(self):
         self.i = g.icons['edit']
diff --git a/Allura/allura/tests/test_helpers.py b/Allura/allura/tests/test_helpers.py
index 0771258..267b349 100644
--- a/Allura/allura/tests/test_helpers.py
+++ b/Allura/allura/tests/test_helpers.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-
 #       Licensed to the Apache Software Foundation (ASF) under one
 #       or more contributor license agreements.  See the NOTICE file
 #       distributed with this work for additional information
@@ -41,7 +39,6 @@ from allura.lib.security import Credentials
 from allura.tests import decorators as td
 from alluratest.controller import setup_basic_test
 import six
-from io import open
 
 
 def setUp(self):
@@ -101,12 +98,12 @@ def test_really_unicode():
     assert s.startswith('\ufeff'), repr(s)
     s = h.really_unicode(
         open(path.join(here_dir, 'data/unicode_test.txt')).read())
-    assert isinstance(s, six.text_type)
+    assert isinstance(s, str)
     # try non-ascii string in legacy 8bit encoding
     h.really_unicode('\u0410\u0401'.encode('cp1251'))
     # ensure invalid encodings are handled gracefully
     s = h._attempt_encodings(b'foo', ['LKDJFLDK'])
-    assert isinstance(s, six.text_type)
+    assert isinstance(s, str)
     # unicode stays the same
     assert_equals(h.really_unicode('¬∂•°‹'), '¬∂•°‹')
     # other types are handled too
@@ -115,7 +112,7 @@ def test_really_unicode():
     assert_equals(h.really_unicode(None), '')
     # markup stays markup
     s = h.really_unicode(Markup('<b>test</b>'))
-    assert isinstance(s, six.text_type)
+    assert isinstance(s, str)
     assert isinstance(s, Markup)
     assert_equals(s, '<b>test</b>')
 
@@ -252,7 +249,7 @@ def test_paging_sanitizer():
         (10, 0): (10, 0),
         ('junk', 'more junk'): (25, 0),
     }
-    for input, output in six.iteritems(test_data):
+    for input, output in test_data.items():
         assert (h.paging_sanitizer(*input)) == output
 
 
@@ -278,7 +275,7 @@ def test_render_any_markup_formatting():
 
 def test_render_any_markdown_encoding():
     # send encoded content in, make sure it converts it to actual unicode object which Markdown lib needs
-    assert_equals(h.render_any_markup('README.md', 'Müller'.encode('utf8')),
+    assert_equals(h.render_any_markup('README.md', 'Müller'.encode()),
                   '<div class="markdown_content"><p>Müller</p></div>')
 
 
@@ -485,7 +482,7 @@ class TestUrlOpen(TestCase):
         import errno
 
         def side_effect(url, timeout=None):
-            raise socket.error(errno.ECONNRESET, 'Connection reset by peer')
+            raise OSError(errno.ECONNRESET, 'Connection reset by peer')
         urlopen.side_effect = side_effect
         self.assertRaises(socket.error, h.urlopen, 'myurl')
         self.assertEqual(urlopen.call_count, 4)
@@ -550,7 +547,7 @@ class TestIterEntryPoints(TestCase):
 
     @patch('allura.lib.helpers.pkg_resources')
     def test_subclassed_ep(self, pkg_resources):
-        class App(object):
+        class App:
             pass
 
         class BetterApp(App):
@@ -566,13 +563,13 @@ class TestIterEntryPoints(TestCase):
 
     @patch('allura.lib.helpers.pkg_resources')
     def test_ambiguous_eps(self, pkg_resources):
-        class App(object):
+        class App:
             pass
 
         class BetterApp(App):
             pass
 
-        class BestApp(object):
+        class BestApp:
             pass
 
         pkg_resources.iter_entry_points.return_value = [
@@ -580,7 +577,7 @@ class TestIterEntryPoints(TestCase):
             self._make_ep('myapp', BetterApp),
             self._make_ep('myapp', BestApp)]
 
-        self.assertRaisesRegexp(ImportError,
+        self.assertRaisesRegex(ImportError,
                                 r'Ambiguous \[allura\] entry points detected. '
                                 'Multiple entry points with name "myapp".',
                                 list, h.iter_entry_points('allura'))
diff --git a/Allura/allura/tests/test_mail_util.py b/Allura/allura/tests/test_mail_util.py
index e100f0d..1138c44 100644
--- a/Allura/allura/tests/test_mail_util.py
+++ b/Allura/allura/tests/test_mail_util.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-
 #       Licensed to the Apache Software Foundation (ASF) under one
 #       or more contributor license agreements.  See the NOTICE file
 #       distributed with this work for additional information
@@ -97,7 +95,7 @@ class TestReactor(unittest.TestCase):
         msg1['Message-ID'] = '<fo...@bar.com>'
         s_msg = msg1.as_string()
         msg2 = parse_message(s_msg)
-        assert isinstance(msg2['payload'], six.text_type)
+        assert isinstance(msg2['payload'], str)
         assert_in('всех', msg2['payload'])
 
     def test_more_encodings(self):
@@ -116,7 +114,7 @@ c28uMyIsClRoZSBhcHBsaWNhdGlvbidzIGNvbW11bmljYXRpb24gd29yayB3ZWxsICxidXQgdGhl
 IGZ0cCx0ZWxuZXQscGluZyBjYW4ndCB3b3JrICEKCgpXaHk/
 """
         msg = parse_message(s_msg)
-        assert isinstance(msg['payload'], six.text_type)
+        assert isinstance(msg['payload'], str)
         assert_in('The Snap7 application', msg['payload'])
 
         s_msg = """Date: Sat, 25 May 2019 09:32:00 +1000
@@ -135,7 +133,7 @@ Content-Transfer-Encoding: 8bit
 >
 """
         msg = parse_message(s_msg)
-        assert isinstance(msg['payload'], six.text_type)
+        assert isinstance(msg['payload'], str)
         assert_in('• foo', msg['payload'])
 
         s_msg = """Date: Sat, 25 May 2019 09:32:00 +1000
@@ -148,7 +146,7 @@ Content-Transfer-Encoding: 8BIT
 programmed or èrogrammed ?
 """
         msg = parse_message(s_msg)
-        assert isinstance(msg['payload'], six.text_type)
+        assert isinstance(msg['payload'], str)
         assert_in('èrogrammed', msg['payload'])
 
     def test_more_encodings_multipart(self):
@@ -178,8 +176,8 @@ Content-Type: text/html; charset="utf-8"
 &gt; • foo.txt (1.0 kB; text/plain)
 """
         msg = parse_message(s_msg)
-        assert isinstance(msg['parts'][1]['payload'], six.text_type)
-        assert isinstance(msg['parts'][2]['payload'], six.text_type)
+        assert isinstance(msg['parts'][1]['payload'], str)
+        assert isinstance(msg['parts'][2]['payload'], str)
         assert_in('• foo', msg['parts'][1]['payload'])
         assert_in('• foo', msg['parts'][2]['payload'])
 
@@ -208,10 +206,10 @@ Content-Type: text/html; charset="utf-8"
         for part in msg2['parts']:
             if part['payload'] is None:
                 continue
-            assert isinstance(part['payload'], six.text_type), type(part['payload'])
+            assert isinstance(part['payload'], str), type(part['payload'])
 
 
-class TestHeader(object):
+class TestHeader:
 
     @raises(TypeError)
     def test_bytestring(self):
@@ -232,7 +230,7 @@ class TestHeader(object):
                      '=?utf-8?b?ItGC0LXRgdC90Y/RgtGB0Y8i?= <da...@b.com>')
 
 
-class TestIsAutoreply(object):
+class TestIsAutoreply:
 
     def setUp(self):
         self.msg = {'headers': {}}
@@ -278,7 +276,7 @@ class TestIsAutoreply(object):
         assert_true(is_autoreply(self.msg))
 
 
-class TestIdentifySender(object):
+class TestIdentifySender:
 
     @mock.patch('allura.model.EmailAddress')
     def test_arg(self, EA):
@@ -327,7 +325,7 @@ def test_parse_message_id():
     ])
 
 
-class TestMailServer(object):
+class TestMailServer:
 
     def setUp(self):
         setup_basic_test()
@@ -337,6 +335,6 @@ class TestMailServer(object):
         listen_port = ('0.0.0.0', 8825)
         mailserver = MailServer(listen_port, None)
         mailserver.process_message('127.0.0.1', 'foo@bar.com', ['1234@tickets.test.p.localhost'],
-                                   'this is the email body with headers and everything Ο'.encode('utf-8'))
+                                   'this is the email body with headers and everything Ο'.encode())
         assert_equal([], log.exception.call_args_list)
         assert log.info.call_args[0][0].startswith('Msg passed along as task '), log.info.call_args
diff --git a/Allura/allura/tests/test_markdown.py b/Allura/allura/tests/test_markdown.py
index 3d32358..992eec8 100644
--- a/Allura/allura/tests/test_markdown.py
+++ b/Allura/allura/tests/test_markdown.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-
 #       Licensed to the Apache Software Foundation (ASF) under one
 #       or more contributor license agreements.  See the NOTICE file
 #       distributed with this work for additional information
diff --git a/Allura/allura/tests/test_middlewares.py b/Allura/allura/tests/test_middlewares.py
index 7d7ae3e..a428a10 100644
--- a/Allura/allura/tests/test_middlewares.py
+++ b/Allura/allura/tests/test_middlewares.py
@@ -21,7 +21,7 @@ from alluratest.tools import assert_not_equal
 from allura.lib.custom_middleware import CORSMiddleware
 
 
-class TestCORSMiddleware(object):
+class TestCORSMiddleware:
 
     def setUp(self):
         self.app = MagicMock()
diff --git a/Allura/allura/tests/test_multifactor.py b/Allura/allura/tests/test_multifactor.py
index 5b1c44e..2c76fc1 100644
--- a/Allura/allura/tests/test_multifactor.py
+++ b/Allura/allura/tests/test_multifactor.py
@@ -34,7 +34,7 @@ from allura.lib.multifactor import RecoveryCodeService, MongodbRecoveryCodeServi
 from allura.lib.multifactor import GoogleAuthenticatorPamFilesystemRecoveryCodeService
 
 
-class TestGoogleAuthenticatorFile(object):
+class TestGoogleAuthenticatorFile:
     sample = textwrap.dedent('''\
         7CL3WL756ISQCU5HRVNAODC44Q
         " RATE_LIMIT 3 30
@@ -80,7 +80,7 @@ class GenericTotpService(TotpService):
         pass
 
 
-class TestTotpService(object):
+class TestTotpService:
 
     sample_key = b'\x00K\xda\xbfv\xc2B\xaa\x1a\xbe\xa5\x96b\xb2\xa0Z:\xc9\xcf\x8a'
     sample_time = 1472502664
@@ -124,7 +124,7 @@ class TestTotpService(object):
         assert srv.get_qr_code(totp, user)
 
 
-class TestAnyTotpServiceImplementation(object):
+class TestAnyTotpServiceImplementation:
 
     __test__ = False
 
@@ -183,7 +183,7 @@ class TestMongodbTotpService(TestAnyTotpServiceImplementation):
         ming.configure(**config)
 
 
-class TestGoogleAuthenticatorPamFilesystemMixin(object):
+class TestGoogleAuthenticatorPamFilesystemMixin:
 
     def setUp(self):
         self.totp_basedir = tempfile.mkdtemp(prefix='totp-test', dir=os.getenv('TMPDIR', '/tmp'))
@@ -204,10 +204,10 @@ class TestGoogleAuthenticatorPamFilesystemTotpService(TestAnyTotpServiceImplemen
         # make a regular .google-authenticator file first, so rate limit info has somewhere to go
         self.Service().set_secret_key(self.mock_user(), self.sample_key)
         # then run test
-        super(TestGoogleAuthenticatorPamFilesystemTotpService, self).test_rate_limiting()
+        super().test_rate_limiting()
 
 
-class TestRecoveryCodeService(object):
+class TestRecoveryCodeService:
 
     def test_generate_one_code(self):
         code = RecoveryCodeService().generate_one_code()
@@ -229,7 +229,7 @@ class TestRecoveryCodeService(object):
         assert_equal(len(recovery.saved_codes), asint(config.get('auth.multifactor.recovery_code.count', 10)))
 
 
-class TestAnyRecoveryCodeServiceImplementation(object):
+class TestAnyRecoveryCodeServiceImplementation:
 
     __test__ = False
 
@@ -317,7 +317,7 @@ class TestGoogleAuthenticatorPamFilesystemRecoveryCodeService(TestAnyRecoveryCod
     Service = GoogleAuthenticatorPamFilesystemRecoveryCodeService
 
     def setUp(self):
-        super(TestGoogleAuthenticatorPamFilesystemRecoveryCodeService, self).setUp()
+        super().setUp()
 
         # make a regular .google-authenticator file first, so recovery keys have somewhere to go
         GoogleAuthenticatorPamFilesystemTotpService().set_secret_key(self.mock_user(),
@@ -327,7 +327,7 @@ class TestGoogleAuthenticatorPamFilesystemRecoveryCodeService(TestAnyRecoveryCod
         # this deletes the file
         GoogleAuthenticatorPamFilesystemTotpService().set_secret_key(self.mock_user(), None)
 
-        super(TestGoogleAuthenticatorPamFilesystemRecoveryCodeService, self).test_get_codes_none()
+        super().test_get_codes_none()
 
     def test_replace_codes_when_no_file(self):
         # this deletes the file
@@ -335,4 +335,4 @@ class TestGoogleAuthenticatorPamFilesystemRecoveryCodeService(TestAnyRecoveryCod
 
         # then it errors because no .google-authenticator file
         with assert_raises(IOError):
-            super(TestGoogleAuthenticatorPamFilesystemRecoveryCodeService, self).test_replace_codes()
+            super().test_replace_codes()
diff --git a/Allura/allura/tests/test_patches.py b/Allura/allura/tests/test_patches.py
index 41029ea..b86b95f 100644
--- a/Allura/allura/tests/test_patches.py
+++ b/Allura/allura/tests/test_patches.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-
 #       Licensed to the Apache Software Foundation (ASF) under one
 #       or more contributor license agreements.  See the NOTICE file
 #       distributed with this work for additional information
diff --git a/Allura/allura/tests/test_plugin.py b/Allura/allura/tests/test_plugin.py
index e7c0d03..76133e0 100644
--- a/Allura/allura/tests/test_plugin.py
+++ b/Allura/allura/tests/test_plugin.py
@@ -47,7 +47,7 @@ from allura.tests.decorators import audits
 from alluratest.controller import setup_basic_test, setup_global_objects
 
 
-class TestProjectRegistrationProvider(object):
+class TestProjectRegistrationProvider:
 
     def setUp(self):
         self.provider = ProjectRegistrationProvider()
@@ -84,7 +84,7 @@ class TestProjectRegistrationProvider(object):
         assert_raises(ProjectConflict, v, 'thisislegit', neighborhood=nbhd)
 
 
-class TestProjectRegistrationProviderParseProjectFromUrl(object):
+class TestProjectRegistrationProviderParseProjectFromUrl:
 
     def setUp(self):
         setup_basic_test()
@@ -138,7 +138,7 @@ class TestProjectRegistrationProviderParseProjectFromUrl(object):
         assert_equal((p, None), self.parse('http://localhost:8080/p/test/not-a-sub'))
 
 
-class UserMock(object):
+class UserMock:
     def __init__(self):
         self.tool_data = {}
         self._projects = []
@@ -158,7 +158,7 @@ class UserMock(object):
         return self._projects
 
 
-class TestProjectRegistrationProviderPhoneVerification(object):
+class TestProjectRegistrationProviderPhoneVerification:
 
     def setUp(self):
         self.p = ProjectRegistrationProvider()
@@ -275,7 +275,7 @@ class TestProjectRegistrationProviderPhoneVerification(object):
                     assert_equal(result, g.phone_service.verify.return_value)
             assert_equal(5, g.phone_service.verify.call_count)
 
-class TestThemeProvider(object):
+class TestThemeProvider:
 
     @patch('allura.app.g')
     @patch('allura.lib.plugin.g')
@@ -307,7 +307,7 @@ class TestThemeProvider(object):
         g.theme_href.assert_called_with('images/testapp_24.png')
 
 
-class TestThemeProvider_notifications(object):
+class TestThemeProvider_notifications:
 
     Provider = ThemeProvider
 
@@ -628,7 +628,7 @@ class TestThemeProvider_notifications(object):
         assert get_note[1] == 'testid-2-False'
 
 
-class TestLocalAuthenticationProvider(object):
+class TestLocalAuthenticationProvider:
 
     def setUp(self):
         setup_basic_test()
@@ -742,7 +742,7 @@ class TestLocalAuthenticationProvider(object):
         assert_equal(detail.ua, 'mybrowser')
 
 
-class TestAuthenticationProvider(object):
+class TestAuthenticationProvider:
 
     def setUp(self):
         setup_basic_test()
diff --git a/Allura/allura/tests/test_security.py b/Allura/allura/tests/test_security.py
index 052e04d..c94c924 100644
--- a/Allura/allura/tests/test_security.py
+++ b/Allura/allura/tests/test_security.py
@@ -76,7 +76,7 @@ class TestSecurity(TestController):
                      status=200)
         # This should fail b/c test-user doesn't have the permission
         self.app.get('/security/test-user/needs_artifact_access_fail',
-                     extra_environ=dict(username=str('test-user')), status=403)
+                     extra_environ=dict(username='test-user'), status=403)
         # This should succeed b/c users with the 'admin' permission on a
         # project implicitly have all permissions to everything in the project
         self.app.get(
diff --git a/Allura/allura/tests/test_tasks.py b/Allura/allura/tests/test_tasks.py
index ca3e3f2..d35634e 100644
--- a/Allura/allura/tests/test_tasks.py
+++ b/Allura/allura/tests/test_tasks.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-
 #       Licensed to the Apache Software Foundation (ASF) under one
 #       or more contributor license agreements.  See the NOTICE file
 #       distributed with this work for additional information
@@ -54,7 +52,6 @@ from allura.tasks import export_tasks
 from allura.tasks import admin_tasks
 from allura.tests import decorators as td
 from allura.lib.decorators import event_handler, task
-from six.moves import range
 
 
 class TestRepoTasks(unittest.TestCase):
@@ -298,13 +295,13 @@ class TestMailTasks(unittest.TestCase):
             # Also py2 and py3 vary in handling of double-quote separators when the name portion is encoded
             unquoted_cyrillic_No = '=?utf-8?b?0J/Qvg==?='  # По
             quoted_cyrillic_No = '=?utf-8?b?ItCf0L4i?='  # "По"
-            assert ('From: {} <fo...@bar.com>'.format(quoted_cyrillic_No) in body or
-                    'From: {} <fo...@bar.com>'.format(unquoted_cyrillic_No) in body), body
+            assert (f'From: {quoted_cyrillic_No} <fo...@bar.com>' in body or
+                    f'From: {unquoted_cyrillic_No} <fo...@bar.com>' in body), body
             assert_in(
                 'Subject: =?utf-8?b?0J/QviDQvtC20LjQstC70ZHQvdC90YvQvCDQsdC10YDQtdCz0LDQvA==?=', body)
             assert_in('Content-Type: text/plain; charset="utf-8"', body)
             assert_in('Content-Transfer-Encoding: base64', body)
-            assert_in(six.ensure_text(b64encode('Громады стройные теснятся'.encode('utf-8'))), body)
+            assert_in(six.ensure_text(b64encode('Громады стройные теснятся'.encode())), body)
 
     def test_send_email_with_disabled_user(self):
         c.user = M.User.by_username('test-admin')
@@ -541,7 +538,7 @@ I'm not here'''
 
 class TestUserNotificationTasks(TestController):
     def setUp(self):
-        super(TestUserNotificationTasks, self).setUp()
+        super().setUp()
         self.setup_with_tools()
 
     @td.with_wiki
@@ -612,7 +609,7 @@ class _TestArtifact(M.Artifact):
 
     def index(self):
         return dict(
-            super(_TestArtifact, self).index(),
+            super().index(),
             text=self.text)
 
 
diff --git a/Allura/allura/tests/test_utils.py b/Allura/allura/tests/test_utils.py
index 2e48858..bc73595 100644
--- a/Allura/allura/tests/test_utils.py
+++ b/Allura/allura/tests/test_utils.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-
 #       Licensed to the Apache Software Foundation (ASF) under one
 #       or more contributor license agreements.  See the NOTICE file
 #       distributed with this work for additional information
@@ -22,7 +20,6 @@ import time
 import unittest
 import datetime as dt
 from os import path
-from io import open
 
 import six
 import ming
@@ -47,7 +44,6 @@ from alluratest.controller import setup_unit_test
 from allura import model as M
 from allura.lib import utils
 from allura.lib import helpers as h
-from six.moves import range
 
 
 @patch.dict('allura.lib.utils.tg.config', clear=True, foo='bar', baz='true')
@@ -385,12 +381,12 @@ def test_skip_mod_date():
     assert getattr(session(M.Artifact)._get(), 'skip_mod_date', None) is False
 
 
-class FakeAttachment(object):
+class FakeAttachment:
     def __init__(self, filename):
         self._id = ObjectId()
         self.filename = filename
     def __repr__(self):
-        return '{} {}'.format(self._id, self.filename)
+        return f'{self._id} {self.filename}'
 
 
 def unique_attachments():
@@ -435,5 +431,5 @@ def test_urlencode():
     assert_equal(utils.urlencode({'a': 'hello'}),
                  'a=hello')
     # list of pairs - including unicode and bytes
-    assert_equal(utils.urlencode([('a', 1), ('b', 'ƒ'), ('c', 'ƒ'.encode('utf8'))]),
+    assert_equal(utils.urlencode([('a', 1), ('b', 'ƒ'), ('c', 'ƒ'.encode())]),
                  'a=1&b=%C6%92&c=%C6%92')
diff --git a/Allura/allura/tests/test_validators.py b/Allura/allura/tests/test_validators.py
index 361e65f..640ab9c 100644
--- a/Allura/allura/tests/test_validators.py
+++ b/Allura/allura/tests/test_validators.py
@@ -51,7 +51,7 @@ class TestJsonConverter(unittest.TestCase):
 class TestJsonFile(unittest.TestCase):
     val = v.JsonFile
 
-    class FieldStorage(object):
+    class FieldStorage:
 
         def __init__(self, content):
             self.value = content
@@ -67,7 +67,7 @@ class TestJsonFile(unittest.TestCase):
 class TestUserMapFile(unittest.TestCase):
     val = v.UserMapJsonFile()
 
-    class FieldStorage(object):
+    class FieldStorage:
 
         def __init__(self, content):
             self.value = content
diff --git a/Allura/allura/tests/test_webhooks.py b/Allura/allura/tests/test_webhooks.py
index ff82089..d7b87d2 100644
--- a/Allura/allura/tests/test_webhooks.py
+++ b/Allura/allura/tests/test_webhooks.py
@@ -49,7 +49,6 @@ from alluratest.controller import (
     TestRestApiBase,
 )
 import six
-from six.moves import range
 
 
 # important to be distinct from 'test' and 'test2' which ForgeGit and
@@ -60,7 +59,7 @@ with_git = td.with_tool(test_project_with_repo, 'git', 'src', 'Git')
 with_git2 = td.with_tool(test_project_with_repo, 'git', 'src2', 'Git2')
 
 
-class TestWebhookBase(object):
+class TestWebhookBase:
     def setUp(self):
         setup_basic_test()
         self.patches = self.monkey_patch()
@@ -125,12 +124,12 @@ class TestValidators(TestWebhookBase):
         wh.app_config_id = app.config._id
         session(wh).flush(wh)
         assert_equal(v.to_python(wh._id), wh)
-        assert_equal(v.to_python(six.text_type(wh._id)), wh)
+        assert_equal(v.to_python(str(wh._id)), wh)
 
 
 class TestWebhookController(TestController):
     def setUp(self):
-        super(TestWebhookController, self).setUp()
+        super().setUp()
         self.patches = self.monkey_patch()
         for p in self.patches:
             p.start()
@@ -140,7 +139,7 @@ class TestWebhookController(TestController):
         self.url = str(self.git.admin_url + 'webhooks')
 
     def tearDown(self):
-        super(TestWebhookController, self).tearDown()
+        super().tearDown()
         for p in self.patches:
             p.stop()
 
@@ -182,10 +181,10 @@ class TestWebhookController(TestController):
     def test_access(self):
         self.app.get(self.url + '/repo-push/')
         self.app.get(self.url + '/repo-push/',
-                     extra_environ={'username': str('test-user')},
+                     extra_environ={'username': 'test-user'},
                      status=403)
         r = self.app.get(self.url + '/repo-push/',
-                         extra_environ={'username': str('*anonymous')},
+                         extra_environ={'username': '*anonymous'},
                          status=302)
         assert_equal(r.location,
                      'http://localhost/auth/'
@@ -277,7 +276,7 @@ class TestWebhookController(TestController):
         form = r.forms[0]
         assert_equal(form['url'].value, data1['url'])
         assert_equal(form['secret'].value, data1['secret'])
-        assert_equal(form['webhook'].value, six.text_type(wh1._id))
+        assert_equal(form['webhook'].value, str(wh1._id))
         form['url'] = 'http://host.org/hook'
         form['secret'] = 'new secret'
         msg = 'edit webhook repo-push\n{} => {}\n{}'.format(
@@ -320,7 +319,7 @@ class TestWebhookController(TestController):
         # invalid id in hidden field, just in case
         r = self.app.get(self.url + '/repo-push/%s' % wh._id)
         data = {k: v[0].value for (k, v) in r.forms[0].fields.items() if k}
-        data['webhook'] = six.text_type(invalid._id)
+        data['webhook'] = str(invalid._id)
         self.app.post(self.url + '/repo-push/edit', data, status=404)
 
         # empty values
@@ -339,7 +338,7 @@ class TestWebhookController(TestController):
         self.create_webhook(data).follow()
         assert_equal(M.Webhook.query.find().count(), 1)
         wh = M.Webhook.query.get(hook_url=data['url'])
-        data = {'webhook': six.text_type(wh._id)}
+        data = {'webhook': str(wh._id)}
         msg = 'delete webhook repo-push {} {}'.format(
             wh.hook_url, self.git.config.url())
         with td.audits(msg):
@@ -359,7 +358,7 @@ class TestWebhookController(TestController):
         data = {'webhook': ''}
         self.app.post(self.url + '/repo-push/delete', data, status=404)
 
-        data = {'webhook': six.text_type(invalid._id)}
+        data = {'webhook': str(invalid._id)}
         self.app.post(self.url + '/repo-push/delete', data, status=404)
         assert_equal(M.Webhook.query.find().count(), 1)
 
@@ -421,7 +420,7 @@ class TestWebhookController(TestController):
 
 class TestSendWebhookHelper(TestWebhookBase):
     def setUp(self, *args, **kw):
-        super(TestSendWebhookHelper, self).setUp(*args, **kw)
+        super().setUp(*args, **kw)
         self.payload = {'some': ['data', 23]}
         self.h = SendWebhookHelper(self.wh, self.payload)
 
@@ -451,7 +450,7 @@ class TestSendWebhookHelper(TestWebhookBase):
         response = Mock(
             status_code=500,
             text='that is why',
-            headers={str('Content-Type'): str('application/json')})
+            headers={'Content-Type': 'application/json'})
         assert_equal(
             self.h.log_msg('Error', response=response),
             "Error: repo-push http://httpbin.org/post /adobe/adobe-1/src/ 500 "
@@ -593,7 +592,7 @@ class TestRepoPushWebhookSender(TestWebhookBase):
                 webhook = M.Webhook(
                     type='repo-push',
                     app_config_id=self.git.config._id,
-                    hook_url='http://httpbin.org/{}/{}'.format(suffix, i),
+                    hook_url=f'http://httpbin.org/{suffix}/{i}',
                     secret='secret')
                 session(webhook).flush(webhook)
 
@@ -632,7 +631,7 @@ class TestRepoPushWebhookSender(TestWebhookBase):
 class TestModels(TestWebhookBase):
     def test_webhook_url(self):
         assert_equal(self.wh.url(),
-                     '/adobe/adobe-1/admin/src/webhooks/repo-push/{}'.format(self.wh._id))
+                     f'/adobe/adobe-1/admin/src/webhooks/repo-push/{self.wh._id}')
 
     def test_webhook_enforce_limit(self):
         self.wh.last_sent = None
@@ -660,7 +659,7 @@ class TestModels(TestWebhookBase):
 
     def test_json(self):
         expected = {
-            '_id': six.text_type(self.wh._id),
+            '_id': str(self.wh._id),
             'url': 'http://localhost/rest/adobe/adobe-1/admin'
                    '/src/webhooks/repo-push/{}'.format(self.wh._id),
             'type': 'repo-push',
@@ -672,7 +671,7 @@ class TestModels(TestWebhookBase):
 
 class TestWebhookRestController(TestRestApiBase):
     def setUp(self):
-        super(TestWebhookRestController, self).setUp()
+        super().setUp()
         self.patches = self.monkey_patch()
         for p in self.patches:
             p.start()
@@ -685,13 +684,13 @@ class TestWebhookRestController(TestRestApiBase):
             webhook = M.Webhook(
                 type='repo-push',
                 app_config_id=self.git.config._id,
-                hook_url='http://httpbin.org/post/{}'.format(i),
-                secret='secret-{}'.format(i))
+                hook_url=f'http://httpbin.org/post/{i}',
+                secret=f'secret-{i}')
             session(webhook).flush(webhook)
             self.webhooks.append(webhook)
 
     def tearDown(self):
-        super(TestWebhookRestController, self).tearDown()
+        super().tearDown()
         for p in self.patches:
             p.stop()
 
@@ -713,12 +712,12 @@ class TestWebhookRestController(TestRestApiBase):
     def test_webhooks_list(self):
         r = self.api_get(self.url)
         webhooks = [{
-            '_id': six.text_type(wh._id),
+            '_id': str(wh._id),
             'url': 'http://localhost/rest/adobe/adobe-1/admin'
                    '/src/webhooks/repo-push/{}'.format(wh._id),
             'type': 'repo-push',
-            'hook_url': 'http://httpbin.org/post/{}'.format(n),
-            'mod_date': six.text_type(wh.mod_date),
+            'hook_url': f'http://httpbin.org/post/{n}',
+            'mod_date': str(wh.mod_date),
         } for n, wh in enumerate(self.webhooks)]
         expected = {
             'webhooks': webhooks,
@@ -731,14 +730,14 @@ class TestWebhookRestController(TestRestApiBase):
 
     def test_webhook_GET(self):
         webhook = self.webhooks[0]
-        r = self.api_get('{}/repo-push/{}'.format(self.url, webhook._id))
+        r = self.api_get(f'{self.url}/repo-push/{webhook._id}')
         expected = {
-            '_id': six.text_type(webhook._id),
+            '_id': str(webhook._id),
             'url': 'http://localhost/rest/adobe/adobe-1/admin'
                    '/src/webhooks/repo-push/{}'.format(webhook._id),
             'type': 'repo-push',
             'hook_url': 'http://httpbin.org/post/0',
-            'mod_date': six.text_type(webhook.mod_date),
+            'mod_date': str(webhook.mod_date),
         }
         dd.assert_equal(r.status_int, 200)
         dd.assert_equal(r.json, expected)
@@ -777,12 +776,12 @@ class TestWebhookRestController(TestRestApiBase):
         webhook = M.Webhook.query.get(hook_url=data['url'])
         assert_equal(webhook.secret, 'super-secret')  # secret generated
         expected = {
-            '_id': six.text_type(webhook._id),
+            '_id': str(webhook._id),
             'url': 'http://localhost/rest/adobe/adobe-1/admin'
                    '/src/webhooks/repo-push/{}'.format(webhook._id),
             'type': 'repo-push',
             'hook_url': data['url'],
-            'mod_date': six.text_type(webhook.mod_date),
+            'mod_date': str(webhook.mod_date),
         }
         dd.assert_equal(r.json, expected)
         assert_equal(M.Webhook.query.find().count(), len(self.webhooks) + 1)
@@ -813,7 +812,7 @@ class TestWebhookRestController(TestRestApiBase):
 
     def test_edit_validation(self):
         webhook = self.webhooks[0]
-        url = '{}/repo-push/{}'.format(self.url, webhook._id)
+        url = f'{self.url}/repo-push/{webhook._id}'
         data = {'url': 'qwe', 'secret': 'qwe'}
         r = self.api_post(url, status=400, **data)
         expected = {
@@ -826,7 +825,7 @@ class TestWebhookRestController(TestRestApiBase):
 
     def test_edit(self):
         webhook = self.webhooks[0]
-        url = '{}/repo-push/{}'.format(self.url, webhook._id)
+        url = f'{self.url}/repo-push/{webhook._id}'
         # change only url
         data = {'url': 'http://hook.slack.com/abcd'}
         msg = ('edit webhook repo-push\n'
@@ -837,12 +836,12 @@ class TestWebhookRestController(TestRestApiBase):
         assert_equal(webhook.hook_url, data['url'])
         assert_equal(webhook.secret, 'secret-0')
         expected = {
-            '_id': six.text_type(webhook._id),
+            '_id': str(webhook._id),
             'url': 'http://localhost/rest/adobe/adobe-1/admin'
                    '/src/webhooks/repo-push/{}'.format(webhook._id),
             'type': 'repo-push',
             'hook_url': data['url'],
-            'mod_date': six.text_type(webhook.mod_date),
+            'mod_date': str(webhook.mod_date),
         }
         dd.assert_equal(r.json, expected)
 
@@ -857,18 +856,18 @@ class TestWebhookRestController(TestRestApiBase):
         assert_equal(webhook.hook_url, 'http://hook.slack.com/abcd')
         assert_equal(webhook.secret, 'new-secret')
         expected = {
-            '_id': six.text_type(webhook._id),
+            '_id': str(webhook._id),
             'url': 'http://localhost/rest/adobe/adobe-1/admin'
                    '/src/webhooks/repo-push/{}'.format(webhook._id),
             'type': 'repo-push',
             'hook_url': 'http://hook.slack.com/abcd',
-            'mod_date': six.text_type(webhook.mod_date),
+            'mod_date': str(webhook.mod_date),
         }
         dd.assert_equal(r.json, expected)
 
     def test_edit_duplicates(self):
         webhook = self.webhooks[0]
-        url = '{}/repo-push/{}'.format(self.url, webhook._id)
+        url = f'{self.url}/repo-push/{webhook._id}'
         data = {'url': 'http://httpbin.org/post/1'}
         r = self.api_post(url, status=400, **data)
         expected = {'result': 'error',
@@ -877,13 +876,13 @@ class TestWebhookRestController(TestRestApiBase):
         assert_equal(r.json, expected)
 
     def test_delete_validation(self):
-        url = '{}/repo-push/invalid'.format(self.url)
+        url = f'{self.url}/repo-push/invalid'
         self.api_delete(url, status=404)
 
     def test_delete(self):
         assert_equal(M.Webhook.query.find().count(), 3)
         webhook = self.webhooks[0]
-        url = '{}/repo-push/{}'.format(self.url, webhook._id)
+        url = f'{self.url}/repo-push/{webhook._id}'
         msg = 'delete webhook repo-push {} {}'.format(
             webhook.hook_url, self.git.config.url())
         with td.audits(msg):
diff --git a/Allura/allura/tests/unit/__init__.py b/Allura/allura/tests/unit/__init__.py
index 27eb1e5..b645099 100644
--- a/Allura/allura/tests/unit/__init__.py
+++ b/Allura/allura/tests/unit/__init__.py
@@ -23,7 +23,7 @@ def setUp(self):
     setup_basic_test()
 
 
-class MockPatchTestCase(object):
+class MockPatchTestCase:
     patches = []
 
     def setUp(self):
@@ -33,11 +33,11 @@ class MockPatchTestCase(object):
 
     def tearDown(self):
         for patch_instance in self._patch_instances:
-            patch_instance.__exit__()
+            patch_instance.__exit__(None, None, None)
 
 
 class WithDatabase(MockPatchTestCase):
 
     def setUp(self):
-        super(WithDatabase, self).setUp()
+        super().setUp()
         clear_all_database_tables()
diff --git a/Allura/allura/tests/unit/controllers/test_discussion_moderation_controller.py b/Allura/allura/tests/unit/controllers/test_discussion_moderation_controller.py
index 7405738..c2c366c 100644
--- a/Allura/allura/tests/unit/controllers/test_discussion_moderation_controller.py
+++ b/Allura/allura/tests/unit/controllers/test_discussion_moderation_controller.py
@@ -34,7 +34,7 @@ class TestWhenModerating(WithDatabase):
                patches.disable_notifications_patch]
 
     def setUp(self):
-        super(TestWhenModerating, self).setUp()
+        super().setUp()
         post = create_post('mypost')
         discussion_controller = Mock(
             discussion=Mock(_id=post.discussion_id),
@@ -86,7 +86,7 @@ class TestIndexWithAPostInTheDiscussion(WithDatabase):
     patches = [patches.fake_app_patch]
 
     def setUp(self):
-        super(TestIndexWithAPostInTheDiscussion, self).setUp()
+        super().setUp()
         self.post = create_post('mypost')
         discussion = self.post.discussion
         self.template_variables = show_moderation_index(discussion)
diff --git a/Allura/allura/tests/unit/factories.py b/Allura/allura/tests/unit/factories.py
index 2cf6ee8..bc1da54 100644
--- a/Allura/allura/tests/unit/factories.py
+++ b/Allura/allura/tests/unit/factories.py
@@ -66,7 +66,7 @@ def create_post(slug):
     author = create_user(username='someguy')
     return Post(slug=slug,
                 thread_id=thread._id,
-                full_slug='{}:{}'.format(thread._id, slug),
+                full_slug=f'{thread._id}:{slug}',
                 discussion_id=discussion._id,
                 author_id=author._id)
 
diff --git a/Allura/allura/tests/unit/phone/test_nexmo.py b/Allura/allura/tests/unit/phone/test_nexmo.py
index 7434de2..32b8615 100644
--- a/Allura/allura/tests/unit/phone/test_nexmo.py
+++ b/Allura/allura/tests/unit/phone/test_nexmo.py
@@ -23,7 +23,7 @@ from alluratest.tools import assert_in, assert_not_in
 from allura.lib.phone.nexmo import NexmoPhoneService
 
 
-class TestPhoneService(object):
+class TestPhoneService:
 
     def setUp(self):
         config = {'phone.api_key': 'test-api-key',
diff --git a/Allura/allura/tests/unit/phone/test_phone_service.py b/Allura/allura/tests/unit/phone/test_phone_service.py
index 34f7a74..20f145b 100644
--- a/Allura/allura/tests/unit/phone/test_phone_service.py
+++ b/Allura/allura/tests/unit/phone/test_phone_service.py
@@ -30,7 +30,7 @@ class MockPhoneService(PhoneService):
         return {'status': 'ok'}
 
 
-class TestPhoneService(object):
+class TestPhoneService:
 
     def test_verify(self):
         res = PhoneService({}).verify('1234567890')
diff --git a/Allura/allura/tests/unit/spam/test_akismet.py b/Allura/allura/tests/unit/spam/test_akismet.py
index caabf95..8dc5a05 100644
--- a/Allura/allura/tests/unit/spam/test_akismet.py
+++ b/Allura/allura/tests/unit/spam/test_akismet.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-
 #       Licensed to the Apache Software Foundation (ASF) under one
 #       or more contributor license agreements.  See the NOTICE file
 #       distributed with this work for additional information
@@ -113,7 +111,7 @@ class TestAkismet(unittest.TestCase):
         c.user = None
         self.akismet.check(self.content, user=self.fake_user)
         expected_data = self.expected_data
-        expected_data.update(comment_author='Søme User'.encode('utf8'),
+        expected_data.update(comment_author='Søme User'.encode(),
                              comment_author_email=b'user@domain')
         self.akismet.service.comment_check.assert_called_once_with(**expected_data)
 
@@ -125,7 +123,7 @@ class TestAkismet(unittest.TestCase):
         c.user = self.fake_user
         self.akismet.check(self.content)
         expected_data = self.expected_data
-        expected_data.update(comment_author='Søme User'.encode('utf8'),
+        expected_data.update(comment_author='Søme User'.encode(),
                              comment_author_email=b'user@domain')
         self.akismet.service.comment_check.assert_called_once_with(**expected_data)
 
@@ -136,7 +134,7 @@ class TestAkismet(unittest.TestCase):
         self.akismet.submit_spam(self.content)
 
         # no IP addr, UA, etc, since this isn't the original request
-        expected_data = dict(comment_content='spåm text'.encode('utf8'),
+        expected_data = dict(comment_content='spåm text'.encode(),
                              comment_type=b'comment',
                              user_ip=b'',
                              user_agent=b'',
@@ -150,7 +148,7 @@ class TestAkismet(unittest.TestCase):
         self.akismet.submit_ham(self.content)
 
         # no IP addr, UA, etc, since this isn't the original request
-        expected_data = dict(comment_content='spåm text'.encode('utf8'),
+        expected_data = dict(comment_content='spåm text'.encode(),
                              comment_type=b'comment',
                              user_ip=b'',
                              user_agent=b'',
@@ -163,7 +161,7 @@ class TestAkismet(unittest.TestCase):
 
         self.akismet.submit_ham(self.content, artifact=self.fake_artifact)
 
-        expected_data = dict(comment_content='spåm text'.encode('utf8'),
+        expected_data = dict(comment_content='spåm text'.encode(),
                              comment_type=b'comment',
                              user_ip=b'33.4.5.66',
                              user_agent=b'',
diff --git a/Allura/allura/tests/unit/spam/test_spam_filter.py b/Allura/allura/tests/unit/spam/test_spam_filter.py
index 117acee..f9a8f59 100644
--- a/Allura/allura/tests/unit/spam/test_spam_filter.py
+++ b/Allura/allura/tests/unit/spam/test_spam_filter.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-
 #       Licensed to the Apache Software Foundation (ASF) under one
 #       or more contributor license agreements.  See the NOTICE file
 #       distributed with this work for additional information
@@ -76,7 +74,7 @@ class TestSpamFilter(unittest.TestCase):
         self.assertTrue(log.exception.called)
 
 
-class TestSpamFilterFunctional(object):
+class TestSpamFilterFunctional:
 
     def setUp(self):
         setup_basic_test()
@@ -95,7 +93,7 @@ class TestSpamFilterFunctional(object):
         assert_equal(results[0].user.username, 'test-user')
 
 
-class TestChainedSpamFilter(object):
+class TestChainedSpamFilter:
 
     def test(self):
         config = {'spam.method': 'mock1 mock2', 'spam.settingA': 'bcd'}
diff --git a/Allura/allura/tests/unit/spam/test_stopforumspam.py b/Allura/allura/tests/unit/spam/test_stopforumspam.py
index a702bb3..2ed47ea 100644
--- a/Allura/allura/tests/unit/spam/test_stopforumspam.py
+++ b/Allura/allura/tests/unit/spam/test_stopforumspam.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-
 #       Licensed to the Apache Software Foundation (ASF) under one
 #       or more contributor license agreements.  See the NOTICE file
 #       distributed with this work for additional information
@@ -26,7 +24,7 @@ from alluratest.tools import assert_equal
 from allura.lib.spam.stopforumspamfilter import StopForumSpamSpamFilter
 
 
-class TestStopForumSpam(object):
+class TestStopForumSpam:
 
     def setUp(self):
         self.content = 'spåm text'
diff --git a/Allura/allura/tests/unit/test_app.py b/Allura/allura/tests/unit/test_app.py
index b6d7610..47b5c2b 100644
--- a/Allura/allura/tests/unit/test_app.py
+++ b/Allura/allura/tests/unit/test_app.py
@@ -70,7 +70,7 @@ class TestDefaultDiscussion(WithDatabase):
     patches = [fake_app_patch]
 
     def setUp(self):
-        super(TestDefaultDiscussion, self).setUp()
+        super().setUp()
         install_app()
         self.discussion = model.Discussion.query.get(
             shortname='my_mounted_app')
@@ -90,7 +90,7 @@ class TestAppDefaults(WithDatabase):
     patches = [fake_app_patch]
 
     def setUp(self):
-        super(TestAppDefaults, self).setUp()
+        super().setUp()
         self.app = install_app()
 
     def test_that_it_has_an_empty_sidebar_menu(self):
diff --git a/Allura/allura/tests/unit/test_helpers/test_ago.py b/Allura/allura/tests/unit/test_helpers/test_ago.py
index 517ecfb..159d0c7 100644
--- a/Allura/allura/tests/unit/test_helpers/test_ago.py
+++ b/Allura/allura/tests/unit/test_helpers/test_ago.py
@@ -23,7 +23,7 @@ from alluratest.tools import assert_equal
 from allura.lib import helpers
 
 
-class TestAgo(object):
+class TestAgo:
 
     def setUp(self):
         self.start_time = datetime(2010, 1, 1, 0, 0, 0)
diff --git a/Allura/allura/tests/unit/test_helpers/test_set_context.py b/Allura/allura/tests/unit/test_helpers/test_set_context.py
index 7a65433..194394a 100644
--- a/Allura/allura/tests/unit/test_helpers/test_set_context.py
+++ b/Allura/allura/tests/unit/test_helpers/test_set_context.py
@@ -31,7 +31,7 @@ from allura.tests.unit.factories import (create_project,
 class TestWhenProjectIsFoundAndAppIsNot(WithDatabase):
 
     def setUp(self):
-        super(TestWhenProjectIsFoundAndAppIsNot, self).setUp()
+        super().setUp()
         self.myproject = create_project('myproject')
         set_context('myproject', neighborhood=self.myproject.neighborhood)
 
@@ -45,7 +45,7 @@ class TestWhenProjectIsFoundAndAppIsNot(WithDatabase):
 class TestWhenProjectIsFoundInNeighborhood(WithDatabase):
 
     def setUp(self):
-        super(TestWhenProjectIsFoundInNeighborhood, self).setUp()
+        super().setUp()
         self.myproject = create_project('myproject')
         set_context('myproject', neighborhood=self.myproject.neighborhood)
 
@@ -60,7 +60,7 @@ class TestWhenAppIsFoundByID(WithDatabase):
     patches = [patches.project_app_loading_patch]
 
     def setUp(self):
-        super(TestWhenAppIsFoundByID, self).setUp()
+        super().setUp()
         self.myproject = create_project('myproject')
         self.app_config = create_app_config(self.myproject, 'my_mounted_app')
         set_context('myproject', app_config_id=self.app_config._id,
@@ -77,7 +77,7 @@ class TestWhenAppIsFoundByMountPoint(WithDatabase):
     patches = [patches.project_app_loading_patch]
 
     def setUp(self):
-        super(TestWhenAppIsFoundByMountPoint, self).setUp()
+        super().setUp()
         self.myproject = create_project('myproject')
         self.app_config = create_app_config(self.myproject, 'my_mounted_app')
         set_context('myproject', mount_point='my_mounted_app',
diff --git a/Allura/allura/tests/unit/test_ldap_auth_provider.py b/Allura/allura/tests/unit/test_ldap_auth_provider.py
index 90dfb08..e0633f5 100644
--- a/Allura/allura/tests/unit/test_ldap_auth_provider.py
+++ b/Allura/allura/tests/unit/test_ldap_auth_provider.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-
 #       Licensed to the Apache Software Foundation (ASF) under one
 #       or more contributor license agreements.  See the NOTICE file
 #       distributed with this work for additional information
@@ -36,7 +34,7 @@ from allura import model as M
 import six
 
 
-class TestLdapAuthenticationProvider(object):
+class TestLdapAuthenticationProvider:
 
     def setUp(self):
         setup_basic_test()
@@ -76,7 +74,7 @@ class TestLdapAuthenticationProvider(object):
             'password': 'test-password',
         }
         self.provider.request.method = 'POST'
-        self.provider.request.body = '&'.join(['{}={}'.format(k,v) for k,v in six.iteritems(params)]).encode('utf-8')
+        self.provider.request.body = '&'.join([f'{k}={v}' for k,v in params.items()]).encode('utf-8')
         ldap.dn.escape_dn_chars = lambda x: x
 
         self.provider._login()
@@ -95,11 +93,11 @@ class TestLdapAuthenticationProvider(object):
             'password': 'test-password',
         }
         self.provider.request.method = 'POST'
-        self.provider.request.body = '&'.join(['{}={}'.format(k,v) for k,v in six.iteritems(params)]).encode('utf-8')
+        self.provider.request.body = '&'.join([f'{k}={v}' for k,v in params.items()]).encode('utf-8')
         ldap.dn.escape_dn_chars = lambda x: x
         dn = 'uid=%s,ou=people,dc=localdomain' % params['username']
         conn = ldap.initialize.return_value
-        conn.search_s.return_value = [(dn, {'cn': ['åℒƒ'.encode('utf-8')]})]
+        conn.search_s.return_value = [(dn, {'cn': ['åℒƒ'.encode()]})]
 
         self.provider._login()
 
diff --git a/Allura/allura/tests/unit/test_mixins.py b/Allura/allura/tests/unit/test_mixins.py
index cdc3aaa..0e429d6 100644
--- a/Allura/allura/tests/unit/test_mixins.py
+++ b/Allura/allura/tests/unit/test_mixins.py
@@ -19,7 +19,7 @@ from mock import Mock
 from allura.model import VotableArtifact
 
 
-class TestVotableArtifact(object):
+class TestVotableArtifact:
 
     def setUp(self):
         self.user1 = Mock()
diff --git a/Allura/allura/tests/unit/test_post_model.py b/Allura/allura/tests/unit/test_post_model.py
index 1e74077..6d6bc4e 100644
--- a/Allura/allura/tests/unit/test_post_model.py
+++ b/Allura/allura/tests/unit/test_post_model.py
@@ -22,8 +22,6 @@ from allura import model as M
 from allura.tests.unit import WithDatabase
 from allura.tests.unit import patches
 from allura.tests.unit.factories import create_post
-from six.moves import range
-from six.moves import map
 
 
 class TestPostModel(WithDatabase):
@@ -31,7 +29,7 @@ class TestPostModel(WithDatabase):
                patches.disable_notifications_patch]
 
     def setUp(self):
-        super(TestPostModel, self).setUp()
+        super().setUp()
         self.post = create_post('mypost')
 
     def test_that_it_is_pending_by_default(self):
diff --git a/Allura/allura/tests/unit/test_repo.py b/Allura/allura/tests/unit/test_repo.py
index 165dae2..648c330 100644
--- a/Allura/allura/tests/unit/test_repo.py
+++ b/Allura/allura/tests/unit/test_repo.py
@@ -304,7 +304,7 @@ class TestPrefixPathsUnion(unittest.TestCase):
         self.assertEqual(prefix_paths_union(a, b), {'a2'})
 
 
-class TestGroupCommits(object):
+class TestGroupCommits:
 
     def setUp(self):
         self.repo = Mock()
diff --git a/Allura/allura/tests/unit/test_session.py b/Allura/allura/tests/unit/test_session.py
index fe1868e..f71e1f0 100644
--- a/Allura/allura/tests/unit/test_session.py
+++ b/Allura/allura/tests/unit/test_session.py
@@ -28,8 +28,6 @@ from allura.model.session import (
     ArtifactSessionExtension,
     substitute_extensions,
 )
-from six.moves import range
-from six.moves import map
 
 
 def test_extensions_cm():
diff --git a/Allura/allura/version.py b/Allura/allura/version.py
index b813104..e880449 100644
--- a/Allura/allura/version.py
+++ b/Allura/allura/version.py
@@ -15,6 +15,5 @@
 #       specific language governing permissions and limitations
 #       under the License.
 
-from six.moves import map
 __version_info__ = (0, 1)
 __version__ = '.'.join(map(str, __version_info__))
diff --git a/Allura/allura/webhooks.py b/Allura/allura/webhooks.py
index 537bc92..a56f18e 100644
--- a/Allura/allura/webhooks.py
+++ b/Allura/allura/webhooks.py
@@ -43,7 +43,6 @@ from allura.lib.decorators import require_post, task
 from allura.lib.utils import DateJSONEncoder
 from allura import model as M
 import six
-from six.moves import map
 
 
 log = logging.getLogger(__name__)
@@ -53,7 +52,7 @@ class WebhookValidator(fev.FancyValidator):
     def __init__(self, sender, app, **kw):
         self.app = app
         self.sender = sender
-        super(WebhookValidator, self).__init__(**kw)
+        super().__init__(**kw)
 
     def _to_python(self, value, state):
         wh = None
@@ -78,7 +77,7 @@ class WebhookCreateForm(schema.Schema):
 
 class WebhookEditForm(WebhookCreateForm):
     def __init__(self, sender, app):
-        super(WebhookEditForm, self).__init__()
+        super().__init__()
         self.add_field('webhook', WebhookValidator(
             sender=sender, app=app, not_empty=True))
 
@@ -101,12 +100,12 @@ class WebhookControllerMeta(type):
         return type.__call__(cls, sender, app, *args, **kw)
 
 
-class WebhookController(six.with_metaclass(WebhookControllerMeta, BaseController, AdminControllerMixin)):
+class WebhookController(BaseController, AdminControllerMixin, metaclass=WebhookControllerMeta):
     create_form = WebhookCreateForm
     edit_form = WebhookEditForm
 
     def __init__(self, sender, app):
-        super(WebhookController, self).__init__()
+        super().__init__()
         self.sender = sender()
         self.app = app
 
@@ -202,7 +201,7 @@ class WebhookController(six.with_metaclass(WebhookControllerMeta, BaseController
             raise exc.HTTPNotFound()
         c.form_values = {'url': kw.get('url') or wh.hook_url,
                          'secret': kw.get('secret') or wh.secret,
-                         'webhook': six.text_type(wh._id)}
+                         'webhook': str(wh._id)}
         return {'sender': self.sender,
                 'action': 'edit',
                 'form': form}
@@ -210,7 +209,7 @@ class WebhookController(six.with_metaclass(WebhookControllerMeta, BaseController
 
 class WebhookRestController(BaseController):
     def __init__(self, sender, app):
-        super(WebhookRestController, self).__init__()
+        super().__init__()
         self.sender = sender()
         self.app = app
         self.create_form = WebhookController.create_form
@@ -220,8 +219,8 @@ class WebhookRestController(BaseController):
         error = getattr(e, 'error_dict', None)
         if error:
             _error = {}
-            for k, val in six.iteritems(error):
-                _error[k] = six.text_type(val)
+            for k, val in error.items():
+                _error[k] = str(val)
             return _error
         error = getattr(e, 'msg', None)
         if not error:
@@ -235,7 +234,7 @@ class WebhookRestController(BaseController):
     @expose('json:')
     @require_post()
     def index(self, **kw):
-        response.content_type = str('application/json')
+        response.content_type = 'application/json'
         try:
             params = {'secret': kw.pop('secret', ''),
                       'url': kw.pop('url', None)}
@@ -299,7 +298,7 @@ class WebhookRestController(BaseController):
         try:
             params = {'secret': kw.pop('secret', old_secret),
                       'url': kw.pop('url', old_url),
-                      'webhook': six.text_type(webhook._id)}
+                      'webhook': str(webhook._id)}
             valid = form.to_python(params)
         except Exception as e:
             response.status_int = 400
@@ -328,7 +327,7 @@ class WebhookRestController(BaseController):
         return {'result': 'ok'}
 
 
-class SendWebhookHelper(object):
+class SendWebhookHelper:
     def __init__(self, webhook, payload):
         self.webhook = webhook
         self.payload = payload
@@ -405,7 +404,7 @@ def send_webhook(webhook_id, payload):
     SendWebhookHelper(webhook, payload).send()
 
 
-class WebhookSender(object):
+class WebhookSender:
     """Base class for webhook senders.
 
     Subclasses are required to implement :meth:`get_payload()` and set
diff --git a/Allura/allura/websetup/__init__.py b/Allura/allura/websetup/__init__.py
index 16540cb..4677516 100644
--- a/Allura/allura/websetup/__init__.py
+++ b/Allura/allura/websetup/__init__.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-
 #       Licensed to the Apache Software Foundation (ASF) under one
 #       or more contributor license agreements.  See the NOTICE file
 #       distributed with this work for additional information
diff --git a/Allura/allura/websetup/bootstrap.py b/Allura/allura/websetup/bootstrap.py
index 1095ac7..d7053f3 100644
--- a/Allura/allura/websetup/bootstrap.py
+++ b/Allura/allura/websetup/bootstrap.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-
 #       Licensed to the Apache Software Foundation (ASF) under one
 #       or more contributor license agreements.  See the NOTICE file
 #       distributed with this work for additional information
@@ -43,8 +41,6 @@ from allura.websetup.schema import REGISTRY
 
 from forgewiki import model as WM
 import six
-from six.moves import range
-from six.moves import input
 
 log = logging.getLogger(__name__)
 
@@ -253,7 +249,7 @@ def bootstrap(command, conf, vars):
             log.info('Registering initial apps')
             with h.push_config(c, user=u_admin):
                 p0.install_apps([{'ep_name': ep_name}
-                                 for ep_name, app in six.iteritems(g.entry_points['tool'])
+                                 for ep_name, app in g.entry_points['tool'].items()
                                  if app._installable(tool_name=ep_name,
                                                      nbhd=n_projects,
                                                      project_tools=[])
diff --git a/Allura/allura/websetup/schema.py b/Allura/allura/websetup/schema.py
index 2eb2448..52bfd97 100644
--- a/Allura/allura/websetup/schema.py
+++ b/Allura/allura/websetup/schema.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-
 #       Licensed to the Apache Software Foundation (ASF) under one
 #       or more contributor license agreements.  See the NOTICE file
 #       distributed with this work for additional information
@@ -56,5 +54,5 @@ def setup_schema(command, conf, vars):
     log.info('setup_schema called')
... 5502 lines suppressed ...